Compare commits
88 Commits
open-relea
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73610bf8a0 | ||
|
|
1c025f0af7 | ||
|
|
2213d45461 | ||
|
|
757d9674cb | ||
|
|
3302555a47 | ||
|
|
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_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||
EXAMS_BASE_URL=''
|
||||
FAVICON_URL=''
|
||||
IGNORED_ERROR_REGEX=''
|
||||
INSIGHTS_BASE_URL=''
|
||||
@@ -28,6 +29,8 @@ LOGO_WHITE_URL=''
|
||||
LEGACY_THEME_NAME=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
ORDER_HISTORY_URL=''
|
||||
PROCTORED_EXAM_FAQ_URL=''
|
||||
PROCTORED_EXAM_RULES_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
SEARCH_CATALOG_URL=''
|
||||
SEGMENT_KEY=''
|
||||
|
||||
@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||
EXAMS_BASE_URL='http://localhost:18740'
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
IGNORED_ERROR_REGEX=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
@@ -28,6 +29,8 @@ LEGACY_THEME_NAME=''
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PORT=2000
|
||||
PROCTORED_EXAM_FAQ_URL=''
|
||||
PROCTORED_EXAM_RULES_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
SEGMENT_KEY=''
|
||||
|
||||
@@ -15,6 +15,7 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||
EXAMS_BASE_URL='http://localhost:18740'
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
IGNORED_ERROR_REGEX=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
@@ -28,6 +29,8 @@ LEGACY_THEME_NAME=''
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PORT=2000
|
||||
PROCTORED_EXAM_FAQ_URL=''
|
||||
PROCTORED_EXAM_RULES_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
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');
|
||||
|
||||
module.exports = createConfig('eslint', {
|
||||
overrides: [{
|
||||
files: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)", "setupTest.js"],
|
||||
rules: {
|
||||
'import/named': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
},
|
||||
}],
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
// TODO: all these rules should be renabled/addressed. temporarily turned off to unblock a release.
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': '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:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
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
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
12
.github/workflows/update-browserslist-db.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Update Browserslist DB
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-browserslist:
|
||||
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
|
||||
|
||||
secrets:
|
||||
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}
|
||||
7
.github/workflows/validate.yml
vendored
7
.github/workflows/validate.yml
vendored
@@ -9,14 +9,13 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
- run: make validate.ci
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
2
Makefile
2
Makefile
@@ -1,5 +1,5 @@
|
||||
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
|
||||
i18n = ./src/i18n
|
||||
|
||||
@@ -9,4 +9,6 @@ module.exports = createConfig('jest', {
|
||||
'src/i18n',
|
||||
'src/.*\\.exp\\..*',
|
||||
],
|
||||
testTimeout: 30000,
|
||||
testEnvironment: 'jsdom'
|
||||
});
|
||||
|
||||
47418
package-lock.json
generated
47418
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -29,51 +29,52 @@
|
||||
"url": "https://github.com/openedx/frontend-app-learning/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.2.4",
|
||||
"@edx/frontend-component-header": "2.4.6",
|
||||
"@edx/frontend-lib-special-exams": "1.16.3",
|
||||
"@edx/frontend-platform": "1.15.6",
|
||||
"@edx/paragon": "19.18.3",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-footer": "^12.0.0",
|
||||
"@edx/frontend-component-header": "^4.0.0",
|
||||
"@edx/frontend-lib-special-exams": "^2.16.1",
|
||||
"@edx/frontend-platform": "^4.2.0",
|
||||
"@edx/paragon": "20.28.4",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.18",
|
||||
"@popperjs/core": "2.11.5",
|
||||
"@popperjs/core": "2.11.6",
|
||||
"@reduxjs/toolkit": "1.8.1",
|
||||
"classnames": "2.3.1",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.22.2",
|
||||
"history": "^5.3.0",
|
||||
"history": "5.3.0",
|
||||
"js-cookie": "3.0.1",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "^7.1.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.8",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-share": "4.4.0",
|
||||
"react-share": "4.4.1",
|
||||
"redux": "4.1.2",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"reselect": "4.1.5",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.7",
|
||||
"truncate-html": "1.0.4",
|
||||
"util": "0.12.4"
|
||||
"util": "0.12.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "1.0.2",
|
||||
"@edx/frontend-build": "9.1.4",
|
||||
"@edx/reactifex": "2.0.1",
|
||||
"@edx/browserslist-config": "1.1.1",
|
||||
"@edx/frontend-build": "^12.8.27",
|
||||
"@edx/reactifex": "2.1.1",
|
||||
"@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/user-event": "13.5.0",
|
||||
"axios-mock-adapter": "1.20.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"es-check": "6.2.1",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"jest": "29.5.0",
|
||||
"rosie": "2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
@@ -8,18 +7,8 @@ import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
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 {
|
||||
accessExpiration,
|
||||
courseId,
|
||||
@@ -39,13 +28,6 @@ function AccessExpirationAlert({ intl, payload }) {
|
||||
upgradeUrl,
|
||||
} = accessExpiration;
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
if (showMMP2P) {
|
||||
return (
|
||||
<AccessExpirationAlertMMP2P payload={payload} />
|
||||
);
|
||||
}
|
||||
|
||||
const logClick = () => {
|
||||
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
|
||||
org_key: org,
|
||||
@@ -134,7 +116,7 @@ function AccessExpirationAlert({ intl, payload }) {
|
||||
{deadlineMessage}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AccessExpirationAlert.propTypes = {
|
||||
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 { PageBanner } from '@edx/paragon';
|
||||
|
||||
function AccessExpirationMasqueradeBanner({ payload }) {
|
||||
const AccessExpirationMasqueradeBanner = ({ payload }) => {
|
||||
const {
|
||||
expirationDate,
|
||||
userTimezone,
|
||||
@@ -27,7 +27,7 @@ function AccessExpirationMasqueradeBanner({ payload }) {
|
||||
/>
|
||||
</PageBanner>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AccessExpirationMasqueradeBanner.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
|
||||
@@ -7,17 +7,17 @@ const AccessExpirationMasqueradeBanner = React.lazy(() => import('./AccessExpira
|
||||
|
||||
function useAccessExpirationAlert(accessExpiration, courseId, org, userTimezone, topic, analyticsPageName) {
|
||||
const isVisible = accessExpiration && !accessExpiration.masqueradingExpiredCourse; // If it exists, show it.
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
accessExpiration,
|
||||
courseId,
|
||||
org,
|
||||
userTimezone,
|
||||
analyticsPageName,
|
||||
};
|
||||
}), [accessExpiration, analyticsPageName, courseId, org, userTimezone]);
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientAccessExpirationAlert',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic,
|
||||
});
|
||||
|
||||
@@ -34,14 +34,14 @@ export function useAccessExpirationMasqueradeBanner(courseId, tab) {
|
||||
|
||||
const isVisible = accessExpiration && accessExpiration.masqueradingExpiredCourse;
|
||||
const expirationDate = accessExpiration && accessExpiration.expirationDate;
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
expirationDate,
|
||||
userTimezone,
|
||||
};
|
||||
}), [expirationDate, userTimezone]);
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientAccessExpirationMasqueradeBanner',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'instructor-toolbar-alerts',
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { WarningFilled } from '@edx/paragon/icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import genericMessages from './messages';
|
||||
|
||||
function ActiveEnterpriseAlert({ intl, payload }) {
|
||||
const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
||||
const { text, courseId } = payload;
|
||||
const changeActiveEnterprise = (
|
||||
<Hyperlink
|
||||
@@ -35,7 +35,7 @@ function ActiveEnterpriseAlert({ intl, payload }) {
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ActiveEnterpriseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -19,9 +19,7 @@ describe('ActiveEnterpriseAlert', () => {
|
||||
it('Shows alert message and links', () => {
|
||||
render(<ActiveEnterpriseAlert {...mockData} />);
|
||||
expect(screen.getByRole('alert')).toBeInTheDocument();
|
||||
expect(screen.getByText('test message')).toBeInTheDocument();
|
||||
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`,
|
||||
);
|
||||
expect(screen.getByText('test message', { exact: false })).toBeInTheDocument();
|
||||
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`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,16 +12,16 @@ export default function useActiveEnterpriseAlert(courseId) {
|
||||
*/
|
||||
const isVisible = courseAccess && !courseAccess.hasAccess && courseAccess.errorCode === 'incorrect_active_enterprise';
|
||||
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
text: courseAccess && courseAccess.userMessage,
|
||||
courseId,
|
||||
};
|
||||
}), [courseAccess, courseId]);
|
||||
useAlert(isVisible, {
|
||||
code: 'clientActiveEnterpriseAlert',
|
||||
topic: 'outline',
|
||||
dismissible: false,
|
||||
type: ALERT_TYPES.ERROR,
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
});
|
||||
|
||||
return { clientActiveEnterpriseAlert: ActiveEnterpriseAlert };
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedMessage,
|
||||
FormattedRelative,
|
||||
FormattedRelativeTime,
|
||||
FormattedTime,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
@@ -11,9 +11,11 @@ import { Info } from '@edx/paragon/icons';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000; // in ms
|
||||
const DAY_SEC = 24 * 60 * 60; // in seconds
|
||||
const DAY_MS = DAY_SEC * 1000; // in ms
|
||||
const YEAR_SEC = 365 * DAY_SEC; // in seconds
|
||||
|
||||
function CourseStartAlert({ payload }) {
|
||||
const CourseStartAlert = ({ payload }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = payload;
|
||||
@@ -25,15 +27,17 @@ function CourseStartAlert({ payload }) {
|
||||
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
const delta = new Date(startDate) - new Date();
|
||||
const timeRemaining = (
|
||||
<FormattedRelative
|
||||
<FormattedRelativeTime
|
||||
key="timeRemaining"
|
||||
value={startDate}
|
||||
value={delta / 1000}
|
||||
numeric="auto"
|
||||
// 1 year interval to help auto format. It won't format without updateIntervalInSeconds.
|
||||
updateIntervalInSeconds={YEAR_SEC}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
);
|
||||
|
||||
const delta = new Date(startDate) - new Date();
|
||||
if (delta < DAY_MS) {
|
||||
return (
|
||||
<Alert variant="info" icon={Info}>
|
||||
@@ -90,7 +94,7 @@ function CourseStartAlert({ payload }) {
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseStartAlert.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PageBanner } from '@edx/paragon';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
function CourseStartMasqueradeBanner({ payload }) {
|
||||
const CourseStartMasqueradeBanner = ({ payload }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = payload;
|
||||
@@ -33,7 +33,7 @@ function CourseStartMasqueradeBanner({ payload }) {
|
||||
/>
|
||||
</PageBanner>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseStartMasqueradeBanner.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useModel } from '../../generic/model-store';
|
||||
const CourseStartAlert = React.lazy(() => import('./CourseStartAlert'));
|
||||
const CourseStartMasqueradeBanner = React.lazy(() => import('./CourseStartMasqueradeBanner'));
|
||||
|
||||
function isStartDateInFuture(courseId) {
|
||||
function IsStartDateInFuture(courseId) {
|
||||
const {
|
||||
start,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
@@ -20,15 +20,15 @@ function useCourseStartAlert(courseId) {
|
||||
isEnrolled,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const isVisible = isEnrolled && isStartDateInFuture(courseId);
|
||||
const isVisible = isEnrolled && IsStartDateInFuture(courseId);
|
||||
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
courseId,
|
||||
};
|
||||
}), [courseId]);
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientCourseStartAlert',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'outline-course-alerts',
|
||||
});
|
||||
|
||||
@@ -42,15 +42,15 @@ export function useCourseStartMasqueradeBanner(courseId, tab) {
|
||||
isMasquerading,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const isVisible = isMasquerading && tab === 'progress' && isStartDateInFuture(courseId);
|
||||
const isVisible = isMasquerading && tab === 'progress' && IsStartDateInFuture(courseId);
|
||||
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
courseId,
|
||||
};
|
||||
}), [courseId]);
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientCourseStartMasqueradeBanner',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'instructor-toolbar-alerts',
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
import useEnrollClickHandler from './clickHook';
|
||||
|
||||
function EnrollmentAlert({ intl, payload }) {
|
||||
const EnrollmentAlert = ({ intl, payload }) => {
|
||||
const {
|
||||
canEnroll,
|
||||
courseId,
|
||||
@@ -55,7 +55,7 @@ function EnrollmentAlert({ intl, payload }) {
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
EnrollmentAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -27,7 +27,7 @@ function useEnrollClickHandler(courseId, orgId, successText) {
|
||||
});
|
||||
global.location.reload();
|
||||
});
|
||||
}, [courseId]);
|
||||
}, [addFlash, courseId, orgId, successText]);
|
||||
|
||||
return { enrollClickHandler, loading };
|
||||
}
|
||||
|
||||
@@ -22,16 +22,16 @@ export function useEnrollmentAlert(courseId) {
|
||||
* 3. the course is private.
|
||||
*/
|
||||
const isVisible = !enrolledUser && authenticatedUser !== null && privateOutline;
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
||||
courseId,
|
||||
extraText: outline && outline.enrollAlert ? outline.enrollAlert.extraText : '',
|
||||
isStaff: course && course.isStaff,
|
||||
};
|
||||
}), [course, courseId, outline]);
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientEnrollmentAlert',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'outline',
|
||||
});
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
|
||||
import { sendActivationEmail } from '../../courseware/data';
|
||||
import messages from './messages';
|
||||
|
||||
function AccountActivationAlert({
|
||||
const AccountActivationAlert = ({
|
||||
intl,
|
||||
}) {
|
||||
}) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
@@ -123,7 +123,7 @@ function AccountActivationAlert({
|
||||
{children()}
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AccountActivationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { WarningFilled } from '@edx/paragon/icons';
|
||||
|
||||
import genericMessages from '../../generic/messages';
|
||||
|
||||
function LogistrationAlert({ intl }) {
|
||||
const LogistrationAlert = ({ intl }) => {
|
||||
const signIn = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
@@ -41,7 +41,7 @@ function LogistrationAlert({ intl }) {
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LogistrationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -35,7 +35,8 @@ function useSequenceEntranceExamAlert(courseId, sequenceId, intl) {
|
||||
|
||||
if (entranceExamPassed) {
|
||||
entranceExamText = intl.formatMessage(
|
||||
messages.entranceExamTextPassed, { entranceExamCurrentScore: entranceExamCurrentScore * 100 },
|
||||
messages.entranceExamTextPassed,
|
||||
{ entranceExamCurrentScore: entranceExamCurrentScore * 100 },
|
||||
);
|
||||
} else {
|
||||
entranceExamText = intl.formatMessage(messages.entranceExamTextNotPassing, {
|
||||
|
||||
@@ -34,91 +34,89 @@ Factory.define('courseHomeMetadata')
|
||||
currency_symbol: '$',
|
||||
},
|
||||
})
|
||||
.attr(
|
||||
'tabs', ['id', 'host'], (id, host) => [
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Course',
|
||||
priority: 0,
|
||||
slug: 'courseware',
|
||||
type: 'courseware',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'course/',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Discussion',
|
||||
priority: 1,
|
||||
slug: 'discussion',
|
||||
type: 'discussion',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'discussion/forum/',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Wiki',
|
||||
priority: 2,
|
||||
slug: 'wiki',
|
||||
type: 'wiki',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'course_wiki',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Progress',
|
||||
priority: 3,
|
||||
slug: 'progress',
|
||||
type: 'progress',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'progress',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Instructor',
|
||||
priority: 4,
|
||||
slug: 'instructor',
|
||||
type: 'instructor',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'instructor',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Dates',
|
||||
priority: 5,
|
||||
slug: 'dates',
|
||||
type: 'dates',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'dates',
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
.attr('tabs', ['id', 'host'], (id, host) => [
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Course',
|
||||
priority: 0,
|
||||
slug: 'courseware',
|
||||
type: 'courseware',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'course/',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Discussion',
|
||||
priority: 1,
|
||||
slug: 'discussion',
|
||||
type: 'discussion',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'discussion/forum/',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Wiki',
|
||||
priority: 2,
|
||||
slug: 'wiki',
|
||||
type: 'wiki',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'course_wiki',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Progress',
|
||||
priority: 3,
|
||||
slug: 'progress',
|
||||
type: 'progress',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'progress',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Instructor',
|
||||
priority: 4,
|
||||
slug: 'instructor',
|
||||
type: 'instructor',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'instructor',
|
||||
},
|
||||
),
|
||||
Factory.build(
|
||||
'tab',
|
||||
{
|
||||
title: 'Dates',
|
||||
priority: 5,
|
||||
slug: 'dates',
|
||||
type: 'dates',
|
||||
},
|
||||
{
|
||||
courseId: id,
|
||||
host,
|
||||
path: 'dates',
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -349,7 +349,7 @@ export async function getOutlineTabData(courseId) {
|
||||
const timeOffsetMillis = getTimeOffsetMillis(headers && headers.date, requestTime, responseTime);
|
||||
const userHasPassingGrade = data.user_has_passing_grade;
|
||||
const verifiedMode = camelCaseObject(data.verified_mode);
|
||||
const welcomeMessageHtml = data.welcome_message_html;
|
||||
const welcomeMessageHtml = data.welcome_message_html || '';
|
||||
|
||||
return {
|
||||
accessExpiration,
|
||||
|
||||
@@ -39,183 +39,186 @@ describe('Course Home Service', () => {
|
||||
afterAll(() => provider.finalize());
|
||||
describe('When a request to fetch tab is made', () => {
|
||||
it('returns tab data for a course_id', async () => {
|
||||
await provider.addInteraction({
|
||||
state: `Tab data exists for course_id ${courseId}`,
|
||||
uponReceiving: 'a request to fetch tab',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/course_home/course_metadata/${courseId}`,
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
can_show_upgrade_sock: boolean(false),
|
||||
verified_mode: like({
|
||||
access_expiration_date: null,
|
||||
currency: 'USD',
|
||||
currency_symbol: '$',
|
||||
price: 149,
|
||||
sku: '8CF08E5',
|
||||
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
}),
|
||||
celebrations: like({
|
||||
first_section: false,
|
||||
streak_length_to_celebrate: null,
|
||||
streak_discount_enabled: false,
|
||||
}),
|
||||
course_access: {
|
||||
has_access: boolean(true),
|
||||
error_code: null,
|
||||
developer_message: null,
|
||||
user_message: null,
|
||||
additional_context_user_message: null,
|
||||
user_fragment: null,
|
||||
setTimeout(() => {
|
||||
provider.addInteraction({
|
||||
state: `Tab data exists for course_id ${courseId}`,
|
||||
uponReceiving: 'a request to fetch tab',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/course_home/course_metadata/${courseId}`,
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
can_show_upgrade_sock: boolean(false),
|
||||
verified_mode: like({
|
||||
access_expiration_date: null,
|
||||
currency: 'USD',
|
||||
currency_symbol: '$',
|
||||
price: 149,
|
||||
sku: '8CF08E5',
|
||||
upgrade_url: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
}),
|
||||
celebrations: like({
|
||||
first_section: false,
|
||||
streak_length_to_celebrate: null,
|
||||
streak_discount_enabled: false,
|
||||
}),
|
||||
course_access: {
|
||||
has_access: boolean(true),
|
||||
error_code: null,
|
||||
developer_message: null,
|
||||
user_message: null,
|
||||
additional_context_user_message: null,
|
||||
user_fragment: null,
|
||||
},
|
||||
course_id: term({
|
||||
generate: 'course-v1:edX+DemoX+Demo_Course',
|
||||
matcher: opaqueKeysRegex,
|
||||
}),
|
||||
is_enrolled: boolean(true),
|
||||
is_self_paced: boolean(false),
|
||||
is_staff: boolean(true),
|
||||
number: string('DemoX'),
|
||||
org: string('edX'),
|
||||
original_user_is_staff: boolean(true),
|
||||
start: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
tabs: eachLike({
|
||||
tab_id: 'courseware',
|
||||
title: 'Course',
|
||||
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||
}),
|
||||
title: string('Demonstration Course'),
|
||||
username: string('edx'),
|
||||
},
|
||||
course_id: term({
|
||||
generate: 'course-v1:edX+DemoX+Demo_Course',
|
||||
matcher: opaqueKeysRegex,
|
||||
}),
|
||||
is_enrolled: boolean(true),
|
||||
is_self_paced: boolean(false),
|
||||
is_staff: boolean(true),
|
||||
number: string('DemoX'),
|
||||
org: string('edX'),
|
||||
original_user_is_staff: boolean(true),
|
||||
start: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
tabs: eachLike({
|
||||
tab_id: 'courseware',
|
||||
},
|
||||
});
|
||||
const normalizedTabData = {
|
||||
canShowUpgradeSock: false,
|
||||
verifiedMode: {
|
||||
accessExpirationDate: null,
|
||||
currency: 'USD',
|
||||
currencySymbol: '$',
|
||||
price: 149,
|
||||
sku: '8CF08E5',
|
||||
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
},
|
||||
celebrations: {
|
||||
firstSection: false,
|
||||
streakLengthToCelebrate: null,
|
||||
streakDiscountEnabled: false,
|
||||
},
|
||||
courseAccess: {
|
||||
hasAccess: true,
|
||||
errorCode: null,
|
||||
developerMessage: null,
|
||||
userMessage: null,
|
||||
additionalContextUserMessage: null,
|
||||
userFragment: null,
|
||||
},
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
isEnrolled: true,
|
||||
isMasquerading: false,
|
||||
isSelfPaced: false,
|
||||
isStaff: true,
|
||||
number: 'DemoX',
|
||||
org: 'edX',
|
||||
originalUserIsStaff: true,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
tabs: [
|
||||
{
|
||||
slug: 'outline',
|
||||
title: 'Course',
|
||||
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||
}),
|
||||
title: string('Demonstration Course'),
|
||||
username: string('edx'),
|
||||
},
|
||||
},
|
||||
});
|
||||
const normalizedTabData = {
|
||||
canShowUpgradeSock: false,
|
||||
verifiedMode: {
|
||||
accessExpirationDate: null,
|
||||
currency: 'USD',
|
||||
currencySymbol: '$',
|
||||
price: 149,
|
||||
sku: '8CF08E5',
|
||||
upgradeUrl: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
},
|
||||
celebrations: {
|
||||
firstSection: false,
|
||||
streakLengthToCelebrate: null,
|
||||
streakDiscountEnabled: false,
|
||||
},
|
||||
courseAccess: {
|
||||
hasAccess: true,
|
||||
errorCode: null,
|
||||
developerMessage: null,
|
||||
userMessage: null,
|
||||
additionalContextUserMessage: null,
|
||||
userFragment: null,
|
||||
},
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
isEnrolled: true,
|
||||
isMasquerading: false,
|
||||
isSelfPaced: false,
|
||||
isStaff: true,
|
||||
number: 'DemoX',
|
||||
org: 'edX',
|
||||
originalUserIsStaff: true,
|
||||
start: '2013-02-05T05:00:00Z',
|
||||
tabs: [
|
||||
{
|
||||
slug: 'outline',
|
||||
title: 'Course',
|
||||
url: `${getConfig().BASE_URL}/course/course-v1:edX+DemoX+Demo_Course/home`,
|
||||
},
|
||||
],
|
||||
title: 'Demonstration Course',
|
||||
username: 'edx',
|
||||
};
|
||||
const response = await getCourseHomeCourseMetadata(courseId, 'outline');
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(normalizedTabData);
|
||||
},
|
||||
],
|
||||
title: 'Demonstration Course',
|
||||
username: 'edx',
|
||||
};
|
||||
const response = getCourseHomeCourseMetadata(courseId, 'outline');
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(normalizedTabData);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When a request to fetch dates tab is made', () => {
|
||||
it('returns course date blocks for a course_id', async () => {
|
||||
await provider.addInteraction({
|
||||
state: `course date blocks exist for course_id ${courseId}`,
|
||||
uponReceiving: 'a request to fetch dates tab',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/course_home/dates/${courseId}`,
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
dates_banner_info: like({
|
||||
missed_deadlines: false,
|
||||
content_type_gating_enabled: false,
|
||||
missed_gated_content: false,
|
||||
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
}),
|
||||
course_date_blocks: eachLike({
|
||||
assignment_type: null,
|
||||
setTimeout(() => {
|
||||
provider.addInteraction({
|
||||
state: `course date blocks exist for course_id ${courseId}`,
|
||||
uponReceiving: 'a request to fetch dates tab',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: `/api/course_home/dates/${courseId}`,
|
||||
},
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
dates_banner_info: like({
|
||||
missed_deadlines: false,
|
||||
content_type_gating_enabled: false,
|
||||
missed_gated_content: false,
|
||||
verified_upgrade_link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
}),
|
||||
course_date_blocks: eachLike({
|
||||
assignment_type: null,
|
||||
complete: null,
|
||||
date: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
date_type: term({
|
||||
generate: 'verified-upgrade-deadline',
|
||||
matcher: dateTypeRegex,
|
||||
}),
|
||||
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
|
||||
learner_has_access: true,
|
||||
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
link_text: 'Upgrade to Verified Certificate',
|
||||
title: 'Verification Upgrade Deadline',
|
||||
extra_info: null,
|
||||
first_component_block_id: '',
|
||||
}),
|
||||
has_ended: boolean(false),
|
||||
learner_is_full_access: boolean(true),
|
||||
user_timezone: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
const camelCaseResponse = {
|
||||
datesBannerInfo: {
|
||||
missedDeadlines: false,
|
||||
contentTypeGatingEnabled: false,
|
||||
missedGatedContent: false,
|
||||
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
},
|
||||
courseDateBlocks: [
|
||||
{
|
||||
assignmentType: null,
|
||||
complete: null,
|
||||
date: term({
|
||||
generate: '2013-02-05T05:00:00Z',
|
||||
matcher: dateRegex,
|
||||
}),
|
||||
date_type: term({
|
||||
generate: 'verified-upgrade-deadline',
|
||||
matcher: dateTypeRegex,
|
||||
}),
|
||||
date: '2013-02-05T05:00:00Z',
|
||||
dateType: 'verified-upgrade-deadline',
|
||||
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
|
||||
learner_has_access: true,
|
||||
learnerHasAccess: true,
|
||||
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
link_text: 'Upgrade to Verified Certificate',
|
||||
linkText: 'Upgrade to Verified Certificate',
|
||||
title: 'Verification Upgrade Deadline',
|
||||
extra_info: null,
|
||||
first_component_block_id: '',
|
||||
}),
|
||||
has_ended: boolean(false),
|
||||
learner_is_full_access: boolean(true),
|
||||
user_timezone: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
const camelCaseResponse = {
|
||||
datesBannerInfo: {
|
||||
missedDeadlines: false,
|
||||
contentTypeGatingEnabled: false,
|
||||
missedGatedContent: false,
|
||||
verifiedUpgradeLink: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
},
|
||||
courseDateBlocks: [
|
||||
{
|
||||
assignmentType: null,
|
||||
complete: null,
|
||||
date: '2013-02-05T05:00:00Z',
|
||||
dateType: 'verified-upgrade-deadline',
|
||||
description: 'You are still eligible to upgrade to a Verified Certificate! Pursue it to highlight the knowledge and skills you gain in this course.',
|
||||
learnerHasAccess: true,
|
||||
link: `${getConfig().ECOMMERCE_BASE_URL}/basket/add/?sku=8CF08E5`,
|
||||
linkText: 'Upgrade to Verified Certificate',
|
||||
title: 'Verification Upgrade Deadline',
|
||||
extraInfo: null,
|
||||
firstComponentBlockId: '',
|
||||
},
|
||||
],
|
||||
hasEnded: false,
|
||||
learnerIsFullAccess: true,
|
||||
userTimezone: null,
|
||||
};
|
||||
|
||||
const response = await getDatesTabData(courseId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(camelCaseResponse);
|
||||
extraInfo: null,
|
||||
firstComponentBlockId: '',
|
||||
},
|
||||
],
|
||||
hasEnded: false,
|
||||
learnerIsFullAccess: true,
|
||||
userTimezone: null,
|
||||
};
|
||||
const response = getDatesTabData(courseId);
|
||||
expect(response).toBeTruthy();
|
||||
expect(response).toEqual(camelCaseResponse);
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,14 +9,12 @@ import Timeline from './timeline/Timeline';
|
||||
import { fetchDatesTab } from '../data';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
import { initDatesMMP2P } from '../../experiments/mm-p2p';
|
||||
import SuggestedScheduleHeader from '../suggested-schedule-messaging/SuggestedScheduleHeader';
|
||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
|
||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||
|
||||
function DatesTab({ intl }) {
|
||||
const DatesTab = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -30,9 +28,6 @@ function DatesTab({ intl }) {
|
||||
courseDateBlocks,
|
||||
} = useModel('dates', courseId);
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
const mmp2p = initDatesMMP2P(courseId);
|
||||
|
||||
const hasDeadlines = courseDateBlocks && courseDateBlocks.some(x => x.dateType === 'assignment-due-date');
|
||||
|
||||
const logUpgradeLinkClick = () => {
|
||||
@@ -51,8 +46,7 @@ function DatesTab({ intl }) {
|
||||
<div role="heading" aria-level="1" className="h2 my-3">
|
||||
{intl.formatMessage(messages.title)}
|
||||
</div>
|
||||
{ /** [MM-P2P] Experiment */ }
|
||||
{isSelfPaced && hasDeadlines && !mmp2p.state.isEnabled && (
|
||||
{isSelfPaced && hasDeadlines && (
|
||||
<>
|
||||
<ShiftDatesAlert model="dates" fetch={fetchDatesTab} />
|
||||
<SuggestedScheduleHeader />
|
||||
@@ -60,10 +54,10 @@ function DatesTab({ intl }) {
|
||||
<UpgradeToShiftDatesAlert logUpgradeLinkClick={logUpgradeLinkClick} model="dates" />
|
||||
</>
|
||||
)}
|
||||
<Timeline mmp2p={mmp2p} />
|
||||
<Timeline />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DatesTab.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -17,15 +17,13 @@ import { useModel } from '../../../generic/model-store';
|
||||
import { getBadgeListAndColor } from './badgelist';
|
||||
import { isLearnerAssignment } from '../utils';
|
||||
|
||||
function Day({
|
||||
const Day = ({
|
||||
date,
|
||||
first,
|
||||
intl,
|
||||
items,
|
||||
last,
|
||||
/** [MM-P2P] Example */
|
||||
mmp2p,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -37,11 +35,6 @@ function Day({
|
||||
|
||||
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 (
|
||||
<li className="dates-day pb-4" data-testid="dates-day">
|
||||
{/* Top Line */}
|
||||
@@ -57,8 +50,7 @@ function Day({
|
||||
<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">
|
||||
<FormattedDate
|
||||
/** [MM-P2P] Experiment */
|
||||
value={mmp2pOverride ? mmp2p.state.upgradeDeadline : date}
|
||||
value={date}
|
||||
day="numeric"
|
||||
month="short"
|
||||
weekday="short"
|
||||
@@ -68,10 +60,7 @@ function Day({
|
||||
{badges}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
/** [MM-P2P] Experiment (conditional) */
|
||||
const { badges: itemBadges } = mmp2pOverride
|
||||
? getBadgeListAndColor(new Date(mmp2p.state.upgradeDeadline), intl, item, items)
|
||||
: getBadgeListAndColor(date, intl, item, items);
|
||||
const { badges: itemBadges } = getBadgeListAndColor(date, intl, item, items);
|
||||
|
||||
const showDueDateTime = item.dateType === 'assignment-due-date';
|
||||
const showLink = item.link && isLearnerAssignment(item);
|
||||
@@ -107,22 +96,14 @@ function Day({
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</div>
|
||||
{ /** [MM-P2P] Experiment (conditional) */ }
|
||||
{ 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>)}
|
||||
{item.description && <div className="small mb-2">{item.description}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Day.propTypes = {
|
||||
date: PropTypes.objectOf(Date).isRequired,
|
||||
@@ -138,25 +119,11 @@ Day.propTypes = {
|
||||
title: PropTypes.string,
|
||||
})).isRequired,
|
||||
last: PropTypes.bool,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p: PropTypes.shape({
|
||||
state: PropTypes.shape({
|
||||
isEnabled: PropTypes.bool.isRequired,
|
||||
upgradeDeadline: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
Day.defaultProps = {
|
||||
first: false,
|
||||
last: false,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p: {
|
||||
state: {
|
||||
isEnabled: false,
|
||||
upgradeDeadline: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default injectIntl(Day);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React from 'react';
|
||||
/** [MM-P2P] Experiment (import) */
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
@@ -8,8 +6,7 @@ import { useModel } from '../../../generic/model-store';
|
||||
import Day from './Day';
|
||||
import { daycmp, isLearnerAssignment } from '../utils';
|
||||
|
||||
/** [MM-P2P] Experiment (argument) */
|
||||
export default function Timeline({ mmp2p }) {
|
||||
const Timeline = () => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -66,17 +63,10 @@ export default function Timeline({ mmp2p }) {
|
||||
return (
|
||||
<ul className="list-unstyled m-0 mt-4 pt-2">
|
||||
{groupedDates.map((groupedDate) => (
|
||||
<Day key={groupedDate.date} {...groupedDate} mmp2p={mmp2p} />
|
||||
<Day key={groupedDate.date} {...groupedDate} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
Timeline.propTypes = {
|
||||
mmp2p: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
Timeline.defaultProps = {
|
||||
mmp2p: {},
|
||||
};
|
||||
export default Timeline;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { generatePath, useHistory } from 'react-router';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useIFrameHeight, useIFramePluginEvents } from '../../generic/hooks';
|
||||
|
||||
function DiscussionTab() {
|
||||
const DiscussionTab = () => {
|
||||
const { courseId } = useSelector(state => state.courseHome);
|
||||
const { path } = useParams();
|
||||
const [originalPath] = useState(path);
|
||||
@@ -29,7 +29,7 @@ function DiscussionTab() {
|
||||
title="discussion"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DiscussionTab.propTypes = {};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { unsubscribeFromCourseGoal } from '../data/api';
|
||||
import messages from './messages';
|
||||
import ResultPage from './ResultPage';
|
||||
|
||||
function GoalUnsubscribe({ intl }) {
|
||||
const GoalUnsubscribe = ({ intl }) => {
|
||||
const { token } = useParams();
|
||||
const [error, setError] = useState(false);
|
||||
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.
|
||||
// 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 });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // deps=[] to only run once
|
||||
|
||||
return (
|
||||
@@ -48,7 +49,7 @@ function GoalUnsubscribe({ intl }) {
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
GoalUnsubscribe.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button, Hyperlink } from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
|
||||
|
||||
function ResultPage({ courseTitle, error, intl }) {
|
||||
const ResultPage = ({ courseTitle, error, intl }) => {
|
||||
const errorDescription = (
|
||||
<FormattedMessage
|
||||
id="learning.goals.unsubscribe.errorDescription"
|
||||
@@ -44,7 +44,7 @@ function ResultPage({ courseTitle, error, intl }) {
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ResultPage.defaultProps = {
|
||||
courseTitle: null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
function LiveTab() {
|
||||
const LiveTab = () => {
|
||||
const { courseId } = useSelector(state => state.courseHome);
|
||||
const liveModel = useSelector(state => state.models.live);
|
||||
useEffect(() => {
|
||||
@@ -17,6 +17,6 @@ function LiveTab() {
|
||||
dangerouslySetInnerHTML={{ __html: liveModel[courseId]?.iframe }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default LiveTab;
|
||||
|
||||
@@ -9,12 +9,10 @@ import { useModel } from '../../generic/model-store';
|
||||
import { isLearnerAssignment } from '../dates-tab/utils';
|
||||
import './DateSummary.scss';
|
||||
|
||||
export default function DateSummary({
|
||||
const DateSummary = ({
|
||||
dateBlock,
|
||||
userTimezone,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -25,9 +23,6 @@ export default function DateSummary({
|
||||
const linkedTitle = dateBlock.link && isLearnerAssignment(dateBlock);
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
const showMMP2P = mmp2p.state.isEnabled && (dateBlock.dateType === 'verified-upgrade-deadline');
|
||||
|
||||
const logVerifiedUpgradeClick = () => {
|
||||
sendTrackEvent('edx.bi.ecommerce.upsell_links_clicked', {
|
||||
org_key: org,
|
||||
@@ -45,8 +40,7 @@ export default function DateSummary({
|
||||
<FontAwesomeIcon icon={faCalendarAlt} className="ml-3 mt-1 mr-1" fixedWidth />
|
||||
<div className="ml-1 font-weight-bold">
|
||||
<FormattedDate
|
||||
/** [MM-P2P] Experiment */
|
||||
value={showMMP2P ? mmp2p.state.upgradeDeadline : dateBlock.date}
|
||||
value={dateBlock.date}
|
||||
day="numeric"
|
||||
month="short"
|
||||
weekday="short"
|
||||
@@ -55,48 +49,33 @@ export default function DateSummary({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/** [MM-P2P] Experiment (conditional) */}
|
||||
{ showMMP2P ? (
|
||||
<div className="row ml-4 pr-2">
|
||||
<div className="date-summary-text">
|
||||
<div className="row ml-4 pr-2">
|
||||
<div className="date-summary-text">
|
||||
{linkedTitle && (
|
||||
<div className="font-weight-bold mt-2">
|
||||
Last chance to upgrade
|
||||
<a href={dateBlock.link}>{dateBlock.title}</a>
|
||||
</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 && (
|
||||
<a
|
||||
href={dateBlock.link}
|
||||
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
||||
className="description-link"
|
||||
>
|
||||
{dateBlock.linkText}
|
||||
</a>
|
||||
{!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 && (
|
||||
<a
|
||||
href={dateBlock.link}
|
||||
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
||||
className="description-link"
|
||||
>
|
||||
{dateBlock.linkText}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DateSummary.propTypes = {
|
||||
dateBlock: PropTypes.shape({
|
||||
@@ -109,22 +88,10 @@ DateSummary.propTypes = {
|
||||
learnerHasAccess: PropTypes.bool,
|
||||
}).isRequired,
|
||||
userTimezone: PropTypes.string,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p: PropTypes.shape({
|
||||
state: PropTypes.shape({
|
||||
isEnabled: PropTypes.bool.isRequired,
|
||||
upgradeDeadline: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
DateSummary.defaultProps = {
|
||||
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';
|
||||
|
||||
export default function LmsHtmlFragment({
|
||||
const LmsHtmlFragment = ({
|
||||
className,
|
||||
html,
|
||||
title,
|
||||
...rest
|
||||
}) {
|
||||
}) => {
|
||||
const wholePage = `
|
||||
<html>
|
||||
<head>
|
||||
<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" 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>
|
||||
<body class="${className}">${html}</body>
|
||||
<script>
|
||||
@@ -55,7 +55,7 @@ export default function LmsHtmlFragment({
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LmsHtmlFragment.defaultProps = {
|
||||
className: '',
|
||||
@@ -66,3 +66,5 @@ LmsHtmlFragment.propTypes = {
|
||||
html: 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 AccountActivationAlert from '../../alerts/logistration-alert/AccountActivationAlert';
|
||||
|
||||
/** [MM-P2P] Experiment */
|
||||
import { initHomeMMP2P, MMP2PFlyover } from '../../experiments/mm-p2p';
|
||||
|
||||
function OutlineTab({ intl }) {
|
||||
const OutlineTab = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
proctoringPanelStatus,
|
||||
@@ -104,9 +101,6 @@ function OutlineTab({ intl }) {
|
||||
return userRoleNames.includes('enterprise_learner');
|
||||
};
|
||||
|
||||
/** [[MM-P2P] Experiment */
|
||||
const MMP2P = initHomeMMP2P(courseId);
|
||||
|
||||
/** show post enrolment survey to only B2C learners */
|
||||
const learnerType = isEnterpriseUser() ? 'enterprise_learner' : 'b2c_learner';
|
||||
|
||||
@@ -116,7 +110,7 @@ function OutlineTab({ intl }) {
|
||||
const currentParams = new URLSearchParams(location.search);
|
||||
const startCourse = currentParams.get('start_course');
|
||||
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
|
||||
// whenever passed in query params.
|
||||
@@ -134,7 +128,6 @@ function OutlineTab({ intl }) {
|
||||
<div role="heading" aria-level="1" className="h2">{title}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
|
||||
<div className="row course-outline-tab">
|
||||
<AccountActivationAlert />
|
||||
<div className="col-12">
|
||||
@@ -146,21 +139,17 @@ function OutlineTab({ intl }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="col col-12 col-md-8">
|
||||
{ /** [MM-P2P] Experiment (the conditional) */ }
|
||||
{ !MMP2P.state.isEnabled
|
||||
&& (
|
||||
<AlertList
|
||||
topic="outline-course-alerts"
|
||||
className="mb-3"
|
||||
customAlerts={{
|
||||
...certificateAvailableAlert,
|
||||
...courseEndAlert,
|
||||
...courseStartAlert,
|
||||
...scheduledContentAlert,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isSelfPaced && hasDeadlines && !MMP2P.state.isEnabled && (
|
||||
<AlertList
|
||||
topic="outline-course-alerts"
|
||||
className="mb-3"
|
||||
customAlerts={{
|
||||
...certificateAvailableAlert,
|
||||
...courseEndAlert,
|
||||
...courseStartAlert,
|
||||
...scheduledContentAlert,
|
||||
}}
|
||||
/>
|
||||
{isSelfPaced && hasDeadlines && (
|
||||
<>
|
||||
<ShiftDatesAlert model="outline" fetch={fetchOutlineTab} />
|
||||
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
|
||||
@@ -203,35 +192,27 @@ function OutlineTab({ intl }) {
|
||||
/>
|
||||
)}
|
||||
<CourseTools />
|
||||
{ /** [MM-P2P] Experiment (conditional) */ }
|
||||
{ MMP2P.state.isEnabled
|
||||
? <MMP2PFlyover isStatic options={MMP2P} />
|
||||
: (
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="course_home"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
/>
|
||||
)}
|
||||
<CourseDates
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p={MMP2P}
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="course_home"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
/>
|
||||
<CourseDates />
|
||||
<CourseHandouts />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
OutlineTab.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -119,11 +119,11 @@ describe('Outline Tab', () => {
|
||||
|
||||
// Click to expand section
|
||||
userEvent.click(expandButton);
|
||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true');
|
||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true'));
|
||||
|
||||
// Click to collapse section
|
||||
userEvent.click(expandButton);
|
||||
expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
|
||||
await waitFor(() => expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false'));
|
||||
});
|
||||
|
||||
it('displays correct icon for complete assignment', async () => {
|
||||
@@ -355,13 +355,13 @@ describe('Outline Tab', () => {
|
||||
|
||||
await fetchAndRender('http://localhost/?weekly_goal=3');
|
||||
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 () => {
|
||||
sendTrackEvent.mockClear();
|
||||
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', () => {
|
||||
@@ -383,25 +383,25 @@ describe('Outline Tab', () => {
|
||||
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeDisabled();
|
||||
});
|
||||
|
||||
it.each`
|
||||
level | days
|
||||
${'Casual'} | ${1}
|
||||
${'Regular'} | ${3}
|
||||
${'Intense'} | ${5}
|
||||
`('calls the API with a goal of $days when $level goal is clicked', async ({ level, days }) => {
|
||||
// click on Casual goal
|
||||
const button = await screen.queryByTestId(`weekly-learning-goal-input-${level}`);
|
||||
fireEvent.click(button);
|
||||
// Verify the request was made
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
||||
// subscribe is turned on automatically
|
||||
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
|
||||
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled();
|
||||
});
|
||||
it.each([
|
||||
{ level: 'Casual', days: 1 },
|
||||
{ level: 'Regular', days: 3 },
|
||||
{ level: 'Intense', days: 5 },
|
||||
])('calls the API with a goal of $days when $level goal is clicked', async ({ level, days }) => {
|
||||
// click on Casual goal
|
||||
const button = await screen.queryByTestId(`weekly-learning-goal-input-${level}`);
|
||||
fireEvent.click(button);
|
||||
// Verify the request was made
|
||||
await waitFor(() => {
|
||||
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
||||
// subscribe is turned on automatically
|
||||
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
|
||||
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled();
|
||||
});
|
||||
|
||||
it('shows and hides subscribe to reminders additional text', async () => {
|
||||
const button = await screen.getByTestId('weekly-learning-goal-input-Regular');
|
||||
fireEvent.click(button);
|
||||
@@ -577,7 +577,7 @@ describe('Outline Tab', () => {
|
||||
const instructorToolbar = await screen.getByTestId('instructor-toolbar');
|
||||
expect(instructorToolbar).toBeInTheDocument();
|
||||
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render banner when not masquerading', async () => {
|
||||
@@ -789,12 +789,14 @@ describe('Outline Tab', () => {
|
||||
const requestingButton = screen.getByRole('button', { name: 'Request certificate' });
|
||||
fireEvent.click(requestingButton);
|
||||
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,
|
||||
is_staff: false,
|
||||
org_key: 'edX',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('tracks unverified cert button', async () => {
|
||||
@@ -833,12 +835,14 @@ describe('Outline Tab', () => {
|
||||
const requestingButton = screen.getByRole('link', { name: 'Verify my ID' });
|
||||
fireEvent.click(requestingButton);
|
||||
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,
|
||||
is_staff: false,
|
||||
org_key: 'edX',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1017,6 +1021,22 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
|
||||
it('displays expiration warning', async () => {
|
||||
const expirationDate = new Date();
|
||||
// This message will render if the expiration date is within 28 days; set the date 10 days in future
|
||||
expirationDate.setTime(expirationDate.getTime() + 864800000);
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, {
|
||||
onboarding_status: 'verified',
|
||||
onboarding_link: 'test',
|
||||
expiration_date: expirationDate.toString(),
|
||||
onboarding_release_date: onboardingReleaseDate.toISOString(),
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays expiration warning for other course', async () => {
|
||||
const expirationDate = new Date();
|
||||
// This message will render if the expiration date is within 28 days; set the date 10 days in future
|
||||
expirationDate.setTime(expirationDate.getTime() + 864800000);
|
||||
@@ -1028,7 +1048,23 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays expired', async () => {
|
||||
const expirationDate = new Date();
|
||||
// This message appears after expiration, set the date 10 days in the past
|
||||
expirationDate.setTime(expirationDate.getTime() - 864800000);
|
||||
axiosMock.onGet(proctoringInfoUrl).reply(200, {
|
||||
onboarding_status: 'verified',
|
||||
onboarding_link: 'test',
|
||||
expiration_date: expirationDate.toString(),
|
||||
onboarding_release_date: onboardingReleaseDate.toISOString(),
|
||||
});
|
||||
await fetchAndRender();
|
||||
await screen.findByText('This course contains proctored exams');
|
||||
expect(screen.queryByText('Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Onboarding profile review can take 2+ business days.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ import { useModel } from '../../generic/model-store';
|
||||
import genericMessages from '../../generic/messages';
|
||||
import messages from './messages';
|
||||
|
||||
function Section({
|
||||
const Section = ({
|
||||
courseId,
|
||||
defaultOpen,
|
||||
expand,
|
||||
intl,
|
||||
section,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
complete,
|
||||
sequenceIds,
|
||||
@@ -38,6 +38,7 @@ function Section({
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(defaultOpen);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const sectionTitle = (
|
||||
@@ -109,7 +110,7 @@ function Section({
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Section.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -16,13 +16,13 @@ import EffortEstimate from '../../shared/effort-estimate';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
function SequenceLink({
|
||||
const SequenceLink = ({
|
||||
id,
|
||||
intl,
|
||||
courseId,
|
||||
first,
|
||||
sequence,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
complete,
|
||||
description,
|
||||
@@ -39,6 +39,50 @@ function SequenceLink({
|
||||
const coursewareUrl = <Link to={`/course/${courseId}/${id}`}>{title}</Link>;
|
||||
const displayTitle = showLink ? coursewareUrl : title;
|
||||
|
||||
const dueDateMessage = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due-date-set"
|
||||
defaultMessage="{description} due {assignmentDue}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const noDueDateMessage = (
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due-date-not-set"
|
||||
defaultMessage="{description}"
|
||||
description="Used below an assignment title"
|
||||
values={{
|
||||
assignmentDue: (
|
||||
<FormattedTime
|
||||
key={`${id}-due`}
|
||||
day="numeric"
|
||||
month="short"
|
||||
year="numeric"
|
||||
timeZoneName="short"
|
||||
value={due}
|
||||
{...timezoneFormatArgs}
|
||||
/>
|
||||
),
|
||||
description: description || '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div className={classNames('', { 'mt-2 pt-2 border-top border-light': !first })}>
|
||||
@@ -70,35 +114,15 @@ function SequenceLink({
|
||||
<EffortEstimate className="ml-3 align-middle" block={sequence} />
|
||||
</div>
|
||||
</div>
|
||||
{due && (
|
||||
<div className="row w-100 m-0 ml-3 pl-3">
|
||||
<small className="text-body pl-2">
|
||||
<FormattedMessage
|
||||
id="learning.outline.sequence-due"
|
||||
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 className="row w-100 m-0 ml-3 pl-3">
|
||||
<small className="text-body pl-2">
|
||||
{due ? dueDateMessage : noDueDateMessage}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SequenceLink.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
|
||||
@@ -25,7 +25,7 @@ export const CERT_STATUS_TYPE = {
|
||||
UNVERIFIED: 'unverified',
|
||||
};
|
||||
|
||||
function CertificateStatusAlert({ intl, payload }) {
|
||||
const CertificateStatusAlert = ({ intl, payload }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
certificateAvailableDate,
|
||||
@@ -189,7 +189,7 @@ function CertificateStatusAlert({ intl, payload }) {
|
||||
)}
|
||||
</AlertWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CertificateStatusAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -75,7 +75,7 @@ function useCertificateStatusAlert(courseId) {
|
||||
&& hasEnded
|
||||
&& !userHasPassingGrade
|
||||
);
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
certificateAvailableDate,
|
||||
certURL,
|
||||
certStatus,
|
||||
@@ -85,11 +85,12 @@ function useCertificateStatusAlert(courseId) {
|
||||
org,
|
||||
notPassingCourseEnded,
|
||||
tabs,
|
||||
};
|
||||
}), [certStatus, certURL, certificateAvailableDate, courseId,
|
||||
endBlock, notPassingCourseEnded, org, tabs, userTimezone]);
|
||||
|
||||
useAlert(isVisible || notPassingCourseEnded, {
|
||||
code: 'clientCertificateStatusAlert',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'outline-course-alerts',
|
||||
});
|
||||
|
||||
|
||||
@@ -3,15 +3,17 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedMessage,
|
||||
FormattedRelative,
|
||||
FormattedRelativeTime,
|
||||
FormattedTime,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
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 {
|
||||
description,
|
||||
endDate,
|
||||
@@ -20,16 +22,19 @@ function CourseEndAlert({ payload }) {
|
||||
|
||||
const timezoneFormatArgs = userTimezone ? { timeZone: userTimezone } : {};
|
||||
|
||||
let msg;
|
||||
const delta = new Date(endDate) - new Date();
|
||||
const timeRemaining = (
|
||||
<FormattedRelative
|
||||
<FormattedRelativeTime
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
let msg;
|
||||
const delta = new Date(endDate) - new Date();
|
||||
if (delta < DAY_MS) {
|
||||
const courseEndTime = (
|
||||
<FormattedTime
|
||||
@@ -83,7 +88,7 @@ function CourseEndAlert({ payload }) {
|
||||
{description}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseEndAlert.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
|
||||
@@ -23,15 +23,15 @@ export function useCourseEndAlert(courseId) {
|
||||
const endDate = endBlock ? new Date(endBlock.date) : null;
|
||||
const delta = endBlock ? endDate - new Date() : 0;
|
||||
const isVisible = isEnrolled && endBlock && delta > 0 && delta < WARNING_PERIOD_MS;
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
description: endBlock && endBlock.description,
|
||||
endDate: endBlock && endBlock.date,
|
||||
userTimezone,
|
||||
};
|
||||
}), [endBlock, userTimezone]);
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientCourseEndAlert',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'outline-course-alerts',
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import outlineMessages from '../../messages';
|
||||
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
function PrivateCourseAlert({ intl, payload }) {
|
||||
const PrivateCourseAlert = ({ intl, payload }) => {
|
||||
const {
|
||||
anonymousUser,
|
||||
canEnroll,
|
||||
@@ -100,7 +100,7 @@ function PrivateCourseAlert({ intl, payload }) {
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PrivateCourseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -18,16 +18,16 @@ export function usePrivateCourseAlert(courseId) {
|
||||
* 2. the user is authenticated.
|
||||
* */
|
||||
const isVisible = !enrolledUser && (privateOutline || authenticatedUser !== null);
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
anonymousUser: authenticatedUser === null,
|
||||
canEnroll: outline && outline.enrollAlert ? outline.enrollAlert.canEnroll : false,
|
||||
courseId,
|
||||
};
|
||||
}), [authenticatedUser, courseId, outline]);
|
||||
|
||||
useAlert(isVisible, {
|
||||
code: 'clientPrivateCourseAlert',
|
||||
dismissible: false,
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'outline-private-alerts',
|
||||
type: ALERT_TYPES.WELCOME,
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Alert, Button } from '@edx/paragon';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ScheduledContentAlert({ payload }) {
|
||||
const ScheduledContentAlert = ({ payload }) => {
|
||||
const {
|
||||
datesTabLink,
|
||||
} = payload;
|
||||
@@ -38,7 +38,7 @@ function ScheduledContentAlert({ payload }) {
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ScheduledContentAlert.propTypes = {
|
||||
payload: PropTypes.shape({
|
||||
|
||||
@@ -20,12 +20,12 @@ const useScheduledContentAlert = (courseId) => {
|
||||
&& !!Object.values(courses).find(course => course.hasScheduledContent === true)
|
||||
);
|
||||
const { isEnrolled } = useModel('courseHomeMeta', courseId);
|
||||
const payload = {
|
||||
const payload = useMemo(() => ({
|
||||
datesTabLink,
|
||||
};
|
||||
}), [datesTabLink]);
|
||||
useAlert(hasScheduledContent && isEnrolled, {
|
||||
code: 'ScheduledContentAlert',
|
||||
payload: useMemo(() => payload, Object.values(payload).sort()),
|
||||
payload,
|
||||
topic: 'outline-course-alerts',
|
||||
});
|
||||
|
||||
|
||||
@@ -231,6 +231,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Expiring Soon',
|
||||
description: 'A label to indicate that proctortrack onboarding exam will expire soon',
|
||||
},
|
||||
expiredProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.expired',
|
||||
defaultMessage: 'Expired',
|
||||
description: 'A label to indicate that proctortrack onboarding exam has expired',
|
||||
},
|
||||
proctoringCurrentStatus: {
|
||||
id: 'learning.proctoringPanel.status',
|
||||
defaultMessage: 'Current Onboarding Status:',
|
||||
@@ -278,9 +283,14 @@ const messages = defineMessages({
|
||||
},
|
||||
expiringSoonProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.expiringSoon',
|
||||
defaultMessage: 'Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
||||
defaultMessage: 'Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
||||
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expiring soon)',
|
||||
},
|
||||
expiredProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.expired',
|
||||
defaultMessage: 'Your onboarding status has expired. Please complete onboarding again to continue taking proctored exams.',
|
||||
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expired)',
|
||||
},
|
||||
proctoringPanelGeneralInfo: {
|
||||
id: 'learning.proctoringPanel.generalInfo',
|
||||
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -8,11 +7,9 @@ import DateSummary from '../DateSummary';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function CourseDates({
|
||||
const CourseDates = ({
|
||||
intl,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -40,8 +37,6 @@ function CourseDates({
|
||||
key={courseDateBlock.title + courseDateBlock.date}
|
||||
dateBlock={courseDateBlock}
|
||||
userTimezone={userTimezone}
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p={mmp2p}
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
@@ -51,17 +46,10 @@ function CourseDates({
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseDates.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
CourseDates.defaultProps = {
|
||||
/** [MM-P2P] Experiment */
|
||||
mmp2p: {},
|
||||
};
|
||||
|
||||
export default injectIntl(CourseDates);
|
||||
|
||||
@@ -7,7 +7,7 @@ import LmsHtmlFragment from '../LmsHtmlFragment';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function CourseHandouts({ intl }) {
|
||||
const CourseHandouts = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -29,7 +29,7 @@ function CourseHandouts({ intl }) {
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseHandouts.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -14,7 +14,7 @@ import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
||||
|
||||
function CourseTools({ intl }) {
|
||||
const CourseTools = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -79,7 +79,7 @@ function CourseTools({ intl }) {
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseTools.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -2,35 +2,35 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
function FlagButton({
|
||||
const FlagButton = ({
|
||||
buttonIcon,
|
||||
title,
|
||||
text,
|
||||
handleSelect,
|
||||
isSelected,
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classnames('flag-button row w-100 align-content-between m-1.5 py-3.5',
|
||||
isSelected ? 'flag-button-selected' : '')}
|
||||
aria-checked={isSelected}
|
||||
role="radio"
|
||||
onClick={() => handleSelect()}
|
||||
data-testid={`weekly-learning-goal-input-${title}`}
|
||||
>
|
||||
<div className="row w-100 m-0 justify-content-center pb-1">
|
||||
{buttonIcon}
|
||||
</div>
|
||||
<div className={classnames('row w-100 m-0 justify-content-center small text-gray-700 pb-1', isSelected ? 'font-weight-bold' : '')}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={classnames('row w-100 m-0 justify-content-center micro text-gray-500', isSelected ? 'font-weight-bold' : '')}>
|
||||
{text}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
className={classnames(
|
||||
'flag-button row w-100 align-content-between m-1.5 py-3.5',
|
||||
isSelected ? 'flag-button-selected' : '',
|
||||
)}
|
||||
aria-checked={isSelected}
|
||||
role="radio"
|
||||
onClick={() => handleSelect()}
|
||||
data-testid={`weekly-learning-goal-input-${title}`}
|
||||
>
|
||||
<div className="row w-100 m-0 justify-content-center pb-1">
|
||||
{buttonIcon}
|
||||
</div>
|
||||
<div className={classnames('row w-100 m-0 justify-content-center small text-gray-700 pb-1', isSelected ? 'font-weight-bold' : '')}>
|
||||
{title}
|
||||
</div>
|
||||
<div className={classnames('row w-100 m-0 justify-content-center micro text-gray-500', isSelected ? 'font-weight-bold' : '')}>
|
||||
{text}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
FlagButton.propTypes = {
|
||||
buttonIcon: PropTypes.element.isRequired,
|
||||
|
||||
@@ -9,12 +9,12 @@ import { ReactComponent as FlagRegularIcon } from './flag_gray.svg';
|
||||
import FlagButton from './FlagButton';
|
||||
import messages from '../messages';
|
||||
|
||||
function LearningGoalButton({
|
||||
const LearningGoalButton = ({
|
||||
level,
|
||||
isSelected,
|
||||
handleSelect,
|
||||
intl,
|
||||
}) {
|
||||
}) => {
|
||||
const buttonDetails = {
|
||||
casual: {
|
||||
daysPerWeek: 1,
|
||||
@@ -47,7 +47,7 @@ function LearningGoalButton({
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LearningGoalButton.propTypes = {
|
||||
level: PropTypes.string.isRequired,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getProctoringInfoData } from '../../data/api';
|
||||
import { fetchProctoringInfoResolved } from '../../data/slice';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function ProctoringInfoPanel({ intl }) {
|
||||
const ProctoringInfoPanel = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -35,6 +35,7 @@ function ProctoringInfoPanel({ intl }) {
|
||||
error: 'error',
|
||||
otherCourseApproved: 'otherCourseApproved',
|
||||
expiringSoon: 'expiringSoon',
|
||||
expired: 'expired',
|
||||
};
|
||||
|
||||
function getReadableStatusClass(examStatus) {
|
||||
@@ -54,9 +55,14 @@ function ProctoringInfoPanel({ intl }) {
|
||||
return readableClass;
|
||||
}
|
||||
|
||||
function isNotYetSubmitted(examStatus) {
|
||||
const NO_SHOW_STATES = ['submitted', 'second_review_required', 'verified'];
|
||||
return !NO_SHOW_STATES.includes(examStatus);
|
||||
function isCurrentlySubmitted(examStatus) {
|
||||
const SUBMITTED_STATES = ['submitted', 'second_review_required'];
|
||||
return SUBMITTED_STATES.includes(examStatus);
|
||||
}
|
||||
|
||||
function isSubmissionRequired(examStatus) {
|
||||
const OK_STATES = [readableStatuses.submitted, readableStatuses.verified];
|
||||
return !OK_STATES.includes(examStatus);
|
||||
}
|
||||
|
||||
function isNotYetReleased(examReleaseDate) {
|
||||
@@ -77,11 +83,19 @@ function ProctoringInfoPanel({ intl }) {
|
||||
return borderClass;
|
||||
}
|
||||
|
||||
function isExpiringSoon(dateString) {
|
||||
// Returns true if the expiration date is within 28 days
|
||||
function isExpired(dateString) {
|
||||
// Returns true if the expiration date has passed
|
||||
const today = new Date();
|
||||
const expirationDateObject = new Date(dateString);
|
||||
return today > expirationDateObject.getTime() - 2419200000;
|
||||
return today >= expirationDateObject.getTime();
|
||||
}
|
||||
|
||||
function isExpiringSoon(dateString) {
|
||||
// Returns true if the expiration date is within 28 days
|
||||
const twentyeightDays = 28 * 24 * 60 * 60 * 1000;
|
||||
const today = new Date();
|
||||
const expirationDateObject = new Date(dateString);
|
||||
return today > expirationDateObject.getTime() - twentyeightDays;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -96,7 +110,9 @@ function ProctoringInfoPanel({ intl }) {
|
||||
setStatus(response.onboarding_status);
|
||||
setLink(response.onboarding_link);
|
||||
const expirationDate = response.expiration_date;
|
||||
if (expirationDate && isExpiringSoon(expirationDate)) {
|
||||
if (expirationDate && isExpired(expirationDate)) {
|
||||
setReadableStatus(getReadableStatusClass('expired'));
|
||||
} else if (expirationDate && isExpiringSoon(expirationDate)) {
|
||||
setReadableStatus(getReadableStatusClass('expiringSoon'));
|
||||
} else {
|
||||
setReadableStatus(getReadableStatusClass(response.onboarding_status));
|
||||
@@ -112,6 +128,7 @@ function ProctoringInfoPanel({ intl }) {
|
||||
.finally(() => {
|
||||
dispatch(fetchProctoringInfoResolved());
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
let onboardingExamButton = null;
|
||||
@@ -154,6 +171,7 @@ function ProctoringInfoPanel({ intl }) {
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
<>
|
||||
{ showInfoPanel && (
|
||||
<section className={`mb-4 p-3 outline-sidebar-proctoring-panel ${getBorderClass()}`}>
|
||||
@@ -175,17 +193,17 @@ function ProctoringInfoPanel({ intl }) {
|
||||
{![readableStatuses.verified, readableStatuses.otherCourseApproved].includes(readableStatus) && (
|
||||
<>
|
||||
<p>
|
||||
{isNotYetSubmitted(status) && (
|
||||
{!isCurrentlySubmitted(status) && (
|
||||
intl.formatMessage(messages.proctoringPanelGeneralInfo)
|
||||
)}
|
||||
{!isNotYetSubmitted(status) && (
|
||||
{isCurrentlySubmitted(status) && (
|
||||
intl.formatMessage(messages.proctoringPanelGeneralInfoSubmitted)
|
||||
)}
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages.proctoringPanelGeneralTime)}</p>
|
||||
</>
|
||||
)}
|
||||
{isNotYetSubmitted(status) && (
|
||||
{isSubmissionRequired(readableStatus) && (
|
||||
onboardingExamButton
|
||||
)}
|
||||
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
||||
@@ -196,7 +214,7 @@ function ProctoringInfoPanel({ intl }) {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProctoringInfoPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function StartOrResumeCourseCard({ intl }) {
|
||||
const StartOrResumeCourseCard = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = 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 */}
|
||||
{/* eslint-disable-next-line react/jsx-no-useless-fragment */}
|
||||
<Card.Footer><></></Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
StartOrResumeCourseCard.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -15,11 +15,11 @@ import { saveWeeklyLearningGoal } from '../../data';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import './FlagButton.scss';
|
||||
|
||||
function WeeklyLearningGoalCard({
|
||||
const WeeklyLearningGoalCard = ({
|
||||
daysPerWeek,
|
||||
subscribedToReminders,
|
||||
intl,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -36,7 +36,7 @@ function WeeklyLearningGoalCard({
|
||||
const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders);
|
||||
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
|
||||
const selectReminders = daysPerWeekGoal === null ? true : isGetReminderSelected;
|
||||
setGetReminderSelected(selectReminders);
|
||||
@@ -51,10 +51,10 @@ function WeeklyLearningGoalCard({
|
||||
reminder_selected: selectReminders,
|
||||
});
|
||||
if (triggeredFromEmail) {
|
||||
sendTrackEvent('welcome.email.clicked.setgoal', {});
|
||||
sendTrackEvent('enrollment.email.clicked.setgoal', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleSubscribeToReminders(event) {
|
||||
const isGetReminderChecked = event.target.checked;
|
||||
@@ -84,6 +84,7 @@ function WeeklyLearningGoalCard({
|
||||
search: currentParams.toString(),
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.search]);
|
||||
|
||||
return (
|
||||
@@ -146,7 +147,7 @@ function WeeklyLearningGoalCard({
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
WeeklyLearningGoalCard.propTypes = {
|
||||
daysPerWeek: PropTypes.number,
|
||||
|
||||
@@ -11,21 +11,22 @@ import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import { dismissWelcomeMessage } from '../../data/thunks';
|
||||
|
||||
function WelcomeMessage({ courseId, intl }) {
|
||||
const WelcomeMessage = ({ courseId, intl }) => {
|
||||
const {
|
||||
welcomeMessageHtml,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
if (!welcomeMessageHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [display, setDisplay] = useState(true);
|
||||
|
||||
const shortWelcomeMessageHtml = truncate(welcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true });
|
||||
const messageCanBeShortened = shortWelcomeMessageHtml.length < welcomeMessageHtml.length;
|
||||
const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
if (!welcomeMessageHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
data-testid="alert-container-welcome"
|
||||
@@ -69,7 +70,7 @@ function WelcomeMessage({ courseId, intl }) {
|
||||
</TransitionReplace>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
WelcomeMessage.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useModel } from '../../generic/model-store';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function ProgressHeader({ intl }) {
|
||||
const ProgressHeader = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
targetUserId,
|
||||
@@ -26,18 +26,16 @@ function ProgressHeader({ intl }) {
|
||||
: intl.formatMessage(messages.progressHeader);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row w-100 m-0 mt-3 mb-4 justify-content-between">
|
||||
<h1>{pageTitle}</h1>
|
||||
{administrator && studioUrl && (
|
||||
<Button variant="outline-primary" size="sm" className="align-self-center" href={studioUrl}>
|
||||
{intl.formatMessage(messages.studioLink)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<div className="row w-100 m-0 mt-3 mb-4 justify-content-between">
|
||||
<h1>{pageTitle}</h1>
|
||||
{administrator && studioUrl && (
|
||||
<Button variant="outline-primary" size="sm" className="align-self-center" href={studioUrl}>
|
||||
{intl.formatMessage(messages.studioLink)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProgressHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -12,7 +12,7 @@ import RelatedLinks from './related-links/RelatedLinks';
|
||||
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
function ProgressTab() {
|
||||
const ProgressTab = () => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -55,6 +55,6 @@ function ProgressTab() {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ProgressTab;
|
||||
|
||||
@@ -1237,6 +1237,7 @@ describe('Progress Tab', () => {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}),
|
||||
{ exact: false },
|
||||
)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1302,7 +1303,7 @@ describe('Progress Tab', () => {
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
||||
expect(screen.getByText('This learner no longer has access to this course. Their access expired on', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2020', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
it('does not render banner when not masquerading', async () => {
|
||||
setMetadata({ is_enrolled: true, original_user_is_staff: true });
|
||||
@@ -1315,7 +1316,7 @@ describe('Progress Tab', () => {
|
||||
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.queryByText('This learner no longer has access to this course. Their access expired on', { exact: false })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2020')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2020', { exact: false })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1331,7 +1332,7 @@ describe('Progress Tab', () => {
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.getByTestId('instructor-toolbar')).toBeInTheDocument();
|
||||
expect(screen.getByText('This learner does not yet have access to this course. The course starts on', { exact: false })).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2999')).toBeInTheDocument();
|
||||
expect(screen.getByText('1/1/2999', { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
it('does not render banner when not masquerading', async () => {
|
||||
setMetadata({
|
||||
@@ -1343,7 +1344,7 @@ describe('Progress Tab', () => {
|
||||
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||
await act(async () => render(<LoadedTabPage courseId={courseId} activeTabSlug="progress">...</LoadedTabPage>, { store }));
|
||||
expect(screen.queryByText('This learner does not yet have access to this course. The course starts on', { exact: false })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2999')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('1/1/2999', { exact: false })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../
|
||||
import { requestCert } from '../../data/thunks';
|
||||
import messages from './messages';
|
||||
|
||||
function CertificateStatus({ intl }) {
|
||||
const CertificateStatus = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -206,6 +206,7 @@ function CertificateStatus({ intl }) {
|
||||
grade_variant: gradeEventName,
|
||||
certificate_status_variant: certEventName,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (!certCase) {
|
||||
@@ -257,7 +258,7 @@ function CertificateStatus({ intl }) {
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CertificateStatus.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -6,13 +6,13 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
|
||||
const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) => {
|
||||
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
||||
|
||||
if (!completePercentage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [showCompletePopover, setShowCompletePopover] = useState(false);
|
||||
|
||||
const completeSegmentOffset = (3.6 * completePercentage) / 8;
|
||||
let completeTooltipDegree = completePercentage < 100 ? -completeSegmentOffset : 0;
|
||||
|
||||
@@ -78,7 +78,7 @@ function CompleteDonutSegment({ completePercentage, intl, lockedPercentage }) {
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CompleteDonutSegment.propTypes = {
|
||||
completePercentage: PropTypes.number.isRequired,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
import CompleteDonutSegment from './CompleteDonutSegment';
|
||||
@@ -8,7 +10,7 @@ import IncompleteDonutSegment from './IncompleteDonutSegment';
|
||||
import LockedDonutSegment from './LockedDonutSegment';
|
||||
import messages from './messages';
|
||||
|
||||
function CompletionDonutChart({ intl }) {
|
||||
const CompletionDonutChart = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -26,6 +28,8 @@ function CompletionDonutChart({ intl }) {
|
||||
const lockedPercentage = lockedCount ? Number(((lockedCount / numTotalUnits) * 100).toFixed(0)) : 0;
|
||||
const incompletePercentage = 100 - completePercentage - lockedPercentage;
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg role="img" width="50%" height="100%" viewBox="0 0 42 42" className="donut" style={{ maxWidth: '178px' }} aria-hidden="true">
|
||||
@@ -35,7 +39,7 @@ function CompletionDonutChart({ intl }) {
|
||||
<circle className="donut-hole" fill="#fff" cx="21" cy="21" r="15.91549430918954" />
|
||||
<g className="donut-chart-text">
|
||||
<text x="50%" y="50%" className="donut-chart-number">
|
||||
{completePercentage}%
|
||||
{completePercentage}{isLocaleRtl && '\u200f'}%
|
||||
</text>
|
||||
<text x="50%" y="50%" className="donut-chart-label">
|
||||
{intl.formatMessage(messages.donutLabel)}
|
||||
@@ -56,7 +60,7 @@ function CompletionDonutChart({ intl }) {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CompletionDonutChart.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -4,23 +4,21 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import CompletionDonutChart from './CompletionDonutChart';
|
||||
import messages from './messages';
|
||||
|
||||
function CourseCompletion({ intl }) {
|
||||
return (
|
||||
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||
<p className="small">
|
||||
{intl.formatMessage(messages.completionBody)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
||||
<CompletionDonutChart />
|
||||
</div>
|
||||
const CourseCompletion = ({ intl }) => (
|
||||
<section className="text-dark-700 mb-4 rounded raised-card p-4">
|
||||
<div className="row w-100 m-0">
|
||||
<div className="col-12 col-sm-6 col-md-7 p-0">
|
||||
<h2>{intl.formatMessage(messages.courseCompletion)}</h2>
|
||||
<p className="small">
|
||||
{intl.formatMessage(messages.completionBody)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
<div className="col-12 col-sm-6 col-md-5 mt-sm-n3 p-0 text-center">
|
||||
<CompletionDonutChart />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
CourseCompletion.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -6,13 +6,13 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function IncompleteDonutSegment({ incompletePercentage, intl }) {
|
||||
const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
|
||||
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
||||
|
||||
if (!incompletePercentage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [showIncompletePopover, setShowIncompletePopover] = useState(false);
|
||||
|
||||
const incompleteSegmentOffset = (3.6 * incompletePercentage) / 16;
|
||||
const incompleteTooltipDegree = incompletePercentage < 100 ? incompleteSegmentOffset : 0;
|
||||
|
||||
@@ -49,7 +49,7 @@ function IncompleteDonutSegment({ incompletePercentage, intl }) {
|
||||
</OverlayTrigger>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
IncompleteDonutSegment.propTypes = {
|
||||
incompletePercentage: PropTypes.number.isRequired,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function LockedDonutSegment({ intl, lockedPercentage }) {
|
||||
const LockedDonutSegment = ({ intl, lockedPercentage }) => {
|
||||
const [showLockedPopover, setShowLockedPopover] = useState(false);
|
||||
|
||||
if (!lockedPercentage) {
|
||||
@@ -62,7 +62,7 @@ function LockedDonutSegment({ intl, lockedPercentage }) {
|
||||
</OverlayTrigger>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
LockedDonutSegment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DashboardLink } from '../../../shared/links';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function CreditInformation({ intl }) {
|
||||
const CreditInformation = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -106,7 +106,7 @@ function CreditInformation({ intl }) {
|
||||
{requirements}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CreditInformation.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -11,7 +11,7 @@ import CreditInformation from '../../credit-information/CreditInformation';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function CourseGrade({ intl }) {
|
||||
const CourseGrade = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -52,7 +52,7 @@ function CourseGrade({ intl }) {
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseGrade.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useModel } from '../../../../generic/model-store';
|
||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||
import messages from '../messages';
|
||||
|
||||
function CourseGradeFooter({ intl, passingGrade }) {
|
||||
const CourseGradeFooter = ({ intl, passingGrade }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -83,7 +83,7 @@ function CourseGradeFooter({ intl, passingGrade }) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseGradeFooter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Button, Icon } from '@edx/paragon';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import messages from '../messages';
|
||||
|
||||
function CourseGradeHeader({ intl }) {
|
||||
const CourseGradeHeader = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -81,7 +81,7 @@ function CourseGradeHeader({ intl }) {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CourseGradeHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
||||
const CurrentGradeTooltip = ({ intl, tooltipClassName }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -41,7 +41,7 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
||||
overlay={(
|
||||
<Popover id={`${isPassing ? 'passing' : 'non-passing'}-grade-tooltip`} aria-hidden="true" className={tooltipClassName}>
|
||||
<Popover.Content data-testid="currentGradeTooltipContent" className={isPassing ? 'text-white' : 'text-dark-700'}>
|
||||
{currentGrade.toFixed(0)}%
|
||||
{currentGrade.toFixed(0)}{isLocaleRtl ? '\u200f' : ''}%
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
@@ -62,7 +62,7 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CurrentGradeTooltip.defaultProps = {
|
||||
tooltipClassName: '',
|
||||
|
||||
@@ -2,14 +2,16 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import CurrentGradeTooltip from './CurrentGradeTooltip';
|
||||
import PassingGradeTooltip from './PassingGradeTooltip';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function GradeBar({ intl, passingGrade }) {
|
||||
const GradeBar = ({ intl, passingGrade }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -26,14 +28,16 @@ function GradeBar({ intl, passingGrade }) {
|
||||
|
||||
const lockedTooltipClassName = gradesFeatureIsFullyLocked ? 'locked-overlay' : '';
|
||||
|
||||
const adjustedRtlStyle = (percentOffest) => (isRtl(getLocale()) ? { transform: `translateX(${100 - percentOffest}%)` } : {});
|
||||
|
||||
return (
|
||||
<div className="col-12 col-sm-6 align-self-center p-0">
|
||||
<div className="sr-only">{intl.formatMessage(messages.courseGradeBarAltText, { currentGrade, passingGrade })}</div>
|
||||
<svg width="100%" height="100px" className="grade-bar" aria-hidden="true">
|
||||
<g style={{ transform: 'translateY(2.61em)' }}>
|
||||
<rect className="grade-bar__base" width="100%" />
|
||||
<rect className="grade-bar--passing" width={`${passingGrade}%`} />
|
||||
<rect className={`grade-bar--current-${isPassing ? 'passing' : 'non-passing'}`} width={`${currentGrade}%`} />
|
||||
<rect className="grade-bar--passing" width={`${passingGrade}%`} style={adjustedRtlStyle(passingGrade)} />
|
||||
<rect className={`grade-bar--current-${isPassing ? 'passing' : 'non-passing'}`} width={`${currentGrade}%`} style={adjustedRtlStyle(currentGrade)} />
|
||||
|
||||
{/* Start divider */}
|
||||
<rect className="grade-bar__divider" />
|
||||
@@ -45,7 +49,7 @@ function GradeBar({ intl, passingGrade }) {
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
GradeBar.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
|
||||
const GradeRangeTooltip = ({ intl, iconButtonClassName, passingGrade }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -72,7 +72,7 @@ function GradeRangeTooltip({ intl, iconButtonClassName, passingGrade }) {
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
GradeRangeTooltip.defaultProps = {
|
||||
iconButtonClassName: '',
|
||||
|
||||
@@ -8,7 +8,7 @@ import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
||||
const PassingGradeTooltip = ({ intl, passingGrade, tooltipClassName }) => {
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
||||
@@ -25,7 +25,7 @@ function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
||||
overlay={(
|
||||
<Popover id="minimum-grade-tooltip" className={`bg-primary-500 ${tooltipClassName}`} aria-hidden="true">
|
||||
<Popover.Content className="text-white">
|
||||
{passingGrade}%
|
||||
{passingGrade}{isLocaleRtl && '\u200f'}%
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
@@ -47,7 +47,7 @@ function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PassingGradeTooltip.defaultProps = {
|
||||
tooltipClassName: '',
|
||||
|
||||
@@ -12,13 +12,14 @@ import DetailedGradesTable from './DetailedGradesTable';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function DetailedGrades({ intl }) {
|
||||
const DetailedGrades = ({ intl }) => {
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const {
|
||||
org,
|
||||
tabs,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const {
|
||||
gradesFeatureIsFullyLocked,
|
||||
@@ -36,11 +37,14 @@ function DetailedGrades({ intl }) {
|
||||
});
|
||||
};
|
||||
|
||||
const outlineLink = (
|
||||
const overviewTab = tabs.find(tab => tab.slug === 'outline');
|
||||
const overviewTabUrl = overviewTab && overviewTab.url;
|
||||
|
||||
const outlineLink = overviewTabUrl && (
|
||||
<Hyperlink
|
||||
variant="muted"
|
||||
isInline
|
||||
destination={`/course/${courseId}/home`}
|
||||
destination={overviewTabUrl}
|
||||
onClick={logOutlineLinkClick}
|
||||
tabIndex={gradesFeatureIsFullyLocked ? '-1' : '0'}
|
||||
>
|
||||
@@ -63,17 +67,19 @@ function DetailedGrades({ intl }) {
|
||||
{!hasSectionScores && (
|
||||
<p className="small">{intl.formatMessage(messages.detailedGradesEmpty)}</p>
|
||||
)}
|
||||
<p className="x-small m-0">
|
||||
<FormattedMessage
|
||||
id="progress.ungradedAlert"
|
||||
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
||||
description="Text that precede link that redirect to course outline page"
|
||||
values={{ outlineLink }}
|
||||
/>
|
||||
</p>
|
||||
{overviewTabUrl && (
|
||||
<p className="x-small m-0">
|
||||
<FormattedMessage
|
||||
id="progress.ungradedAlert"
|
||||
defaultMessage="For progress on ungraded aspects of the course, view your {outlineLink}."
|
||||
description="Text that precede link that redirect to course outline page"
|
||||
values={{ outlineLink }}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DetailedGrades.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import messages from '../messages';
|
||||
import SubsectionTitleCell from './SubsectionTitleCell';
|
||||
|
||||
function DetailedGradesTable({ intl }) {
|
||||
const DetailedGradesTable = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -17,6 +19,7 @@ function DetailedGradesTable({ intl }) {
|
||||
sectionScores,
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
return (
|
||||
sectionScores.map((chapter) => {
|
||||
const subsectionScores = chapter.subsections.filter(
|
||||
@@ -32,7 +35,7 @@ function DetailedGradesTable({ intl }) {
|
||||
|
||||
const detailedGradesData = subsectionScores.map((subsection) => ({
|
||||
subsectionTitle: <SubsectionTitleCell subsection={subsection} />,
|
||||
score: <span className={subsection.learnerHasAccess ? '' : 'greyed-out'}>{subsection.numPointsEarned}/{subsection.numPointsPossible}</span>,
|
||||
score: <span className={subsection.learnerHasAccess ? '' : 'greyed-out'}>{subsection.numPointsEarned}{isLocaleRtl ? '\\' : '/'}{subsection.numPointsPossible}</span>,
|
||||
}));
|
||||
|
||||
return (
|
||||
@@ -61,7 +64,7 @@ function DetailedGradesTable({ intl }) {
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DetailedGradesTable.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -2,24 +2,27 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function ProblemScoreDrawer({ intl, problemScores, subsection }) {
|
||||
const ProblemScoreDrawer = ({ intl, problemScores, subsection }) => {
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
return (
|
||||
<span className="row w-100 m-0 x-small ml-4 pt-2 pl-1 text-gray-700 flex-nowrap">
|
||||
<span id="problem-score-label" className="col-auto p-0">{intl.formatMessage(messages.problemScoreLabel)}</span>
|
||||
<div className={classNames('col', 'p-0', { 'greyed-out': !subsection.learnerHasAccess })}>
|
||||
<ul className="list-unstyled row w-100 m-0" aria-labelledby="problem-score-label">
|
||||
{problemScores.map(problemScore => (
|
||||
<li className="ml-3">{problemScore.earned}/{problemScore.possible}</li>
|
||||
<li className="ml-3">{problemScore.earned}{isLocaleRtl ? '\\' : '/'}{problemScore.possible}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProblemScoreDrawer.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -14,7 +14,7 @@ import messages from '../messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import ProblemScoreDrawer from './ProblemScoreDrawer';
|
||||
|
||||
function SubsectionTitleCell({ intl, subsection }) {
|
||||
const SubsectionTitleCell = ({ intl, subsection }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -99,7 +99,7 @@ function SubsectionTitleCell({ intl, subsection }) {
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SubsectionTitleCell.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Icon } from '@edx/paragon';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import messages from '../messages';
|
||||
|
||||
function AssignmentTypeCell({
|
||||
const AssignmentTypeCell = ({
|
||||
intl, assignmentType, footnoteMarker, footnoteId, locked,
|
||||
}) {
|
||||
}) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -42,7 +42,7 @@ function AssignmentTypeCell({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
AssignmentTypeCell.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
function DroppableAssignmentFootnote({ footnotes, intl }) {
|
||||
const DroppableAssignmentFootnote = ({ footnotes, intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -37,7 +37,7 @@ function DroppableAssignmentFootnote({ footnotes, intl }) {
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
DroppableAssignmentFootnote.propTypes = {
|
||||
footnotes: PropTypes.arrayOf(PropTypes.shape({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useModel } from '../../../../generic/model-store';
|
||||
import GradeSummaryHeader from './GradeSummaryHeader';
|
||||
import GradeSummaryTable from './GradeSummaryTable';
|
||||
|
||||
function GradeSummary() {
|
||||
const GradeSummary = () => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -28,6 +28,6 @@ function GradeSummary() {
|
||||
<GradeSummaryTable setAllOfSomeAssignmentTypeIsLocked={setAllOfSomeAssignmentTypeIsLocked} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default GradeSummary;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Blocked, InfoOutline } from '@edx/paragon/icons';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
function GradeSummaryHeader({ intl, allOfSomeAssignmentTypeIsLocked }) {
|
||||
const GradeSummaryHeader = ({ intl, allOfSomeAssignmentTypeIsLocked }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -54,7 +54,7 @@ function GradeSummaryHeader({ intl, allOfSomeAssignmentTypeIsLocked }) {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
GradeSummaryHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -2,7 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
@@ -12,7 +14,7 @@ import GradeSummaryTableFooter from './GradeSummaryTableFooter';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
||||
const GradeSummaryTable = ({ intl, setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -66,15 +68,27 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
||||
|
||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignment.type);
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return {
|
||||
type: {
|
||||
footnoteId, footnoteMarker, type: assignment.type, locked,
|
||||
},
|
||||
weight: { weight: `${(assignment.weight * 100).toFixed(0)}%`, locked },
|
||||
grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}%`, locked },
|
||||
weight: { weight: `${(assignment.weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
grade: { grade: `${(assignment.averageGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${(assignment.weightedGrade * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
};
|
||||
});
|
||||
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 (
|
||||
<>
|
||||
@@ -85,45 +99,28 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
||||
{
|
||||
Header: `${intl.formatMessage(messages.assignmentType)}`,
|
||||
accessor: 'type',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
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
|
||||
/>
|
||||
),
|
||||
Cell: ({ value }) => getAssignmentTypeCell(value),
|
||||
headerClassName: 'h5 mb-0',
|
||||
},
|
||||
{
|
||||
Header: `${intl.formatMessage(messages.weight)}`,
|
||||
accessor: 'weight',
|
||||
headerClassName: 'justify-content-end h5 mb-0',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
Cell: ({ value }) => (
|
||||
<span className={value.locked ? 'greyed-out' : ''}>{value.weight}</span> // eslint-disable-line react/prop-types
|
||||
),
|
||||
Cell: ({ value }) => getCell(value.locked, value.weight),
|
||||
cellClassName: 'text-right small',
|
||||
},
|
||||
{
|
||||
Header: `${intl.formatMessage(messages.grade)}`,
|
||||
accessor: 'grade',
|
||||
headerClassName: 'justify-content-end h5 mb-0',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
Cell: ({ value }) => (
|
||||
<span className={value.locked ? 'greyed-out' : ''}>{value.grade}</span> // eslint-disable-line react/prop-types
|
||||
),
|
||||
Cell: ({ value }) => getCell(value.locked, value.grade),
|
||||
cellClassName: 'text-right small',
|
||||
},
|
||||
{
|
||||
Header: `${intl.formatMessage(messages.weightedGrade)}`,
|
||||
accessor: 'weightedGrade',
|
||||
headerClassName: 'justify-content-end h5 mb-0 text-right',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
Cell: ({ value }) => (
|
||||
<span className={value.locked ? 'greyed-out' : ''}>{value.weightedGrade}</span> // eslint-disable-line react/prop-types
|
||||
),
|
||||
Cell: ({ value }) => getCell(value.locked, value.weightedGrade),
|
||||
cellClassName: 'text-right font-weight-bold small',
|
||||
},
|
||||
]}
|
||||
@@ -137,7 +134,7 @@ function GradeSummaryTable({ intl, setAllOfSomeAssignmentTypeIsLocked }) {
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
GradeSummaryTable.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
getLocale, injectIntl, intlShape, isRtl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
function GradeSummaryTableFooter({ intl }) {
|
||||
const GradeSummaryTableFooter = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -22,15 +24,17 @@ function GradeSummaryTableFooter({ intl }) {
|
||||
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
||||
const totalGrade = (percent * 100).toFixed(0);
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
return (
|
||||
<DataTable.TableFooter className={`border-top border-primary ${bgColor}`}>
|
||||
<div className="row w-100 m-0">
|
||||
<div id="weighted-grade-summary" className="col-8 p-0 small">{intl.formatMessage(messages.weightedGradeSummary)}</div>
|
||||
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}%</div>
|
||||
<div data-testid="gradeSummaryFooterTotalWeightedGrade" aria-labelledby="weighted-grade-summary" className="col-4 p-0 text-right font-weight-bold small">{totalGrade}{isLocaleRtl && '\u200f'}%</div>
|
||||
</div>
|
||||
</DataTable.TableFooter>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
GradeSummaryTableFooter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -9,12 +9,13 @@ import { Hyperlink } from '@edx/paragon';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
function RelatedLinks({ intl }) {
|
||||
const RelatedLinks = ({ intl }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
const {
|
||||
org,
|
||||
tabs,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
@@ -27,26 +28,35 @@ function RelatedLinks({ intl }) {
|
||||
});
|
||||
};
|
||||
|
||||
const overviewTab = tabs.find(tab => tab.slug === 'outline');
|
||||
const overviewTabUrl = overviewTab && overviewTab.url;
|
||||
const datesTab = tabs.find(tab => tab.slug === 'dates');
|
||||
const datesTabUrl = datesTab && datesTab.url;
|
||||
|
||||
return (
|
||||
<section className="mb-4 x-small">
|
||||
<h3 className="h4">{intl.formatMessage(messages.relatedLinks)}</h3>
|
||||
<ul className="pl-4">
|
||||
{datesTabUrl && (
|
||||
<li>
|
||||
<Hyperlink destination={`/course/${courseId}/dates`} onClick={() => logLinkClicked('dates')}>
|
||||
<Hyperlink destination={datesTabUrl} onClick={() => logLinkClicked('dates')}>
|
||||
{intl.formatMessage(messages.datesCardLink)}
|
||||
</Hyperlink>
|
||||
<p>{intl.formatMessage(messages.datesCardDescription)}</p>
|
||||
</li>
|
||||
)}
|
||||
{overviewTabUrl && (
|
||||
<li>
|
||||
<Hyperlink destination={`/course/${courseId}/home`} onClick={() => logLinkClicked('course_outline')}>
|
||||
<Hyperlink destination={overviewTabUrl} onClick={() => logLinkClicked('course_outline')}>
|
||||
{intl.formatMessage(messages.outlineCardLink)}
|
||||
</Hyperlink>
|
||||
<p>{intl.formatMessage(messages.outlineCardDescription)}</p>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
RelatedLinks.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { resetDeadlines } from '../data';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
function ShiftDatesAlert({ fetch, intl, model }) {
|
||||
const ShiftDatesAlert = ({ fetch, intl, model }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -29,12 +29,12 @@ function ShiftDatesAlert({ fetch, intl, model }) {
|
||||
missedGatedContent,
|
||||
} = datesBannerInfo;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
if (!missedDeadlines || missedGatedContent || hasEnded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<Row className="w-100 m-0">
|
||||
@@ -55,7 +55,7 @@ function ShiftDatesAlert({ fetch, intl, model }) {
|
||||
</Row>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ShiftDatesAlert.propTypes = {
|
||||
fetch: PropTypes.func.isRequired,
|
||||
|
||||
@@ -3,13 +3,11 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
function SuggestedScheduleHeader({ intl }) {
|
||||
return (
|
||||
<p className="large">
|
||||
{intl.formatMessage(messages.suggestedSchedule)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
const SuggestedScheduleHeader = ({ intl }) => (
|
||||
<p className="large">
|
||||
{intl.formatMessage(messages.suggestedSchedule)}
|
||||
</p>
|
||||
);
|
||||
|
||||
SuggestedScheduleHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
function UpgradeToCompleteAlert({ intl, logUpgradeLinkClick }) {
|
||||
const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -55,7 +55,7 @@ function UpgradeToCompleteAlert({ intl, logUpgradeLinkClick }) {
|
||||
</Row>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
UpgradeToCompleteAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
function UpgradeToShiftDatesAlert({ intl, logUpgradeLinkClick, model }) {
|
||||
const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -57,7 +57,7 @@ function UpgradeToShiftDatesAlert({ intl, logUpgradeLinkClick, model }) {
|
||||
</Row>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
UpgradeToShiftDatesAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
@@ -6,30 +6,28 @@ import classNames from 'classnames';
|
||||
import messages from './messages';
|
||||
import Tabs from '../generic/tabs/Tabs';
|
||||
|
||||
function CourseTabsNavigation({
|
||||
const CourseTabsNavigation = ({
|
||||
activeTabSlug, className, tabs, intl,
|
||||
}) {
|
||||
return (
|
||||
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
|
||||
<div className="container-xl">
|
||||
<Tabs
|
||||
className="nav-underline-tabs"
|
||||
aria-label={intl.formatMessage(messages.courseMaterial)}
|
||||
>
|
||||
{tabs.map(({ url, title, slug }) => (
|
||||
<a
|
||||
key={slug}
|
||||
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
}) => (
|
||||
<div id="courseTabsNavigation" className={classNames('course-tabs-navigation', className)}>
|
||||
<div className="container-xl">
|
||||
<Tabs
|
||||
className="nav-underline-tabs"
|
||||
aria-label={intl.formatMessage(messages.courseMaterial)}
|
||||
>
|
||||
{tabs.map(({ url, title, slug }) => (
|
||||
<a
|
||||
key={slug}
|
||||
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
CourseTabsNavigation.propTypes = {
|
||||
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
|
||||
const checkUnitToSequenceUnitRedirect = memoize((
|
||||
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId,
|
||||
) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
||||
if (sequenceMightBeUnit) {
|
||||
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then we need to look up the
|
||||
// correct parent sequence for it, and redirect there.
|
||||
const unitId = sequenceId; // just for clarity during the rest of this method
|
||||
getSequenceForUnitDeprecated(courseId, unitId).then(
|
||||
parentId => {
|
||||
if (parentId) {
|
||||
history.replace(`/course/${courseId}/${parentId}/${unitId}`);
|
||||
} else {
|
||||
const checkUnitToSequenceUnitRedirect = memoize(
|
||||
(courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, section, routeUnitId) => {
|
||||
if (courseStatus === 'loaded' && sequenceStatus === 'failed' && !section && !routeUnitId) {
|
||||
if (sequenceMightBeUnit) {
|
||||
// If the sequence failed to load as a sequence, but it is marked as a possible unit, then
|
||||
// we need to look up the correct parent sequence for it, and redirect there.
|
||||
const unitId = sequenceId; // just for clarity during the rest of this method
|
||||
getSequenceForUnitDeprecated(courseId, unitId).then(
|
||||
parentId => {
|
||||
if (parentId) {
|
||||
history.replace(`/course/${courseId}/${parentId}/${unitId}`);
|
||||
} else {
|
||||
history.replace(`/course/${courseId}`);
|
||||
}
|
||||
},
|
||||
() => { // error case
|
||||
history.replace(`/course/${courseId}`);
|
||||
}
|
||||
},
|
||||
() => { // error case
|
||||
history.replace(`/course/${courseId}`);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 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
|
||||
const checkSequenceToSequenceUnitRedirect = memoize((courseId, sequenceStatus, sequence, unitId) => {
|
||||
@@ -225,9 +225,9 @@ class CoursewareContainer extends Component {
|
||||
// Check unit to sequence-unit redirect:
|
||||
// /course/:courseId/:unitId -> /course/:courseId/:sequenceId/:unitId
|
||||
// by filling in the ID of the parent sequence of :unitId.
|
||||
checkUnitToSequenceUnitRedirect(
|
||||
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, sectionViaSequenceId, routeUnitId,
|
||||
);
|
||||
checkUnitToSequenceUnitRedirect((
|
||||
courseStatus, courseId, sequenceStatus, sequenceMightBeUnit, sequenceId, sectionViaSequenceId, routeUnitId
|
||||
));
|
||||
|
||||
// Check sequence to sequence-unit redirect:
|
||||
// /course/:courseId/:sequenceId -> /course/:courseId/:sequenceId/:unitId
|
||||
@@ -255,7 +255,7 @@ class CoursewareContainer extends Component {
|
||||
|
||||
this.props.checkBlockCompletion(courseId, sequenceId, routeUnitId);
|
||||
history.push(`/course/${courseId}/${sequenceId}/${nextUnitId}`);
|
||||
}
|
||||
};
|
||||
|
||||
handleNextSequenceClick = () => {
|
||||
const {
|
||||
@@ -274,14 +274,14 @@ class CoursewareContainer extends Component {
|
||||
handleNextSectionCelebration(sequenceId, nextSequence.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviousSequenceClick = () => {
|
||||
const { previousSequence, courseId } = this.props;
|
||||
if (previousSequence !== null) {
|
||||
history.push(`/course/${courseId}/${previousSequence.id}/last`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
@@ -320,6 +320,7 @@ const sequenceShape = PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
unitIds: PropTypes.arrayOf(PropTypes.string),
|
||||
sectionId: PropTypes.string.isRequired,
|
||||
saveUnitPosition: PropTypes.any, // eslint-disable-line
|
||||
});
|
||||
|
||||
const sectionShape = PropTypes.shape({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user