Compare commits
111 Commits
rijuma/cou
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3961455e3d | ||
|
|
b282bc05df | ||
|
|
d987aed861 | ||
|
|
9ece337504 | ||
|
|
d3235af879 | ||
|
|
d0a8778015 | ||
|
|
f8381e7900 | ||
|
|
1d5484ff1d | ||
|
|
52692dc662 | ||
|
|
f91af211f6 | ||
|
|
7318fb3ef7 | ||
|
|
7233f08d3d | ||
|
|
d6d229f1c3 | ||
|
|
47b9a436a6 | ||
|
|
e556d5b74c | ||
|
|
694d95a816 | ||
|
|
e83813da8e | ||
|
|
a54a1b8c3c | ||
|
|
d3188efbcc | ||
|
|
33f737579a | ||
|
|
870263001e | ||
|
|
af50d5a6ed | ||
|
|
7fccf7794c | ||
|
|
c760bc479b | ||
|
|
d5140a6bf0 | ||
|
|
9bf5d01c41 | ||
|
|
f3334085d7 | ||
|
|
4840fff44b | ||
|
|
579bd0365b | ||
|
|
2b4a9661a5 | ||
|
|
a6e4e28e58 | ||
|
|
e6f7588ccd | ||
|
|
db29e314c3 | ||
|
|
e9121f9261 | ||
|
|
9cbc2276d6 | ||
|
|
4c8aa7c80c | ||
|
|
68926334a1 | ||
|
|
56a73eee15 | ||
|
|
bf95916063 | ||
|
|
48270c35dd | ||
|
|
33d7d669d9 | ||
|
|
a75c89cd14 | ||
|
|
06902d8ae8 | ||
|
|
e4134641e6 | ||
|
|
77fcc83efd | ||
|
|
b0505352be | ||
|
|
ddbc2124ef | ||
|
|
462e75f6a6 | ||
|
|
bc4c8c2dec | ||
|
|
ecd5164806 | ||
|
|
44d952bef7 | ||
|
|
7eddc918bb | ||
|
|
f28528e813 | ||
|
|
ab3f5fd7bc | ||
|
|
73eaf61261 | ||
|
|
db9663b664 | ||
|
|
7edac93752 | ||
|
|
d1dede568e | ||
|
|
31b02d777f | ||
|
|
67bb54a028 | ||
|
|
847d4e5ce6 | ||
|
|
b89cdb4a69 | ||
|
|
a1d0afff6c | ||
|
|
1714f285b0 | ||
|
|
03cda5326a | ||
|
|
a71152b008 | ||
|
|
d14c2a9ffd | ||
|
|
b6c29df0a0 | ||
|
|
2ce833341b | ||
|
|
ff57a6b217 | ||
|
|
dc6ee749be | ||
|
|
236fb57023 | ||
|
|
d3d2f75c12 | ||
|
|
8e9306d35a | ||
|
|
b1ee8a3713 | ||
|
|
73406fbb31 | ||
|
|
f4ae1c51ff | ||
|
|
7ef3892027 | ||
|
|
1484bc50f7 | ||
|
|
6b197aad27 | ||
|
|
1412bfe209 | ||
|
|
e8d3bd7c24 | ||
|
|
511091055b | ||
|
|
24c9437e91 | ||
|
|
fb6f110732 | ||
|
|
1656b73a31 | ||
|
|
81671ad328 | ||
|
|
4cc716b20c | ||
|
|
756fbbac83 | ||
|
|
903fe28ff6 | ||
|
|
14c662dc53 | ||
|
|
af432eab27 | ||
|
|
dde640df33 | ||
|
|
b827db800d | ||
|
|
5b7f76b43d | ||
|
|
cf4bea3604 | ||
|
|
85e6e9266d | ||
|
|
360af1f0e9 | ||
|
|
26f4a90976 | ||
|
|
0d45c78ace | ||
|
|
c18214dc41 | ||
|
|
54611c1b4d | ||
|
|
7ca4b71ff7 | ||
|
|
63a7ff83cf | ||
|
|
8ecaa018da | ||
|
|
64ca156095 | ||
|
|
c06f2c37ab | ||
|
|
d5a092b220 | ||
|
|
81b621195e | ||
|
|
226c4cc1d7 | ||
|
|
7f6a59b701 |
4
.env
4
.env
@@ -12,10 +12,12 @@ CREDIT_HELP_LINK_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
DISCUSSIONS_MFE_BASE_URL=''
|
||||
DISCOUNT_CODE_INFO_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=''
|
||||
ENTERPRISE_LEARNER_PORTAL_URL=''
|
||||
EXAMS_BASE_URL=''
|
||||
FAVICON_URL=''
|
||||
IGNORED_ERROR_REGEX=''
|
||||
@@ -49,3 +51,5 @@ TWITTER_URL=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -8,14 +8,16 @@ APP_ID='learning'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
||||
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||
DISCOUNT_CODE_INFO_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
|
||||
EXAMS_BASE_URL=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
IGNORED_ERROR_REGEX=''
|
||||
@@ -51,3 +53,5 @@ CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||
OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -8,14 +8,16 @@ APP_ID='learning'
|
||||
BASE_URL='http://localhost:2000'
|
||||
CONTACT_URL='http://localhost:18000/contact'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CREDIT_HELP_LINK_URL='https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_credit_courses.html#keep-track-of-credit-requirements'
|
||||
CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-college-credit-or-credit-hours-for-my-course'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DISCOVERY_API_BASE_URL='http://localhost:18381'
|
||||
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
|
||||
DISCOUNT_CODE_INFO_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_JUMPNAV='true'
|
||||
ENABLE_NOTICES=''
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME='localhost:8734'
|
||||
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:8734'
|
||||
EXAMS_BASE_URL='http://localhost:18740'
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
IGNORED_ERROR_REGEX=''
|
||||
@@ -48,3 +50,5 @@ TWITTER_URL='https://twitter.com/edXOnline'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:Enterprise'
|
||||
FEATURE_ENABLE_CHAT_V2_ENDPOINT='false'
|
||||
|
||||
18
.github/workflows/add-issue-to-btr-project.yml
vendored
Normal file
18
.github/workflows/add-issue-to-btr-project.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Run the workflow that adds new tickets that are labelled "release testing"
|
||||
# to the org-wide BTR project board
|
||||
|
||||
name: Add release testing issues to the BTR project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
# This workflow is triggered when an issue is labeled with 'release testing'.
|
||||
# It adds the issue to the BTR project and applies the 'needs triage' label
|
||||
# if it doesn't already have it.
|
||||
|
||||
jobs:
|
||||
handle-release-testing:
|
||||
uses: openedx/.github/.github/workflows/add-issue-to-btr-project.yml@master
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
8
.github/workflows/validate.yml
vendored
8
.github/workflows/validate.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Download code coverage results
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: code-coverage-report
|
||||
pattern: code-coverage-report
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
@@ -41,9 +41,8 @@ Cloning and Setup
|
||||
|
||||
git clone https://github.com/openedx/frontend-app-learning.git
|
||||
|
||||
2. Use node v20.x.
|
||||
2. Use the version of Node specified in ``.nvmrc``.
|
||||
|
||||
The current version of the micro-frontend build scripts supports node 18.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an ``.nvmrc`` file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
@@ -131,7 +130,7 @@ Deployment
|
||||
|
||||
The Learning MFE is similar to all the other Open edX MFEs. Read the Open
|
||||
edX Developer Guide's section on
|
||||
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
|
||||
`MFE applications <https://openedx.github.io/frontend-platform/>`_.
|
||||
|
||||
Plugins
|
||||
=======
|
||||
@@ -145,7 +144,7 @@ Environment Variables
|
||||
This MFE is configured via environment variables supplied at build time.
|
||||
All micro-frontends have a shared set of required environment variables,
|
||||
as documented in the Open edX Developer Guide under
|
||||
`Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`_.
|
||||
`Required Environment Variables <https://openedx.github.io/frontend-platform/>`_.
|
||||
|
||||
The learning micro-frontend also supports the following additional variables:
|
||||
|
||||
|
||||
9867
package-lock.json
generated
9867
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -15,11 +15,10 @@
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"postinstall": "patch-package",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"start:with-theme": "paragon install-theme && npm start && npm install",
|
||||
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests",
|
||||
"test": "NODE_ENV=test fedx-scripts jest --coverage --passWithNoTests",
|
||||
"test:watch": "fedx-scripts jest --watch --passWithNoTests",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
@@ -35,20 +34,19 @@
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/frontend-component-header": "^5.8.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.19.2",
|
||||
"@edx/frontend-lib-special-exams": "^3.1.3",
|
||||
"@edx/frontend-platform": "^8.0.0",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/react-unit-test-utils": "3.0.0",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^8.0.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.24.0",
|
||||
"@edx/frontend-lib-special-exams": "^4.0.0",
|
||||
"@edx/frontend-platform": "^8.4.0",
|
||||
"@edx/openedx-atlas": "^0.7.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.4",
|
||||
"@openedx/frontend-build": "^14.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.2.1",
|
||||
"@openedx/frontend-slot-footer": "^1.0.2",
|
||||
"@openedx/paragon": "^22.3.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -58,12 +56,11 @@
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "^7.1.3",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-router": "6.15.0",
|
||||
@@ -77,11 +74,9 @@
|
||||
"truncate-html": "1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@pact-foundation/pact": "^13.0.0",
|
||||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"axios-mock-adapter": "2.1.0",
|
||||
"bundlewatch": "^0.4.0",
|
||||
@@ -95,7 +90,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "dist/*.js",
|
||||
"maxSize": "1400kB"
|
||||
"maxSize": "1450kB"
|
||||
}
|
||||
],
|
||||
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||
index 2879dd9..9efd0fc 100644
|
||||
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
|
||||
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const path = require('path');
|
||||
+const fs = require('fs');
|
||||
const PostCssAutoprefixerPlugin = require('autoprefixer');
|
||||
const PostCssRTLCSS = require('postcss-rtlcss');
|
||||
const PostCssCustomMediaCSS = require('postcss-custom-media');
|
||||
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
|
||||
const commonConfig = require('./webpack.common.config');
|
||||
const presets = require('../lib/presets');
|
||||
|
||||
+/**
|
||||
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
|
||||
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
|
||||
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
|
||||
+ * root directory. Its env variables can be accessed with getConfig().
|
||||
+ *
|
||||
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
|
||||
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
|
||||
+ */
|
||||
+
|
||||
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
|
||||
+
|
||||
+if (envConfigPath) {
|
||||
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
|
||||
+ fs.copyFileSync(envConfigPath, envConfigFilename);
|
||||
+}
|
||||
+
|
||||
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
|
||||
dotenv.config({
|
||||
path: path.resolve(process.cwd(), '.env'),
|
||||
@@ -1,14 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
FormattedMessage, FormattedDate, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, FormattedDate, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AccessExpirationAlert = ({ intl, payload }) => {
|
||||
const AccessExpirationAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
accessExpiration,
|
||||
courseId,
|
||||
@@ -119,7 +118,6 @@ const AccessExpirationAlert = ({ intl, payload }) => {
|
||||
};
|
||||
|
||||
AccessExpirationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
accessExpiration: PropTypes.shape({
|
||||
expirationDate: PropTypes.string.isRequired,
|
||||
@@ -134,4 +132,4 @@ AccessExpirationAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccessExpirationAlert);
|
||||
export default AccessExpirationAlert;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { WarningFilled } from '@openedx/paragon/icons';
|
||||
@@ -7,7 +7,8 @@ import { WarningFilled } from '@openedx/paragon/icons';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import genericMessages from './messages';
|
||||
|
||||
const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
||||
const ActiveEnterpriseAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const { text, courseId } = payload;
|
||||
const changeActiveEnterprise = (
|
||||
<Hyperlink
|
||||
@@ -38,11 +39,10 @@ const ActiveEnterpriseAlert = ({ intl, payload }) => {
|
||||
};
|
||||
|
||||
ActiveEnterpriseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
courseId: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ActiveEnterpriseAlert);
|
||||
export default ActiveEnterpriseAlert;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
import { Info, WarningFilled } from '@openedx/paragon/icons';
|
||||
@@ -11,7 +11,8 @@ import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
import useEnrollClickHandler from './clickHook';
|
||||
|
||||
const EnrollmentAlert = ({ intl, payload }) => {
|
||||
const EnrollmentAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
canEnroll,
|
||||
courseId,
|
||||
@@ -58,7 +59,6 @@ const EnrollmentAlert = ({ intl, payload }) => {
|
||||
};
|
||||
|
||||
EnrollmentAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
canEnroll: PropTypes.bool,
|
||||
courseId: PropTypes.string,
|
||||
@@ -67,4 +67,4 @@ EnrollmentAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EnrollmentAlert);
|
||||
export default EnrollmentAlert;
|
||||
|
||||
@@ -9,13 +9,12 @@ import {
|
||||
Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Check, ArrowForward } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendActivationEmail } from '../../courseware/data';
|
||||
import messages from './messages';
|
||||
|
||||
const AccountActivationAlert = ({
|
||||
intl,
|
||||
}) => {
|
||||
const AccountActivationAlert = () => {
|
||||
const intl = useIntl();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showSpinner, setShowSpinner] = useState(false);
|
||||
const [showCheck, setShowCheck] = useState(false);
|
||||
@@ -125,8 +124,4 @@ const AccountActivationAlert = ({
|
||||
);
|
||||
};
|
||||
|
||||
AccountActivationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccountActivationAlert);
|
||||
export default AccountActivationAlert;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { WarningFilled } from '@openedx/paragon/icons';
|
||||
|
||||
import genericMessages from '../../generic/messages';
|
||||
|
||||
const LogistrationAlert = ({ intl }) => {
|
||||
const LogistrationAlert = () => {
|
||||
const intl = useIntl();
|
||||
const signIn = (
|
||||
<Hyperlink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
@@ -43,8 +44,4 @@ const LogistrationAlert = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
LogistrationAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LogistrationAlert);
|
||||
export default LogistrationAlert;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const DECODE_ROUTES = {
|
||||
|
||||
export const ROUTES = {
|
||||
UNSUBSCRIBE: '/goal-unsubscribe/:token',
|
||||
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch',
|
||||
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch?',
|
||||
REDIRECT: '/redirect/*',
|
||||
DASHBOARD: 'dashboard',
|
||||
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Tabs, Tab } from '@openedx/paragon';
|
||||
|
||||
import { useParams } from 'react-router';
|
||||
@@ -13,7 +13,8 @@ const filterTypes = ['text', 'video', 'sequence'];
|
||||
const filterOther = 'other';
|
||||
const validFilters = [filterAll, ...filterTypes, filterOther];
|
||||
|
||||
export const CoursewareSearchResultsFilter = ({ intl }) => {
|
||||
export const CoursewareSearchResultsFilter = () => {
|
||||
const intl = useIntl();
|
||||
const { courseId } = useParams();
|
||||
const lastSearch = useModel('contentSearchResults', courseId);
|
||||
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
|
||||
@@ -63,7 +64,6 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
|
||||
variant="tabs"
|
||||
activeKey={activeKey}
|
||||
onSelect={setFilter}
|
||||
aria-label={intl.formatMessage(messages.searchResultsFilterDescription)}
|
||||
>
|
||||
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => (
|
||||
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}>
|
||||
@@ -74,8 +74,4 @@ export const CoursewareSearchResultsFilter = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CoursewareSearchResultsFilter.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearchResultsFilter);
|
||||
export default CoursewareSearchResultsFilter;
|
||||
|
||||
@@ -141,15 +141,7 @@ const CoursewareSearch = ({ ...sectionProps }) => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
key={status}
|
||||
className="courseware-search__results"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
aria-busy={status === 'loading'}
|
||||
data-testid="courseware-search-results"
|
||||
role={status === 'results' ? 'alert' : 'none'}
|
||||
>
|
||||
<div className="courseware-search__results" aria-live="polite" data-testid="courseware-search-results">
|
||||
{status === 'loading' ? (
|
||||
<div className="courseware-search__spinner" data-testid="courseware-search-spinner">
|
||||
<Spinner animation="border" variant="light" screenReaderText={formatMessage(messages.loading)} />
|
||||
@@ -165,6 +157,8 @@ const CoursewareSearch = ({ ...sectionProps }) => {
|
||||
{total > 0 ? (
|
||||
<div
|
||||
className="courseware-search__results-summary"
|
||||
aria-relevant="all"
|
||||
aria-atomic="true"
|
||||
data-testid="courseware-search-summary"
|
||||
>{formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })}
|
||||
</div>
|
||||
|
||||
@@ -244,7 +244,7 @@ describe('CoursewareSearch', () => {
|
||||
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show a wrapper div with proper aria attributes', () => {
|
||||
it('should show a summary for the results within a container with aria-live="polite"', () => {
|
||||
mockModels({
|
||||
searchKeyword: 'fubar',
|
||||
total: 1,
|
||||
@@ -253,9 +253,7 @@ describe('CoursewareSearch', () => {
|
||||
|
||||
const results = screen.queryByTestId('courseware-search-results');
|
||||
|
||||
expect(results).toHaveAttribute('aria-live', 'assertive');
|
||||
expect(results).toHaveAttribute('aria-atomic', 'true');
|
||||
expect(results).toHaveAttribute('role', 'alert');
|
||||
expect(results).toHaveAttribute('aria-live', 'polite');
|
||||
expect(within(results).queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const CoursewareSearchEmpty = ({ intl }) => (
|
||||
<div className="courseware-search-results">
|
||||
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
CoursewareSearchEmpty.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
const CoursewareSearchEmpty = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="courseware-search-results">
|
||||
<p className="courseware-search-results__empty" data-testid="no-results">{intl.formatMessage(messages.searchResultsNone)}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearchEmpty);
|
||||
export default CoursewareSearchEmpty;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
screen,
|
||||
} from '../../setupTest';
|
||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||
import messages from './messages';
|
||||
|
||||
function renderComponent() {
|
||||
const { container } = render(<CoursewareSearchEmpty />);
|
||||
@@ -16,9 +17,12 @@ describe('CoursewareSearchEmpty', () => {
|
||||
initializeMockApp();
|
||||
});
|
||||
|
||||
it('should match the snapshot', () => {
|
||||
it('render empty results text and corresponding classes', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByTestId('no-results')).toMatchSnapshot();
|
||||
const emptyText = screen.getByText(messages.searchResultsNone.defaultMessage);
|
||||
expect(emptyText).toBeInTheDocument();
|
||||
expect(emptyText).toHaveClass('courseware-search-results__empty');
|
||||
expect(emptyText).toHaveAttribute('data-testid', 'no-results');
|
||||
expect(emptyText.parentElement).toHaveClass('courseware-search-results');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import React from 'react';
|
||||
import {
|
||||
Folder, TextFields, VideoCamera, Article,
|
||||
} from '@openedx/paragon/icons';
|
||||
@@ -6,7 +6,6 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||
import messages from './messages';
|
||||
|
||||
const iconTypeMapping = {
|
||||
text: TextFields,
|
||||
@@ -22,8 +21,6 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
||||
return <CoursewareSearchEmpty />;
|
||||
}
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const baseUrl = `${getConfig().LMS_BASE_URL}`;
|
||||
|
||||
return (
|
||||
@@ -45,30 +42,24 @@ const CoursewareSearchResults = ({ results = [] }) => {
|
||||
rel: 'nofollow',
|
||||
} : { href: `${baseUrl}${url}` };
|
||||
|
||||
const ariaSeaparator = formatMessage(messages.searchResultsBreadcrumbSeparator);
|
||||
const ariaLocation = location?.length ? formatMessage(messages.searchResultsBreadcrumb, { path: location.join(ariaSeaparator) }) : '';
|
||||
|
||||
return (
|
||||
<a key={id} className="courseware-search-results__item" {...linkProps}>
|
||||
<div className="courseware-search-results__icon"><Icon src={icon} /></div>
|
||||
<div className="courseware-search-results__info">
|
||||
<div className="courseware-search-results__title">
|
||||
<h3>{title}</h3>
|
||||
{contentHits ? (<em aria-hidden="true">{contentHits}</em>) : null }
|
||||
<span>{title}</span>
|
||||
{contentHits ? (<em>{contentHits}</em>) : null }
|
||||
</div>
|
||||
|
||||
<div aria-label={ariaLocation}>
|
||||
{location?.length ? (
|
||||
<ul className="courseware-search-results__breadcrumbs" aria-hidden="true">
|
||||
{
|
||||
{location?.length ? (
|
||||
<ul className="courseware-search-results__breadcrumbs">
|
||||
{
|
||||
// This ignore is necessary because the breadcrumb texts might have duplicates.
|
||||
// The breadcrumbs are not expected to change.
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
location.map((breadcrumb, i) => (<li key={`${i}:${breadcrumb}`}><div>{breadcrumb}</div></li>))
|
||||
}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import CoursewareSearchResults from './CoursewareSearchResults';
|
||||
import messages from './messages';
|
||||
import searchResultsFactory from './test-data/search-results-factory';
|
||||
import * as mock from './test-data/mocked-response.json';
|
||||
|
||||
jest.mock('react-redux');
|
||||
|
||||
@@ -34,8 +35,53 @@ describe('CoursewareSearchResults', () => {
|
||||
renderComponent({ results });
|
||||
});
|
||||
|
||||
it('should match the snapshot', () => {
|
||||
expect(screen.getByTestId('search-results')).toMatchSnapshot();
|
||||
it('should render complete list', () => {
|
||||
const courses = screen.getAllByRole('link');
|
||||
expect(courses.length).toBe(mock.results.length);
|
||||
});
|
||||
|
||||
it('should render correct link for internal course', () => {
|
||||
const courses = screen.getAllByRole('link');
|
||||
const firstCourse = courses[0];
|
||||
const firstCourseTitle = firstCourse.querySelector('.courseware-search-results__title span');
|
||||
expect(firstCourseTitle.innerHTML).toEqual(mock.results[0].data.content.display_name);
|
||||
expect(firstCourse.href).toContain(mock.results[0].data.url);
|
||||
expect(firstCourse).not.toHaveAttribute('target', '_blank');
|
||||
expect(firstCourse).not.toHaveAttribute('rel', 'nofollow');
|
||||
});
|
||||
|
||||
it('should render correct link if is External url course', () => {
|
||||
const courses = screen.getAllByRole('link');
|
||||
const externalCourse = courses[courses.length - 1];
|
||||
const externalCourseTitle = externalCourse.querySelector('.courseware-search-results__title span');
|
||||
expect(externalCourseTitle.innerHTML).toEqual(mock.results[mock.results.length - 1].data.content.display_name);
|
||||
expect(externalCourse.href).toContain(mock.results[mock.results.length - 1].data.url);
|
||||
expect(externalCourse).toHaveAttribute('target', '_blank');
|
||||
expect(externalCourse).toHaveAttribute('rel', 'nofollow');
|
||||
const icon = externalCourse.querySelector('svg');
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render location breadcrumbs', () => {
|
||||
const breadcrumbs = screen.getAllByText(mock.results[0].data.location[0]);
|
||||
expect(breadcrumbs.length).toBeGreaterThan(0);
|
||||
const firstBreadcrumb = breadcrumbs[0].closest('li');
|
||||
expect(firstBreadcrumb).toBeInTheDocument();
|
||||
expect(firstBreadcrumb.querySelector('div').textContent).toBe(mock.results[0].data.location[0]);
|
||||
expect(firstBreadcrumb.nextSibling.querySelector('div').textContent).toBe(mock.results[0].data.location[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when results are provided with content hits', () => {
|
||||
beforeEach(() => {
|
||||
const { results } = searchResultsFactory('Passing');
|
||||
renderComponent({ results });
|
||||
});
|
||||
|
||||
it('should render content hits', () => {
|
||||
const contentHits = screen.getByText('1');
|
||||
expect(contentHits).toBeInTheDocument();
|
||||
expect(contentHits.tagName).toBe('EM');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { ManageSearch } from '@openedx/paragon/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -7,9 +7,8 @@ import messages from './messages';
|
||||
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
|
||||
import { setShowSearch } from '../data/slice';
|
||||
|
||||
const CoursewareSearchToggle = ({
|
||||
intl,
|
||||
}) => {
|
||||
const CoursewareSearchToggle = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const enabled = useCoursewareSearchFeatureFlag();
|
||||
const { query } = useCoursewareSearchParams();
|
||||
@@ -41,8 +40,4 @@ const CoursewareSearchToggle = ({
|
||||
);
|
||||
};
|
||||
|
||||
CoursewareSearchToggle.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoursewareSearchToggle);
|
||||
export default CoursewareSearchToggle;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoursewareSearchEmpty should match the snapshot 1`] = `
|
||||
<p
|
||||
class="courseware-search-results__empty"
|
||||
data-testid="no-results"
|
||||
>
|
||||
No results found.
|
||||
</p>
|
||||
`;
|
||||
@@ -1,1363 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = `
|
||||
<div
|
||||
class="courseware-search-results"
|
||||
data-testid="search-results"
|
||||
>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 4H2v16h20V6H12l-2-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Demo Course Overview
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Passing a Course
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
1
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10 4H2v16h20V6H12l-2-2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Passing a Course
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Passing a Course."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Passing a Course
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Text Input
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Text input."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Text input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Pointing on a Picture
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Pointing on a Picture."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Pointing on a Picture
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Getting Answers
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Getting Answers."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Getting Answers
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Welcome!
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
30
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Multiple Choice Questions
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Multiple Choice Questions."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Multiple Choice Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Numerical Input
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Homework - Question Styles, then Numerical Input."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Question Styles
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Numerical Input
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Connecting a Circuit and a Circuit Diagram
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Video Presentation Styles."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Video Presentation Styles
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
CAPA
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 2: Get Interactive, then Homework - Labs and Demos, then Code Grader."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 2: Get Interactive
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Labs and Demos
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Code Grader
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Interactive Questions
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then Interactive Questions."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Interactive Questions
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Blank HTML Page
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
6
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Introduction, then Demo Course Overview, then Introduction: Video and Sequences."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Introduction
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Demo Course Overview
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Introduction: Video and Sequences
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Discussion Forums
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
5
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Discussion Forums."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Discussion Forums
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Overall Grade
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
7
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then Overall Grade Performance."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Overall Grade Performance
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Blank HTML Page
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Homework - Find Your Study Buddy."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Find Your Study Buddy
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
3
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Homework - Find Your Study Buddy, then Homework - Find Your Study Buddy."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Homework - Find Your Study Buddy
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
Be Social
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
4
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 3: Be Social, then Lesson 3 - Be Social, then Be Social."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 3: Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 3 - Be Social
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Be Social
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
EdX Exams
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
4
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: About Exams and Certificates, then edX Exams, then EdX Exams."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
About Exams and Certificates
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
edX Exams
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
EdX Exams
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
When Are Your Exams?
|
||||
</h3>
|
||||
<em
|
||||
aria-hidden="true"
|
||||
>
|
||||
2
|
||||
</em>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Location: Example Week 1: Getting Started, then Lesson 1 - Getting Started, then When Are Your Exams? ."
|
||||
>
|
||||
<ul
|
||||
aria-hidden="true"
|
||||
class="courseware-search-results__breadcrumbs"
|
||||
>
|
||||
<li>
|
||||
<div>
|
||||
Example Week 1: Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Lesson 1 - Getting Started
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
When Are Your Exams?
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="courseware-search-results__item"
|
||||
href="https://www.edx.org"
|
||||
rel="nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__icon"
|
||||
>
|
||||
<span
|
||||
class="pgn__icon"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="24"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="courseware-search-results__info"
|
||||
>
|
||||
<div
|
||||
class="courseware-search-results__title"
|
||||
>
|
||||
<h3>
|
||||
External Course Link Test
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
aria-label=""
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,306 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
|
||||
{
|
||||
"filters": [
|
||||
{
|
||||
"count": 7,
|
||||
"key": "capa",
|
||||
"label": "CAPA",
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"key": "sequence",
|
||||
"label": "Sequence",
|
||||
},
|
||||
{
|
||||
"count": 9,
|
||||
"key": "text",
|
||||
"label": "Text",
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"key": "unknown",
|
||||
"label": "Unknown",
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"key": "video",
|
||||
"label": "Video",
|
||||
},
|
||||
],
|
||||
"maxScore": 3.4545178,
|
||||
"ms": 5,
|
||||
"results": [
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
],
|
||||
"score": 3.4545178,
|
||||
"title": "Demo Course Overview",
|
||||
"type": "sequence",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Passing a Course",
|
||||
],
|
||||
"score": 3.4545178,
|
||||
"title": "Passing a Course",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Passing a Course",
|
||||
],
|
||||
"score": 3.4545178,
|
||||
"title": "Passing a Course",
|
||||
"type": "sequence",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Text input",
|
||||
],
|
||||
"score": 1.5874016,
|
||||
"title": "Text Input",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Pointing on a Picture",
|
||||
],
|
||||
"score": 1.5499392,
|
||||
"title": "Pointing on a Picture",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Getting Answers",
|
||||
],
|
||||
"score": 1.5003732,
|
||||
"title": "Getting Answers",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
"Introduction: Video and Sequences",
|
||||
],
|
||||
"score": 1.4792063,
|
||||
"title": "Welcome!",
|
||||
"type": "video",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Multiple Choice Questions",
|
||||
],
|
||||
"score": 1.4341705,
|
||||
"title": "Multiple Choice Questions",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Numerical Input",
|
||||
],
|
||||
"score": 1.2987298,
|
||||
"title": "Numerical Input",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"Video Presentation Styles",
|
||||
],
|
||||
"score": 1.1870136,
|
||||
"title": "Connecting a Circuit and a Circuit Diagram",
|
||||
"type": "video",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||
"location": [
|
||||
"Example Week 2: Get Interactive",
|
||||
"Homework - Labs and Demos",
|
||||
"Code Grader",
|
||||
],
|
||||
"score": 1.0107487,
|
||||
"title": "CAPA",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"Interactive Questions",
|
||||
],
|
||||
"score": 0.96387196,
|
||||
"title": "Interactive Questions",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
"Introduction: Video and Sequences",
|
||||
],
|
||||
"score": 0.8844358,
|
||||
"title": "Blank HTML Page",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Discussion Forums",
|
||||
],
|
||||
"score": 0.8803684,
|
||||
"title": "Discussion Forums",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Overall Grade Performance",
|
||||
],
|
||||
"score": 0.87981963,
|
||||
"title": "Overall Grade",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Homework - Find Your Study Buddy",
|
||||
],
|
||||
"score": 0.84284115,
|
||||
"title": "Blank HTML Page",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Homework - Find Your Study Buddy",
|
||||
"Homework - Find Your Study Buddy",
|
||||
],
|
||||
"score": 0.84284115,
|
||||
"title": "Find Your Study Buddy",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Be Social",
|
||||
],
|
||||
"score": 0.84210813,
|
||||
"title": "Be Social",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"EdX Exams",
|
||||
],
|
||||
"score": 0.8306555,
|
||||
"title": "EdX Exams",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"When Are Your Exams? ",
|
||||
],
|
||||
"score": 0.82610154,
|
||||
"title": "When Are Your Exams? ",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "random-element-id",
|
||||
"location": null,
|
||||
"score": 0.82610154,
|
||||
"title": "External Course Link Test",
|
||||
"type": "unknown",
|
||||
"url": "https://www.edx.org",
|
||||
},
|
||||
],
|
||||
"total": 29,
|
||||
}
|
||||
`;
|
||||
@@ -9,8 +9,8 @@
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
border-top: 1px solid $light-300;
|
||||
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals.
|
||||
border-top: 1px solid var(--pgn-color-light-300);
|
||||
z-index: var(--pgn-elevation-modal-zindex); // Bootstrap's z-index layer for Modals.
|
||||
|
||||
&__form {
|
||||
position: relative;
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
&__results-summary {
|
||||
font-size: .9rem;
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
padding: 1rem 0 .5rem;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
margin-top: 1.5rem;
|
||||
|
||||
&__empty {
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
padding: 6rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -76,17 +76,17 @@
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: $light-300;
|
||||
background: var(--pgn-color-light-300);
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid $light-300;
|
||||
border-top: 1px solid var(--pgn-color-light-300);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
padding: 0.375rem 0 0 0.375rem;
|
||||
color: $gray-300;
|
||||
color: var(--pgn-color-gray-300);
|
||||
}
|
||||
|
||||
&__info {
|
||||
@@ -99,12 +99,7 @@
|
||||
align-items: center;
|
||||
line-height: 2.5;
|
||||
font-size: 0.875rem;
|
||||
color: $black;
|
||||
|
||||
> h3 {
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
color: var(--pgn-color-black);
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
@@ -118,7 +113,7 @@
|
||||
font-variant-numeric: lining-nums tabular-nums;
|
||||
min-width: 1.25rem;
|
||||
line-height: 1rem;
|
||||
background: $light-300;
|
||||
background: var(--pgn-color-light-300);
|
||||
border-radius: 99rem;
|
||||
font-style: normal;
|
||||
margin-left: 0.375rem;
|
||||
@@ -130,7 +125,7 @@
|
||||
&__breadcrumbs {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@@ -161,14 +156,14 @@
|
||||
}
|
||||
|
||||
.courseware-search-results-tabs {
|
||||
border-bottom-color: $gray-400 !important;
|
||||
border-bottom-color: var(--pgn-color-gray-400) !important;
|
||||
|
||||
&.nav-tabs .nav-link.active {
|
||||
border-bottom-width: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, 'md')) {
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
.courseware-search {
|
||||
&__close {
|
||||
right: -2.5rem;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderHook, act } from '@testing-library/react-hooks';
|
||||
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { fetchCoursewareSearchSettings } from '../data/thunks';
|
||||
@@ -38,13 +38,13 @@ describe('CoursewareSearch Hooks', () => {
|
||||
|
||||
it('should return true if feature is enabled', async () => {
|
||||
const hook = await renderTestHook();
|
||||
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
expect(hook.result.current).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if feature is disabled', async () => {
|
||||
const hook = await renderTestHook(false);
|
||||
await hook.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
await waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
|
||||
expect(hook.result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -125,7 +125,7 @@ describe('CoursewareSearch Hooks', () => {
|
||||
it('should return the element bounding box', async () => {
|
||||
const hook = await renderTestHook({ elementId: 'test', mockedInfo });
|
||||
|
||||
hook.waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
||||
await waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());
|
||||
|
||||
expect(hook.result.current).toEqual(mockedInfo);
|
||||
});
|
||||
|
||||
@@ -10,8 +10,8 @@ describe('mapSearchResponse', () => {
|
||||
response = mapSearchResponse(camelCaseObject(mockedResponse));
|
||||
});
|
||||
|
||||
it('should match snapshot', () => {
|
||||
expect(response).toMatchSnapshot();
|
||||
it('should match number of results', () => {
|
||||
expect(response.results.length).toBe(mockedResponse.results.length);
|
||||
});
|
||||
|
||||
it('should match expected filters', () => {
|
||||
@@ -24,6 +24,25 @@ describe('mapSearchResponse', () => {
|
||||
];
|
||||
expect(response.filters).toEqual(expectedFilters);
|
||||
});
|
||||
|
||||
it('should match expected results', () => {
|
||||
const mockFirstResult = mockedResponse.results[0];
|
||||
const expectedFirstResult = {
|
||||
id: mockFirstResult.data.id,
|
||||
title: mockFirstResult.data.content.display_name,
|
||||
type: mockFirstResult.data.content_type.toLowerCase(),
|
||||
location: mockFirstResult.data.location,
|
||||
url: mockFirstResult.data.url,
|
||||
contentHits: 0,
|
||||
score: mockFirstResult.score,
|
||||
};
|
||||
expect(response.results[0]).toEqual(expectedFirstResult);
|
||||
});
|
||||
|
||||
it('should match expected ms and max score', () => {
|
||||
expect(response.maxScore).toBe(mockedResponse.max_score);
|
||||
expect(response.ms).toBe(mockedResponse.took);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the a keyword is provided', () => {
|
||||
|
||||
@@ -56,21 +56,7 @@ const messages = defineMessages({
|
||||
defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.',
|
||||
description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.',
|
||||
},
|
||||
searchResultsFilterDescription: {
|
||||
id: 'learn.coursewareSearch.searchResultsFilterDescription',
|
||||
defaultMessage: 'Search result filters',
|
||||
description: 'Screen Reader text to describe the filter options.',
|
||||
},
|
||||
searchResultsBreadcrumb: {
|
||||
id: 'learn.coursewareSearch.searchResultsBreadcrumb',
|
||||
defaultMessage: 'Location: {path}.',
|
||||
description: 'Screen Reader text to describe the search result breadcrumbs.',
|
||||
},
|
||||
searchResultsBreadcrumbSeparator: {
|
||||
id: 'learn.searchResultsBreadcrumbSeparator',
|
||||
defaultMessage: ', then ',
|
||||
description: 'Screen Reader text to connect breadcrumb sections. i.e.: "Introduction, then Register, then Something else.',
|
||||
},
|
||||
|
||||
// These are translations for labeling the filters
|
||||
'filter:all': {
|
||||
id: 'learn.coursewareSearch.filter:all',
|
||||
|
||||
@@ -31,7 +31,6 @@ Factory.define('outlineTabData')
|
||||
course_access_redirect: false,
|
||||
has_scheduled_content: null,
|
||||
access_expiration: null,
|
||||
can_show_upgrade_sock: false,
|
||||
cert_data: {
|
||||
cert_status: null,
|
||||
cert_web_view_url: null,
|
||||
|
||||
@@ -17,7 +17,21 @@ Factory.define('progressTabData')
|
||||
percent: 1,
|
||||
is_passing: true,
|
||||
},
|
||||
final_grades: 0.5,
|
||||
credit_course_requirements: null,
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Homework',
|
||||
short_label: 'HW',
|
||||
weight: 1,
|
||||
average_grade: 1,
|
||||
weighted_grade: 1,
|
||||
num_droppable: 1,
|
||||
num_total: 2,
|
||||
has_hidden_contribution: 'none',
|
||||
last_grade_publish_date: null,
|
||||
},
|
||||
],
|
||||
section_scores: [
|
||||
{
|
||||
display_name: 'First section',
|
||||
|
||||
@@ -1,942 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"proctoringPanelStatus": "loading",
|
||||
"showSearch": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
"hasAccess": true,
|
||||
"userFragment": null,
|
||||
"userMessage": null,
|
||||
},
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"isEnrolled": false,
|
||||
"isMasquerading": false,
|
||||
"isNewDiscussionSidebarViewEnabled": false,
|
||||
"isSelfPaced": false,
|
||||
"isStaff": false,
|
||||
"number": "DemoX",
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "8CF08E5",
|
||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"dates": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"courseDateBlocks": [
|
||||
{
|
||||
"date": "2020-05-01T17:59:41Z",
|
||||
"dateType": "course-start-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "",
|
||||
"title": "Course Starts",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-04T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Multi Badges Completed",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-05T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Multi Badges Past Due",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-27T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Past Due 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-27T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Past Due 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-28T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Completed/Due 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-28T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Completed/Due 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-29T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Completed 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-29T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Completed 2",
|
||||
},
|
||||
{
|
||||
"date": "2020-06-16T17:59:40.942669Z",
|
||||
"dateType": "verified-upgrade-deadline",
|
||||
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Upgrade to Verified Certificate",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Verified 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Verified 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": "ORA Dates are set by the instructor, and can't be changed",
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "ORA Verified 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-18T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Verified 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-18T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Verified 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-19T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"learnerHasAccess": true,
|
||||
"title": "One Unreleased 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-19T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Unreleased 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-20T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Both Unreleased 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-20T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Both Unreleased 2",
|
||||
},
|
||||
{
|
||||
"date": "2030-08-23T00:00:00Z",
|
||||
"dateType": "course-end-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "",
|
||||
"title": "Course Ends",
|
||||
},
|
||||
{
|
||||
"date": "2030-09-01T00:00:00Z",
|
||||
"dateType": "verification-deadline-date",
|
||||
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "Verification Deadline",
|
||||
},
|
||||
],
|
||||
"datesBannerInfo": {
|
||||
"contentTypeGatingEnabled": false,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
"hasEnded": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"learnerIsFullAccess": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": {
|
||||
"activeAttempt": null,
|
||||
"allowProctoringOptOut": false,
|
||||
"apiErrorMsg": "",
|
||||
"exam": {
|
||||
"attempt": {
|
||||
"attempt_code": "",
|
||||
"attempt_id": null,
|
||||
"attempt_status": "",
|
||||
"course_id": "",
|
||||
"desktop_application_js_url": "",
|
||||
"exam_display_name": "",
|
||||
"exam_started_poll_url": "",
|
||||
"exam_type": "",
|
||||
"exam_url_path": "",
|
||||
"external_id": "",
|
||||
"in_timed_exam": true,
|
||||
"ping_interval": null,
|
||||
"taking_as_proctored": true,
|
||||
"time_remaining_seconds": null,
|
||||
"use_legacy_attempt_api": true,
|
||||
},
|
||||
"backend": "",
|
||||
"content_id": "",
|
||||
"course_id": "",
|
||||
"due_date": null,
|
||||
"exam_name": "",
|
||||
"external_id": "",
|
||||
"hide_after_due": false,
|
||||
"id": null,
|
||||
"is_active": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored": false,
|
||||
"prerequisite_status": {
|
||||
"are_prerequisites_satisifed": true,
|
||||
"declined_prerequisites": [],
|
||||
"failed_prerequisites": [],
|
||||
"pending_prerequisites": [],
|
||||
"satisfied_prerequisites": [],
|
||||
},
|
||||
"time_limit_mins": null,
|
||||
"type": "",
|
||||
},
|
||||
"examAccessToken": {
|
||||
"exam_access_token": "",
|
||||
"exam_access_token_expiration": "",
|
||||
},
|
||||
"isLoading": true,
|
||||
"proctoringSettings": {
|
||||
"exam_proctoring_backend": {
|
||||
"download_url": "",
|
||||
"instructions": [],
|
||||
"name": "",
|
||||
"rules": {},
|
||||
},
|
||||
"integration_specific_email": "",
|
||||
"learner_notification_from_email": "",
|
||||
"provider_name": "",
|
||||
"provider_tech_support_email": "",
|
||||
"provider_tech_support_phone": "",
|
||||
"provider_tech_support_url": "",
|
||||
},
|
||||
"timeIsOver": false,
|
||||
},
|
||||
"tours": {
|
||||
"showCoursewareTour": false,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
"showNewUserCourseHomeTour": false,
|
||||
"toursEnabled": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"proctoringPanelStatus": "loading",
|
||||
"showSearch": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
"hasAccess": true,
|
||||
"userFragment": null,
|
||||
"userMessage": null,
|
||||
},
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"isEnrolled": false,
|
||||
"isMasquerading": false,
|
||||
"isNewDiscussionSidebarViewEnabled": false,
|
||||
"isSelfPaced": false,
|
||||
"isStaff": false,
|
||||
"number": "DemoX",
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "8CF08E5",
|
||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"outline": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"accessExpiration": null,
|
||||
"canShowUpgradeSock": false,
|
||||
"certData": {
|
||||
"certStatus": null,
|
||||
"certWebViewUrl": null,
|
||||
"certificateAvailableDate": null,
|
||||
},
|
||||
"courseBlocks": {
|
||||
"courses": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": {
|
||||
"hasScheduledContent": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"sectionIds": [
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
],
|
||||
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
|
||||
},
|
||||
},
|
||||
"sections": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": {
|
||||
"complete": false,
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"hideFromTOC": undefined,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
"resumeBlock": false,
|
||||
"sequenceIds": [
|
||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||
],
|
||||
"title": "Title of Section",
|
||||
},
|
||||
},
|
||||
"sequences": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": {
|
||||
"complete": false,
|
||||
"description": null,
|
||||
"due": null,
|
||||
"effortActivities": 2,
|
||||
"effortTime": 15,
|
||||
"hideFromTOC": undefined,
|
||||
"icon": null,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||
"navigationDisabled": undefined,
|
||||
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
"showLink": true,
|
||||
"title": "Title of Sequence",
|
||||
},
|
||||
},
|
||||
},
|
||||
"courseGoals": {
|
||||
"daysPerWeek": null,
|
||||
"goalOptions": [],
|
||||
"selectedGoal": null,
|
||||
"subscribedToReminders": null,
|
||||
"weeklyLearningGoalEnabled": false,
|
||||
},
|
||||
"courseTools": [
|
||||
{
|
||||
"analyticsId": "edx.bookmarks",
|
||||
"title": "Bookmarks",
|
||||
"url": "https://example.com/bookmarks",
|
||||
},
|
||||
],
|
||||
"datesBannerInfo": {
|
||||
"contentTypeGatingEnabled": false,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
},
|
||||
"datesWidget": {
|
||||
"courseDateBlocks": [],
|
||||
},
|
||||
"enableProctoredExams": undefined,
|
||||
"enrollAlert": {
|
||||
"canEnroll": true,
|
||||
"extraText": "Contact the administrator.",
|
||||
},
|
||||
"enrollmentMode": undefined,
|
||||
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
||||
"hasEnded": undefined,
|
||||
"hasScheduledContent": null,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"offer": null,
|
||||
"resumeCourse": {
|
||||
"hasVisitedCourse": false,
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||
},
|
||||
"timeOffsetMillis": 0,
|
||||
"userHasPassingGrade": undefined,
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": "2050-01-01T12:00:00",
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "ABCD1234",
|
||||
"upgradeUrl": "http://localhost:18000/dashboard",
|
||||
},
|
||||
"welcomeMessageHtml": "<p>Welcome to this course!</p>",
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": {
|
||||
"activeAttempt": null,
|
||||
"allowProctoringOptOut": false,
|
||||
"apiErrorMsg": "",
|
||||
"exam": {
|
||||
"attempt": {
|
||||
"attempt_code": "",
|
||||
"attempt_id": null,
|
||||
"attempt_status": "",
|
||||
"course_id": "",
|
||||
"desktop_application_js_url": "",
|
||||
"exam_display_name": "",
|
||||
"exam_started_poll_url": "",
|
||||
"exam_type": "",
|
||||
"exam_url_path": "",
|
||||
"external_id": "",
|
||||
"in_timed_exam": true,
|
||||
"ping_interval": null,
|
||||
"taking_as_proctored": true,
|
||||
"time_remaining_seconds": null,
|
||||
"use_legacy_attempt_api": true,
|
||||
},
|
||||
"backend": "",
|
||||
"content_id": "",
|
||||
"course_id": "",
|
||||
"due_date": null,
|
||||
"exam_name": "",
|
||||
"external_id": "",
|
||||
"hide_after_due": false,
|
||||
"id": null,
|
||||
"is_active": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored": false,
|
||||
"prerequisite_status": {
|
||||
"are_prerequisites_satisifed": true,
|
||||
"declined_prerequisites": [],
|
||||
"failed_prerequisites": [],
|
||||
"pending_prerequisites": [],
|
||||
"satisfied_prerequisites": [],
|
||||
},
|
||||
"time_limit_mins": null,
|
||||
"type": "",
|
||||
},
|
||||
"examAccessToken": {
|
||||
"exam_access_token": "",
|
||||
"exam_access_token_expiration": "",
|
||||
},
|
||||
"isLoading": true,
|
||||
"proctoringSettings": {
|
||||
"exam_proctoring_backend": {
|
||||
"download_url": "",
|
||||
"instructions": [],
|
||||
"name": "",
|
||||
"rules": {},
|
||||
},
|
||||
"integration_specific_email": "",
|
||||
"learner_notification_from_email": "",
|
||||
"provider_name": "",
|
||||
"provider_tech_support_email": "",
|
||||
"provider_tech_support_phone": "",
|
||||
"provider_tech_support_url": "",
|
||||
},
|
||||
"timeIsOver": false,
|
||||
},
|
||||
"tours": {
|
||||
"showCoursewareTour": false,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
"showNewUserCourseHomeTour": false,
|
||||
"toursEnabled": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"proctoringPanelStatus": "loading",
|
||||
"showSearch": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
"hasAccess": true,
|
||||
"userFragment": null,
|
||||
"userMessage": null,
|
||||
},
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"isEnrolled": false,
|
||||
"isMasquerading": false,
|
||||
"isNewDiscussionSidebarViewEnabled": false,
|
||||
"isSelfPaced": false,
|
||||
"isStaff": false,
|
||||
"number": "DemoX",
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "8CF08E5",
|
||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"progress": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"accessExpiration": null,
|
||||
"certificateData": {},
|
||||
"completionSummary": {
|
||||
"completeCount": 1,
|
||||
"incompleteCount": 1,
|
||||
"lockedCount": 0,
|
||||
},
|
||||
"courseGrade": {
|
||||
"isPassing": true,
|
||||
"letterGrade": "pass",
|
||||
"percent": 1,
|
||||
},
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"creditCourseRequirements": null,
|
||||
"end": "3027-03-31T00:00:00Z",
|
||||
"enrollmentMode": "audit",
|
||||
"gradesFeatureIsFullyLocked": false,
|
||||
"gradesFeatureIsPartiallyLocked": false,
|
||||
"gradingPolicy": {
|
||||
"assignmentPolicies": [
|
||||
{
|
||||
"averageGrade": "1.0000",
|
||||
"numDroppable": 1,
|
||||
"shortLabel": "HW",
|
||||
"type": "Homework",
|
||||
"weight": 1,
|
||||
"weightedGrade": 1,
|
||||
},
|
||||
],
|
||||
"gradeRange": {
|
||||
"pass": 0.75,
|
||||
},
|
||||
},
|
||||
"hasScheduledContent": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"sectionScores": [
|
||||
{
|
||||
"displayName": "First section",
|
||||
"subsections": [
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
|
||||
"displayName": "First subsection",
|
||||
"hasGradedAssignment": true,
|
||||
"learnerHasAccess": true,
|
||||
"numPointsEarned": 0,
|
||||
"numPointsPossible": 3,
|
||||
"percentGraded": 0,
|
||||
"problemScores": [
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
],
|
||||
"showCorrectness": "always",
|
||||
"showGrades": true,
|
||||
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"displayName": "Second section",
|
||||
"subsections": [
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"displayName": "Second subsection",
|
||||
"hasGradedAssignment": true,
|
||||
"numPointsEarned": 1,
|
||||
"numPointsPossible": 1,
|
||||
"percentGraded": 1,
|
||||
"problemScores": [
|
||||
{
|
||||
"earned": 1,
|
||||
"possible": 1,
|
||||
},
|
||||
],
|
||||
"showCorrectness": "always",
|
||||
"showGrades": true,
|
||||
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
|
||||
"userHasPassingGrade": false,
|
||||
"verificationData": {
|
||||
"link": null,
|
||||
"status": "none",
|
||||
"statusDate": null,
|
||||
},
|
||||
"verifiedMode": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": {
|
||||
"activeAttempt": null,
|
||||
"allowProctoringOptOut": false,
|
||||
"apiErrorMsg": "",
|
||||
"exam": {
|
||||
"attempt": {
|
||||
"attempt_code": "",
|
||||
"attempt_id": null,
|
||||
"attempt_status": "",
|
||||
"course_id": "",
|
||||
"desktop_application_js_url": "",
|
||||
"exam_display_name": "",
|
||||
"exam_started_poll_url": "",
|
||||
"exam_type": "",
|
||||
"exam_url_path": "",
|
||||
"external_id": "",
|
||||
"in_timed_exam": true,
|
||||
"ping_interval": null,
|
||||
"taking_as_proctored": true,
|
||||
"time_remaining_seconds": null,
|
||||
"use_legacy_attempt_api": true,
|
||||
},
|
||||
"backend": "",
|
||||
"content_id": "",
|
||||
"course_id": "",
|
||||
"due_date": null,
|
||||
"exam_name": "",
|
||||
"external_id": "",
|
||||
"hide_after_due": false,
|
||||
"id": null,
|
||||
"is_active": true,
|
||||
"is_practice_exam": false,
|
||||
"is_proctored": false,
|
||||
"prerequisite_status": {
|
||||
"are_prerequisites_satisifed": true,
|
||||
"declined_prerequisites": [],
|
||||
"failed_prerequisites": [],
|
||||
"pending_prerequisites": [],
|
||||
"satisfied_prerequisites": [],
|
||||
},
|
||||
"time_limit_mins": null,
|
||||
"type": "",
|
||||
},
|
||||
"examAccessToken": {
|
||||
"exam_access_token": "",
|
||||
"exam_access_token_expiration": "",
|
||||
},
|
||||
"isLoading": true,
|
||||
"proctoringSettings": {
|
||||
"exam_proctoring_backend": {
|
||||
"download_url": "",
|
||||
"instructions": [],
|
||||
"name": "",
|
||||
"rules": {},
|
||||
},
|
||||
"integration_specific_email": "",
|
||||
"learner_notification_from_email": "",
|
||||
"provider_name": "",
|
||||
"provider_tech_support_email": "",
|
||||
"provider_tech_support_phone": "",
|
||||
"provider_tech_support_url": "",
|
||||
},
|
||||
"timeIsOver": false,
|
||||
},
|
||||
"tours": {
|
||||
"showCoursewareTour": false,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
"showNewUserCourseHomeTour": false,
|
||||
"toursEnabled": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -3,93 +3,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logInfo } from '@edx/frontend-platform/logging';
|
||||
import { appendBrowserTimezoneToUrl } from '../../utils';
|
||||
|
||||
const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => {
|
||||
let dropCount = numDroppable;
|
||||
// Drop the lowest grades
|
||||
while (dropCount && points.length >= dropCount) {
|
||||
const lowestScore = Math.min(...points);
|
||||
const lowestScoreIndex = points.indexOf(lowestScore);
|
||||
points.splice(lowestScoreIndex, 1);
|
||||
dropCount--;
|
||||
}
|
||||
let averageGrade = 0;
|
||||
let weightedGrade = 0;
|
||||
if (points.length) {
|
||||
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
|
||||
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
|
||||
// exists in edx-platform.
|
||||
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4);
|
||||
weightedGrade = averageGrade * assignmentWeight;
|
||||
}
|
||||
return { averageGrade, weightedGrade };
|
||||
};
|
||||
|
||||
function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
|
||||
const gradeByAssignmentType = {};
|
||||
assignmentPolicies.forEach(assignment => {
|
||||
// Create an array with the number of total assignments and set the scores to 0
|
||||
// as placeholders for assignments that have not yet been released
|
||||
gradeByAssignmentType[assignment.type] = {
|
||||
grades: Array(assignment.numTotal).fill(0),
|
||||
numAssignmentsCreated: 0,
|
||||
numTotalExpectedAssignments: assignment.numTotal,
|
||||
};
|
||||
});
|
||||
|
||||
sectionScores.forEach((chapter) => {
|
||||
chapter.subsections.forEach((subsection) => {
|
||||
if (!(subsection.hasGradedAssignment && subsection.showGrades && subsection.numPointsPossible)) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
assignmentType,
|
||||
numPointsEarned,
|
||||
numPointsPossible,
|
||||
} = subsection;
|
||||
|
||||
// If a subsection's assignment type does not match an assignment policy in Studio,
|
||||
// we won't be able to include it in this accumulation of grades by assignment type.
|
||||
// This may happen if a course author has removed/renamed an assignment policy in Studio and
|
||||
// neglected to update the subsection's of that assignment type
|
||||
if (!gradeByAssignmentType[assignmentType]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
numAssignmentsCreated,
|
||||
} = gradeByAssignmentType[assignmentType];
|
||||
|
||||
numAssignmentsCreated++;
|
||||
if (numAssignmentsCreated <= gradeByAssignmentType[assignmentType].numTotalExpectedAssignments) {
|
||||
// Remove a placeholder grade so long as the number of recorded created assignments is less than the number
|
||||
// of expected assignments
|
||||
gradeByAssignmentType[assignmentType].grades.shift();
|
||||
}
|
||||
// Add the graded assignment to the list
|
||||
gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0);
|
||||
// Record the created assignment
|
||||
gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated;
|
||||
});
|
||||
});
|
||||
|
||||
return assignmentPolicies.map((assignment) => {
|
||||
const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades(
|
||||
gradeByAssignmentType[assignment.type].grades,
|
||||
assignment.weight,
|
||||
assignment.numDroppable,
|
||||
);
|
||||
|
||||
return {
|
||||
averageGrade,
|
||||
numDroppable: assignment.numDroppable,
|
||||
shortLabel: assignment.shortLabel,
|
||||
type: assignment.type,
|
||||
weight: assignment.weight,
|
||||
weightedGrade,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweak the metadata for consistency
|
||||
* @param metadata the data to normalize
|
||||
@@ -236,11 +149,6 @@ export async function getProgressTabData(courseId, targetUserId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
const camelCasedData = camelCaseObject(data);
|
||||
|
||||
camelCasedData.gradingPolicy.assignmentPolicies = normalizeAssignmentPolicies(
|
||||
camelCasedData.gradingPolicy.assignmentPolicies,
|
||||
camelCasedData.sectionScores,
|
||||
);
|
||||
|
||||
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
||||
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
||||
// in order to preserve a course team's desired grade formatting.
|
||||
@@ -367,7 +275,6 @@ export async function getOutlineTabData(courseId) {
|
||||
} = tabData;
|
||||
|
||||
const accessExpiration = camelCaseObject(data.access_expiration);
|
||||
const canShowUpgradeSock = data.can_show_upgrade_sock;
|
||||
const certData = camelCaseObject(data.cert_data);
|
||||
const courseBlocks = data.course_blocks ? normalizeOutlineBlocks(courseId, data.course_blocks.blocks) : {};
|
||||
const courseGoals = camelCaseObject(data.course_goals);
|
||||
@@ -389,7 +296,6 @@ export async function getOutlineTabData(courseId) {
|
||||
|
||||
return {
|
||||
accessExpiration,
|
||||
canShowUpgradeSock,
|
||||
certData,
|
||||
courseBlocks,
|
||||
courseGoals,
|
||||
|
||||
@@ -46,7 +46,6 @@ describe('Course Home Service', () => {
|
||||
willRespondWith: {
|
||||
status: 200,
|
||||
body: {
|
||||
can_show_upgrade_sock: boolean(false),
|
||||
verified_mode: like({
|
||||
access_expiration_date: null,
|
||||
currency: 'USD',
|
||||
@@ -94,7 +93,6 @@ describe('Course Home Service', () => {
|
||||
},
|
||||
});
|
||||
const normalizedTabData = {
|
||||
canShowUpgradeSock: false,
|
||||
verifiedMode: {
|
||||
accessExpirationDate: null,
|
||||
currency: 'USD',
|
||||
|
||||
@@ -90,14 +90,14 @@ describe('Data layer integration tests', () => {
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||
expect(state).toMatchSnapshot({
|
||||
expect(state).toEqual(expect.objectContaining({
|
||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||
// to keep track of conversations. This UUID is generated on each run.
|
||||
// Instead, we use an asymmetric matcher here.
|
||||
learningAssistant: expect.objectContaining({
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([401, 403, 404])(
|
||||
@@ -137,14 +137,14 @@ describe('Data layer integration tests', () => {
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||
expect(state).toMatchSnapshot({
|
||||
expect(state).toEqual(expect.objectContaining({
|
||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||
// to keep track of conversations. This UUID is generated on each run.
|
||||
// Instead, we use an asymmetric matcher here.
|
||||
learningAssistant: expect.objectContaining({
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([401, 403, 404])(
|
||||
@@ -185,14 +185,14 @@ describe('Data layer integration tests', () => {
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||
expect(state).toMatchSnapshot({
|
||||
expect(state).toEqual(expect.objectContaining({
|
||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||
// to keep track of conversations. This UUID is generated on each run.
|
||||
// Instead, we use an asymmetric matcher here.
|
||||
learningAssistant: expect.objectContaining({
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Should handle the url including a targetUserId', async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import Timeline from './timeline/Timeline';
|
||||
@@ -14,7 +14,8 @@ import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||
import UpgradeToCompleteAlert from '../suggested-schedule-messaging/UpgradeToCompleteAlert';
|
||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||
|
||||
const DatesTab = ({ intl }) => {
|
||||
const DatesTab = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -59,8 +60,4 @@ const DatesTab = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
DatesTab.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DatesTab);
|
||||
export default DatesTab;
|
||||
|
||||
@@ -5,8 +5,7 @@ import { useSelector } from 'react-redux';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedTime,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Tooltip, OverlayTrigger } from '@openedx/paragon';
|
||||
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
@@ -20,10 +19,10 @@ import { isLearnerAssignment } from '../utils';
|
||||
const Day = ({
|
||||
date,
|
||||
first,
|
||||
intl,
|
||||
items,
|
||||
last,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -108,7 +107,6 @@ const Day = ({
|
||||
Day.propTypes = {
|
||||
date: PropTypes.objectOf(Date).isRequired,
|
||||
first: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
date: PropTypes.string,
|
||||
dateType: PropTypes.string,
|
||||
@@ -126,4 +124,4 @@ Day.defaultProps = {
|
||||
last: false,
|
||||
};
|
||||
|
||||
export default injectIntl(Day);
|
||||
export default Day;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams, generatePath, useNavigate } from 'react-router-dom';
|
||||
@@ -30,6 +29,4 @@ const DiscussionTab = () => {
|
||||
);
|
||||
};
|
||||
|
||||
DiscussionTab.propTypes = {};
|
||||
|
||||
export default injectIntl(DiscussionTab);
|
||||
export default DiscussionTab;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import HeaderSlot from '../../plugin-slots/HeaderSlot';
|
||||
import PageLoading from '../../generic/PageLoading';
|
||||
@@ -10,7 +10,8 @@ import { unsubscribeFromCourseGoal } from '../data/api';
|
||||
import messages from './messages';
|
||||
import ResultPage from './ResultPage';
|
||||
|
||||
const GoalUnsubscribe = ({ intl }) => {
|
||||
const GoalUnsubscribe = () => {
|
||||
const intl = useIntl();
|
||||
const { token } = useParams();
|
||||
const [error, setError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -51,8 +52,4 @@ const GoalUnsubscribe = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
GoalUnsubscribe.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GoalUnsubscribe);
|
||||
export default GoalUnsubscribe;
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';
|
||||
|
||||
const ResultPage = ({ courseTitle, error, intl }) => {
|
||||
const errorDescription = (
|
||||
<FormattedMessage
|
||||
id="learning.goals.unsubscribe.errorDescription"
|
||||
defaultMessage="We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help."
|
||||
values={{
|
||||
contactSupport: (
|
||||
<Hyperlink
|
||||
className="text-reset"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().CONTACT_URL}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contactSupport)}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
const ResultPage = ({ courseTitle, error }) => {
|
||||
const intl = useIntl();
|
||||
const errorDescription = intl.formatMessage(
|
||||
messages.errorDescription,
|
||||
{
|
||||
contactSupport: (
|
||||
<Hyperlink
|
||||
className="text-reset"
|
||||
style={{ textDecoration: 'underline' }}
|
||||
destination={`${getConfig().CONTACT_URL}`}
|
||||
>
|
||||
{intl.formatMessage(messages.contactSupport)}
|
||||
</Hyperlink>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const header = error
|
||||
@@ -54,7 +52,6 @@ ResultPage.defaultProps = {
|
||||
ResultPage.propTypes = {
|
||||
courseTitle: PropTypes.string,
|
||||
error: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ResultPage);
|
||||
export default ResultPage;
|
||||
|
||||
@@ -16,6 +16,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Something went wrong',
|
||||
description: 'It indicate that the unsubscribing request has failed',
|
||||
},
|
||||
errorDescription: {
|
||||
id: 'learning.goals.unsubscribe.errorDescription',
|
||||
defaultMessage: 'We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.',
|
||||
description: 'Message that notifies user that unsubscribing failed and to try again',
|
||||
},
|
||||
goToDashboard: {
|
||||
id: 'learning.goals.unsubscribe.goToDashboard',
|
||||
defaultMessage: 'Go to dashboard',
|
||||
|
||||
@@ -65,6 +65,7 @@ const DateSummary = ({
|
||||
)}
|
||||
{!linkedTitle && dateBlock.link && (
|
||||
<a
|
||||
id={dateBlock.dateType === 'verified-upgrade-deadline' ? 'date-verified-upgrade-deadline' : ''}
|
||||
href={dateBlock.link}
|
||||
onClick={dateBlock.dateType === 'verified-upgrade-deadline' ? logVerifiedUpgradeClick : () => {}}
|
||||
className="description-link"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { CourseOutlineTabNotificationsSlot } from '../../plugin-slots/CourseOutlineTabNotificationsSlot';
|
||||
import { AlertList } from '../../generic/user-messages';
|
||||
|
||||
import CourseDates from './widgets/CourseDates';
|
||||
@@ -16,7 +16,6 @@ import CourseTools from './widgets/CourseTools';
|
||||
import { fetchOutlineTab } from '../data';
|
||||
import messages from './messages';
|
||||
import ShiftDatesAlert from '../suggested-schedule-messaging/ShiftDatesAlert';
|
||||
import UpgradeNotification from '../../generic/upgrade-notification/UpgradeNotification';
|
||||
import UpgradeToShiftDatesAlert from '../suggested-schedule-messaging/UpgradeToShiftDatesAlert';
|
||||
import useCertificateAvailableAlert from './alerts/certificate-status-alert';
|
||||
import useCourseEndAlert from './alerts/course-end-alert';
|
||||
@@ -40,13 +39,11 @@ const OutlineTab = () => {
|
||||
isSelfPaced,
|
||||
org,
|
||||
title,
|
||||
userTimezone,
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
|
||||
const expandButtonRef = useRef();
|
||||
|
||||
const {
|
||||
accessExpiration,
|
||||
courseBlocks: {
|
||||
courses,
|
||||
sections,
|
||||
@@ -55,20 +52,12 @@ const OutlineTab = () => {
|
||||
selectedGoal,
|
||||
weeklyLearningGoalEnabled,
|
||||
} = {},
|
||||
datesBannerInfo,
|
||||
datesWidget: {
|
||||
courseDateBlocks,
|
||||
},
|
||||
enableProctoredExams,
|
||||
offer,
|
||||
timeOffsetMillis,
|
||||
verifiedMode,
|
||||
} = useModel('outline', courseId);
|
||||
|
||||
const {
|
||||
marketingUrl,
|
||||
} = useModel('coursewareMeta', courseId);
|
||||
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -192,27 +181,7 @@ const OutlineTab = () => {
|
||||
/>
|
||||
)}
|
||||
<CourseTools />
|
||||
<PluginSlot
|
||||
id="outline_tab_notifications_slot"
|
||||
pluginProps={{
|
||||
courseId,
|
||||
model: 'outline',
|
||||
}}
|
||||
>
|
||||
<UpgradeNotification
|
||||
offer={offer}
|
||||
verifiedMode={verifiedMode}
|
||||
accessExpiration={accessExpiration}
|
||||
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
|
||||
marketingUrl={marketingUrl}
|
||||
upsellPageName="course_home"
|
||||
userTimezone={userTimezone}
|
||||
shouldDisplayBorder
|
||||
timeOffsetMillis={timeOffsetMillis}
|
||||
courseId={courseId}
|
||||
org={org}
|
||||
/>
|
||||
</PluginSlot>
|
||||
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
||||
<CourseDates />
|
||||
<CourseHandouts />
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Factory } from 'rosie';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Cookies from 'js-cookie';
|
||||
@@ -139,7 +139,7 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
await fetchAndRender();
|
||||
|
||||
expect(screen.getByTestId('outline_tab_notifications_slot')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('org.openedx.frontend.learning.course_outline_tab_notifications.v1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles expand/collapse all button click', async () => {
|
||||
@@ -1190,80 +1190,6 @@ describe('Outline Tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upgrade Card', () => {
|
||||
it('renders title when upgrade is available', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByRole('heading', { name: 'Pursue a verified certificate' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays link to upgrade', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('viewing upgrade card sends analytics', async () => {
|
||||
sendTrackEvent.mockClear();
|
||||
sendTrackingLogEvent.mockClear();
|
||||
await fetchAndRender();
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith('Promotion Viewed', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
creative: 'sidebarupsell',
|
||||
name: 'In-Course Verification Prompt',
|
||||
position: 'sidebar-message',
|
||||
promotion_id: 'courseware_verified_certificate_upsell',
|
||||
});
|
||||
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.bi.course.upgrade.sidebarupsell.displayed', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
});
|
||||
});
|
||||
|
||||
it('clicking upgrade link sends analytics', async () => {
|
||||
await fetchAndRender();
|
||||
|
||||
// Clearing after render to remove any events sent on view (ex. 'Promotion Viewed')
|
||||
sendTrackEvent.mockClear();
|
||||
sendTrackingLogEvent.mockClear();
|
||||
const upgradeButton = screen.getByRole('link', { name: 'Upgrade for $149' });
|
||||
|
||||
fireEvent.click(upgradeButton);
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(1, 'Promotion Clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
creative: 'sidebarupsell',
|
||||
name: 'In-Course Verification Prompt',
|
||||
position: 'sidebar-message',
|
||||
promotion_id: 'courseware_verified_certificate_upsell',
|
||||
});
|
||||
expect(sendTrackEvent).toHaveBeenNthCalledWith(2, 'edx.bi.ecommerce.upsell_links_clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
linkCategory: 'green_upgrade',
|
||||
linkName: 'course_home_green',
|
||||
linkType: 'button',
|
||||
pageName: 'course_home',
|
||||
});
|
||||
|
||||
expect(sendTrackingLogEvent).toHaveBeenCalledTimes(2);
|
||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(1, 'edx.bi.course.upgrade.sidebarupsell.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
});
|
||||
expect(sendTrackingLogEvent).toHaveBeenNthCalledWith(2, 'edx.course.enrollment.upgrade.clicked', {
|
||||
org_key: 'edX',
|
||||
courserun_key: courseId,
|
||||
location: 'sidebar-message',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Account Activation Alert', () => {
|
||||
beforeEach(() => {
|
||||
const intersectionObserverMock = () => ({
|
||||
|
||||
@@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {
|
||||
FormattedDate,
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -25,7 +24,8 @@ export const CERT_STATUS_TYPE = {
|
||||
UNVERIFIED: 'unverified',
|
||||
};
|
||||
|
||||
const CertificateStatusAlert = ({ intl, payload }) => {
|
||||
const CertificateStatusAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
certificateAvailableDate,
|
||||
@@ -192,7 +192,6 @@ const CertificateStatusAlert = ({ intl, payload }) => {
|
||||
};
|
||||
|
||||
CertificateStatusAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
certificateAvailableDate: PropTypes.string,
|
||||
certStatus: PropTypes.string,
|
||||
@@ -210,4 +209,4 @@ CertificateStatusAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CertificateStatusAlert);
|
||||
export default CertificateStatusAlert;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -14,7 +14,8 @@ import outlineMessages from '../../messages';
|
||||
import useEnrollClickHandler from '../../../../alerts/enrollment-alert/clickHook';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
const PrivateCourseAlert = ({ intl, payload }) => {
|
||||
const PrivateCourseAlert = ({ payload }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
anonymousUser,
|
||||
canEnroll,
|
||||
@@ -103,7 +104,6 @@ const PrivateCourseAlert = ({ intl, payload }) => {
|
||||
};
|
||||
|
||||
PrivateCourseAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
payload: PropTypes.shape({
|
||||
anonymousUser: PropTypes.bool,
|
||||
canEnroll: PropTypes.bool,
|
||||
@@ -111,4 +111,4 @@ PrivateCourseAlert.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PrivateCourseAlert);
|
||||
export default PrivateCourseAlert;
|
||||
|
||||
@@ -204,122 +204,122 @@ const messages = defineMessages({
|
||||
notStartedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.notStarted',
|
||||
defaultMessage: 'Not Started',
|
||||
description: 'It indcate that proctortrack onboarding exam hasn’t started yet',
|
||||
description: 'It indcate that proctored onboarding exam hasn’t started yet',
|
||||
},
|
||||
startedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.started',
|
||||
defaultMessage: 'Started',
|
||||
description: 'Label to indicate the starting status of the proctortrack onboarding exam',
|
||||
description: 'Label to indicate the starting status of the proctored onboarding exam',
|
||||
},
|
||||
submittedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.submitted',
|
||||
defaultMessage: 'Submitted',
|
||||
description: 'Label to indicate the submitted status of proctortrack onboarding exam',
|
||||
description: 'Label to indicate the submitted status of proctored onboarding exam',
|
||||
},
|
||||
verifiedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.verified',
|
||||
defaultMessage: 'Verified',
|
||||
description: 'Label to indicate the verified status of the proctortrack onboarding exam',
|
||||
description: 'Label to indicate the verified status of the proctored onboarding exam',
|
||||
},
|
||||
rejectedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.rejected',
|
||||
defaultMessage: 'Rejected',
|
||||
description: 'Label to indicate the rejection status of the proctortrack onboarding exam',
|
||||
description: 'Label to indicate the rejection status of the proctored onboarding exam',
|
||||
},
|
||||
errorProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.error',
|
||||
defaultMessage: 'Error',
|
||||
description: 'Label to indicate that there is error in proctortrack onboarding exam',
|
||||
description: 'Label to indicate that there is error in proctored onboarding exam',
|
||||
},
|
||||
otherCourseApprovedProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.otherCourseApproved',
|
||||
defaultMessage: 'Approved in Another Course',
|
||||
description: 'Label to indicate that the proctortrack onboarding exam is verified based on taking onboarding exam on another course',
|
||||
description: 'Label to indicate that the proctored onboarding exam is verified based on taking onboarding exam on another course',
|
||||
},
|
||||
expiringSoonProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.expiringSoon',
|
||||
defaultMessage: 'Expiring Soon',
|
||||
description: 'A label to indicate that proctortrack onboarding exam will expire soon',
|
||||
description: 'A label to indicate that proctored onboarding exam will expire soon',
|
||||
},
|
||||
expiredProctoringStatus: {
|
||||
id: 'learning.proctoringPanel.status.expired',
|
||||
defaultMessage: 'Expired',
|
||||
description: 'A label to indicate that proctortrack onboarding exam has expired',
|
||||
description: 'A label to indicate that proctored onboarding exam has expired',
|
||||
},
|
||||
proctoringCurrentStatus: {
|
||||
id: 'learning.proctoringPanel.status',
|
||||
defaultMessage: 'Current Onboarding Status:',
|
||||
description: 'The text that precede the status label of proctortrack onboarding exam',
|
||||
description: 'The text that precede the status label of proctored onboarding exam',
|
||||
},
|
||||
notStartedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.notStarted',
|
||||
defaultMessage: 'You have not started your onboarding exam.',
|
||||
description: 'The text that explain the meaning of (not started) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (not started) label of the proctored onboarding exam',
|
||||
},
|
||||
startedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.started',
|
||||
defaultMessage: 'You have started your onboarding exam.',
|
||||
description: 'The text that explain the meaning of (started) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (started) label of the proctored onboarding exam',
|
||||
},
|
||||
submittedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.submitted',
|
||||
defaultMessage: 'You have submitted your onboarding exam.',
|
||||
description: 'The text that explain the meaning of (submitted) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (submitted) label of the proctored onboarding exam',
|
||||
},
|
||||
verifiedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.verified',
|
||||
defaultMessage: 'Your onboarding exam has been approved in this course.',
|
||||
description: 'The text that explain the meaning of (verified) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (verified) label of the proctored onboarding exam',
|
||||
},
|
||||
rejectedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.rejected',
|
||||
defaultMessage: 'Your onboarding exam has been rejected. Please retry onboarding.',
|
||||
description: 'The text that explain the meaning of (rejected) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (rejected) label of the proctored onboarding exam',
|
||||
},
|
||||
errorProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.error',
|
||||
defaultMessage: 'An error has occurred during your onboarding exam. Please retry onboarding.',
|
||||
description: 'The text that explain the meaning of (error) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (error) label of the proctored onboarding exam',
|
||||
},
|
||||
otherCourseApprovedProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.otherCourseApproved',
|
||||
defaultMessage: 'Your onboarding exam has been approved in another course.',
|
||||
description: 'The text that explain the meaning of (approved in another course) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (approved in another course) label of the proctored onboarding exam',
|
||||
},
|
||||
otherCourseApprovedProctoringDetail: {
|
||||
id: 'learning.proctoringPanel.detail.otherCourseApproved',
|
||||
defaultMessage: 'If your device has changed, we recommend that you complete this course\'s onboarding exam in order to ensure that your setup still meets the requirements for proctoring.',
|
||||
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (approved in another course)',
|
||||
description: 'The text that recommend an action when the status of the proctored onboarding exam is (approved in another course)',
|
||||
},
|
||||
expiringSoonProctoringMessage: {
|
||||
id: 'learning.proctoringPanel.message.expiringSoon',
|
||||
defaultMessage: 'Your onboarding profile has been approved. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.',
|
||||
description: 'The text that recommend an action when the status of the proctortrack onboarding exam is (expiring soon)',
|
||||
description: 'The text that recommend an action when the status of the proctored 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)',
|
||||
description: 'The text that recommend an action when the status of the proctored onboarding exam is (expired)',
|
||||
},
|
||||
proctoringPanelGeneralInfo: {
|
||||
id: 'learning.proctoringPanel.generalInfo',
|
||||
defaultMessage: 'You must complete the onboarding process prior to taking any proctored exam. ',
|
||||
description: 'It indicate key and important fact to learner about the importance of taking proctortrack onboarding exam',
|
||||
description: 'It indicate key and important fact to learner about the importance of taking proctored onboarding exam',
|
||||
},
|
||||
proctoringPanelGeneralInfoSubmitted: {
|
||||
id: 'learning.proctoringPanel.generalInfoSubmitted',
|
||||
defaultMessage: 'Your submitted profile is in review.',
|
||||
description: 'The text that explain the meaning of (in review) label of the proctortrack onboarding exam',
|
||||
description: 'The text that explain the meaning of (in review) label of the proctored onboarding exam',
|
||||
},
|
||||
proctoringPanelGeneralTime: {
|
||||
id: 'learning.proctoringPanel.generalTime',
|
||||
defaultMessage: 'Onboarding profile review can take 2+ business days.',
|
||||
description: 'This text explain for how long the (in review) status of the proctortrack onboarding exam might remain',
|
||||
description: 'This text explain for how long the (in review) status of the proctored onboarding exam might remain',
|
||||
},
|
||||
proctoringOnboardingButton: {
|
||||
id: 'learning.proctoringPanel.onboardingButton',
|
||||
defaultMessage: 'Complete Onboarding',
|
||||
description: 'Text shown on the button that starts the actual proctortrack onboarding exam when it is released',
|
||||
description: 'Text shown on the button that starts the actual proctored onboarding exam when it is released',
|
||||
},
|
||||
proctoringOnboardingPracticeButton: {
|
||||
id: 'learning.proctoringPanel.onboardingPracticeButton',
|
||||
@@ -329,17 +329,17 @@ const messages = defineMessages({
|
||||
proctoringOnboardingButtonNotOpen: {
|
||||
id: 'learning.proctoringPanel.onboardingButtonNotOpen',
|
||||
defaultMessage: 'Onboarding Opens: {releaseDate}',
|
||||
description: 'It indicate when or from when the learner can take the proctortrack onboarding exam',
|
||||
description: 'It indicate when or from when the learner can take the proctored onboarding exam',
|
||||
},
|
||||
proctoringReviewRequirementsButton: {
|
||||
id: 'learning.proctoringPanel.reviewRequirementsButton',
|
||||
defaultMessage: 'Review instructions and system requirements',
|
||||
description: 'Anchor text for button which redirect leaner to doc or a detailed page about proctortrack onboarding exam',
|
||||
description: 'Anchor text for button which redirect leaner to doc or a detailed page about proctored onboarding exam',
|
||||
},
|
||||
proctoringOnboardingButtonPastDue: {
|
||||
id: 'learning.proctoringPanel.onboardingButtonPastDue',
|
||||
defaultMessage: 'Onboarding Past Due',
|
||||
description: 'Text that show when the deadline of proctortrack onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason',
|
||||
description: 'Text that show when the deadline of proctored onboarding exam has passed, it appears on button that start the onboarding exam however for this case the button is disabled for obvious reason',
|
||||
},
|
||||
sequenceDueDate: {
|
||||
id: 'learning.outline.sequence-due-date-set',
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import DateSummary from '../DateSummary';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const CourseDates = ({
|
||||
intl,
|
||||
}) => {
|
||||
const CourseDates = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -40,7 +39,7 @@ const CourseDates = ({
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
<a className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
||||
<a id="dates-tab-link" className="font-weight-bold ml-4 pl-1 small" href={datesTabLink}>
|
||||
{intl.formatMessage(messages.allDates)}
|
||||
</a>
|
||||
</div>
|
||||
@@ -48,8 +47,4 @@ const CourseDates = ({
|
||||
);
|
||||
};
|
||||
|
||||
CourseDates.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseDates);
|
||||
export default CourseDates;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import LmsHtmlFragment from '../LmsHtmlFragment';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const CourseHandouts = ({ intl }) => {
|
||||
const CourseHandouts = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -31,8 +32,4 @@ const CourseHandouts = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseHandouts.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseHandouts);
|
||||
export default CourseHandouts;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBookmark, faCertificate, faInfo, faCalendar, faStar,
|
||||
@@ -14,7 +14,8 @@ import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import LaunchCourseHomeTourButton from '../../../product-tours/newUserCourseHomeTour/LaunchCourseHomeTourButton';
|
||||
|
||||
const CourseTools = ({ intl }) => {
|
||||
const CourseTools = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -81,8 +82,4 @@ const CourseTools = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseTools.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseTools);
|
||||
export default CourseTools;
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
.flag-button {
|
||||
background-color: $white;
|
||||
border: 1px solid $light-400;
|
||||
background-color: var(--pgn-color-white);
|
||||
border: 1px solid var(--pgn-color-light-400);
|
||||
border-radius: .2rem;
|
||||
box-shadow: 0 0 0 2px $light-400;
|
||||
box-shadow: 0 0 0 2px var(--pgn-color-light-400);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $primary-300;
|
||||
box-shadow: 0 0 0 2px $white;
|
||||
border: 1px solid var(--pgn-color-primary-300);
|
||||
box-shadow: 0 0 0 2px var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.flag-button-selected {
|
||||
border: 1px solid $primary-300;
|
||||
box-shadow: 0 0 0 2px $primary-300;
|
||||
border: 1px solid var(--pgn-color-primary-300);
|
||||
box-shadow: 0 0 0 2px var(--pgn-color-primary-300);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
// These flag svgs are derivatives of the Flag icon from paragon
|
||||
import { ReactComponent as FlagIntenseIcon } from './flag_black.svg';
|
||||
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
||||
@@ -13,8 +13,8 @@ const LearningGoalButton = ({
|
||||
level,
|
||||
isSelected,
|
||||
handleSelect,
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const buttonDetails = {
|
||||
casual: {
|
||||
daysPerWeek: 1,
|
||||
@@ -53,7 +53,6 @@ LearningGoalButton.propTypes = {
|
||||
level: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LearningGoalButton);
|
||||
export default LearningGoalButton;
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getExternalLinkUrl } from '@edx/frontend-platform';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
@@ -10,7 +11,8 @@ import { getProctoringInfoData } from '../../data/api';
|
||||
import { fetchProctoringInfoResolved } from '../../data/slice';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const ProctoringInfoPanel = ({ intl }) => {
|
||||
const ProctoringInfoPanel = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -206,7 +208,7 @@ const ProctoringInfoPanel = ({ intl }) => {
|
||||
{isSubmissionRequired(readableStatus) && (
|
||||
onboardingExamButton
|
||||
)}
|
||||
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
||||
<Button variant="outline-primary" block href={getExternalLinkUrl('https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams')}>
|
||||
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -216,8 +218,4 @@ const ProctoringInfoPanel = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
ProctoringInfoPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ProctoringInfoPanel);
|
||||
export default ProctoringInfoPanel;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.outline-sidebar-proctoring-panel {
|
||||
border: 1px solid $dark-500;
|
||||
border-top: 5px solid $brand-600;
|
||||
border: 1px solid var(--pgn-color-dark-500);
|
||||
border-top: 5px solid var(--pgn-color-brand-600);
|
||||
}
|
||||
.proctoring-onboarding-success {
|
||||
border-top: 5px solid $primary-500;
|
||||
border-top: 5px solid var(--pgn-color-primary-500);
|
||||
}
|
||||
.proctoring-onboarding-submitted {
|
||||
border-top: 5px solid $dark-500;
|
||||
border-top: 5px solid var(--pgn-color-dark-500);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Button, Card } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
||||
import messages from '../messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const StartOrResumeCourseCard = ({ intl }) => {
|
||||
const StartOrResumeCourseCard = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -62,8 +63,4 @@ const StartOrResumeCourseCard = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StartOrResumeCourseCard.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(StartOrResumeCourseCard);
|
||||
export default StartOrResumeCourseCard;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Form, Card, Icon } from '@openedx/paragon';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Email } from '@openedx/paragon/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import messages from '../messages';
|
||||
@@ -18,8 +18,8 @@ import './FlagButton.scss';
|
||||
const WeeklyLearningGoalCard = ({
|
||||
daysPerWeek,
|
||||
subscribedToReminders,
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -152,11 +152,10 @@ const WeeklyLearningGoalCard = ({
|
||||
WeeklyLearningGoalCard.propTypes = {
|
||||
daysPerWeek: PropTypes.number,
|
||||
subscribedToReminders: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
WeeklyLearningGoalCard.defaultProps = {
|
||||
daysPerWeek: null,
|
||||
subscribedToReminders: false,
|
||||
};
|
||||
export default injectIntl(WeeklyLearningGoalCard);
|
||||
export default WeeklyLearningGoalCard;
|
||||
|
||||
@@ -661,143 +661,133 @@ describe('Progress Tab', () => {
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Grade Summary when assignment policies are not populated', async () => {
|
||||
it('does not render Grade Summary when assignment type grade summary is not populated', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
section_scores: [],
|
||||
assignment_type_grade_summary: [],
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates grades correctly when number of droppable assignments equals total number of assignments', async () => {
|
||||
it('shows lock icon when all subsections of assignment type are hidden', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 2,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 0% 0%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of droppable assignments is less than total number of assignments', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of droppable assignments is zero', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 100% 50% 50%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of total assignments is less than the number of assignments created', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 1,
|
||||
num_total: 1, // two assignments created in the factory, but 1 is expected per Studio settings
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 5, // two assignments created in the factory, but 5 are expected per Studio settings
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 100% 20% 20%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates weighted grades correctly', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 1,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 0.5,
|
||||
},
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 1,
|
||||
short_label: 'Ex',
|
||||
type: 'Exam',
|
||||
weight: 0.5,
|
||||
short_label: 'Final',
|
||||
type: 'Final Exam',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Final Exam',
|
||||
weight: 0.4,
|
||||
average_grade: 0.0,
|
||||
weighted_grade: 0.0,
|
||||
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||
has_hidden_contribution: 'all',
|
||||
short_label: 'Final',
|
||||
num_droppable: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 50% 100% 50%' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument();
|
||||
// Should show lock icon for grade and weighted grade
|
||||
expect(screen.getAllByTestId('lock-icon')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('shows percent plus hidden grades when some subsections of assignment type are hidden', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
average_grade: 0.25,
|
||||
weighted_grade: 0.25,
|
||||
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||
has_hidden_contribution: 'some',
|
||||
short_label: 'HW',
|
||||
num_droppable: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
// Should show percent + hidden scores for grade and weighted grade
|
||||
const hiddenScoresCells = screen.getAllByText(/% \+ Hidden Scores/);
|
||||
expect(hiddenScoresCells).toHaveLength(2);
|
||||
// Only correct visible scores should be shown (from subsection2)
|
||||
// The correct visible score is 1/4 = 0.25 -> 25%
|
||||
expect(hiddenScoresCells[0]).toHaveTextContent('25% + Hidden Scores');
|
||||
expect(hiddenScoresCells[1]).toHaveTextContent('25% + Hidden Scores');
|
||||
});
|
||||
|
||||
it('displays a warning message with the latest due date when not all assignment scores are included in the total grade', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
average_grade: 1,
|
||||
weighted_grade: 1,
|
||||
last_grade_publish_date: tomorrow.toISOString(),
|
||||
has_hidden_contribution: 'none',
|
||||
short_label: 'HW',
|
||||
num_droppable: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
const formattedDateTime = new Intl.DateTimeFormat('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
}).format(tomorrow);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
`Some assignment scores are not yet included in your total grade. These grades will be released by ${formattedDateTime}.`,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders override notice', async () => {
|
||||
|
||||
@@ -187,7 +187,8 @@ const CertificateStatus = () => {
|
||||
// regardless of passing or nonpassing status
|
||||
if (!canViewCertificate) {
|
||||
certCase = 'notAvailable';
|
||||
endDate = intl.formatDate(end, {
|
||||
// use the certificate_available_date if it is available, otherwise use the end date of the course
|
||||
endDate = intl.formatDate((certificateAvailableDate || end), {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
|
||||
.donut-chart-label {
|
||||
font: {
|
||||
family: $font-family-sans-serif;
|
||||
family: var(--pgn-typography-font-family-sans-serif);
|
||||
size: .2rem;
|
||||
weight: $font-weight-normal;
|
||||
weight: var(--pgn-typography-font-weight-normal);
|
||||
}
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.donut-chart-number {
|
||||
font: {
|
||||
family: $font-family-monospace;
|
||||
family: var(--pgn-typography-font-family-monospace);
|
||||
size: .5rem;
|
||||
weight: $font-weight-bold;
|
||||
weight: var(--pgn-typography-font-weight-bold);
|
||||
}
|
||||
line-height: 1rem;
|
||||
text-anchor: middle;
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
.donut-chart-text {
|
||||
fill: $primary-500;
|
||||
fill: var(--pgn-color-primary-500);
|
||||
-moz-transform: translateY(0.25em);
|
||||
-ms-transform: translateY(0.25em);
|
||||
-webkit-transform: translateY(0.25em);
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
.donut-ring, .donut-segment, .donut-hole {
|
||||
&.complete-stroke {
|
||||
stroke: $info-500;
|
||||
stroke: var(--pgn-color-info-500);
|
||||
}
|
||||
|
||||
&.divider-stroke {
|
||||
@@ -65,10 +65,10 @@
|
||||
}
|
||||
|
||||
&.incomplete-stroke {
|
||||
stroke: $light-300;
|
||||
stroke: var(--pgn-color-light-300);
|
||||
}
|
||||
|
||||
&.locked-stroke {
|
||||
stroke: $primary-500;
|
||||
stroke: var(--pgn-color-primary-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,57 @@ import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||
import messages from '../messages';
|
||||
import { getLatestDueDateInFuture } from '../../utils';
|
||||
|
||||
const ResponsiveText = ({
|
||||
wideScreen, children, hasLetterGrades, passingGrade,
|
||||
}) => {
|
||||
const className = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||
const iconSize = wideScreen ? 'h3' : 'h4';
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{children}
|
||||
{hasLetterGrades && (
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
|
||||
<GradeRangeTooltip iconButtonClassName={iconSize} passingGrade={passingGrade} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const NoticeRow = ({
|
||||
wideScreen, icon, bgClass, message,
|
||||
}) => {
|
||||
const textClass = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||
return (
|
||||
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${bgClass}`}>
|
||||
<div className="col-auto p-0">{icon}</div>
|
||||
<div className="col-11 pl-2 px-0">
|
||||
<span className={textClass}>{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CourseGradeFooter = ({ passingGrade }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
courseGrade: {
|
||||
isPassing,
|
||||
letterGrade,
|
||||
},
|
||||
gradingPolicy: {
|
||||
gradeRange,
|
||||
},
|
||||
assignmentTypeGradeSummary,
|
||||
courseGrade: { isPassing, letterGrade },
|
||||
gradingPolicy: { gradeRange },
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const latestDueDate = getLatestDueDateInFuture(assignmentTypeGradeSummary);
|
||||
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||
const hasLetterGrades = Object.keys(gradeRange).length > 1;
|
||||
|
||||
const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key
|
||||
// build footer text
|
||||
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
||||
|
||||
if (isPassing) {
|
||||
if (hasLetterGrades) {
|
||||
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
|
||||
@@ -47,42 +78,63 @@ const CourseGradeFooter = ({ passingGrade }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const icon = isPassing ? <Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
||||
: <Icon src={WarningFilled} className="d-inline-flex align-bottom" />;
|
||||
const passingIcon = isPassing ? (
|
||||
<Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
||||
) : (
|
||||
<Icon src={WarningFilled} className="d-inline-flex align-bottom" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${isPassing ? 'bg-success-100' : 'bg-warning-100'}`}>
|
||||
<div className="col-auto p-0">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="col-11 pl-2 px-0">
|
||||
{!wideScreen && (
|
||||
<span className="h5 align-bottom">
|
||||
<div>
|
||||
<NoticeRow
|
||||
wideScreen={wideScreen}
|
||||
icon={passingIcon}
|
||||
bgClass={isPassing ? 'bg-success-100' : 'bg-warning-100'}
|
||||
message={(
|
||||
<ResponsiveText
|
||||
wideScreen={wideScreen}
|
||||
hasLetterGrades={hasLetterGrades}
|
||||
passingGrade={passingGrade}
|
||||
>
|
||||
{footerText}
|
||||
{hasLetterGrades && (
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
|
||||
<GradeRangeTooltip iconButtonClassName="h4" passingGrade={passingGrade} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</ResponsiveText>
|
||||
)}
|
||||
{wideScreen && (
|
||||
<span className="h4 m-0 align-bottom">
|
||||
{footerText}
|
||||
{hasLetterGrades && (
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
|
||||
<GradeRangeTooltip iconButtonClassName="h3" passingGrade={passingGrade} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
/>
|
||||
{latestDueDate && (
|
||||
<NoticeRow
|
||||
wideScreen={wideScreen}
|
||||
icon={<Icon src={WarningFilled} className="d-inline-flex align-bottom" />}
|
||||
bgClass="bg-warning-100"
|
||||
message={intl.formatMessage(messages.courseGradeFooterDueDateNotice, {
|
||||
dueDate: intl.formatDate(latestDueDate, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResponsiveText.propTypes = {
|
||||
wideScreen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
hasLetterGrades: PropTypes.bool.isRequired,
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
NoticeRow.propTypes = {
|
||||
wideScreen: PropTypes.bool.isRequired,
|
||||
icon: PropTypes.element.isRequired,
|
||||
bgClass: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseGradeFooter.propTypes = {
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ const CourseGradeHeader = () => {
|
||||
previewText = intl.formatMessage(messages.courseGradePreviewUpgradeDeadlinePassedBody);
|
||||
}
|
||||
return (
|
||||
<div className="row w-100 m-0 p-4 rounded-top bg-primary-500 text-white">
|
||||
<div id="grade-course-header" className="row w-100 m-0 p-4 rounded-top bg-primary-500 text-white">
|
||||
<div className={`col-12 ${verifiedMode ? 'col-md-9' : ''} p-0`}>
|
||||
<div className="row w-100 m-0 p-0">
|
||||
<div className="col-1 p-0">
|
||||
@@ -71,7 +71,7 @@ const CourseGradeHeader = () => {
|
||||
</div>
|
||||
{verifiedMode && (
|
||||
<div className="col-12 col-md-3 mt-3 mt-md-0 p-0 align-self-center text-right">
|
||||
<Button variant="brand" size="sm" href={verifiedMode.upgradeUrl} onClick={logUpgradeButtonClick}>
|
||||
<Button id="upgrade-button" variant="brand" size="sm" href={verifiedMode.upgradeUrl} onClick={logUpgradeButtonClick}>
|
||||
{intl.formatMessage(messages.courseGradePreviewUpgradeButton)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
assignmentTypeGradeSummary,
|
||||
courseGrade: {
|
||||
isPassing,
|
||||
percent,
|
||||
@@ -25,6 +26,8 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
const hasHiddenGrades = assignmentTypeGradeSummary.some((assignmentType) => assignmentType.hasHiddenContribution !== 'none');
|
||||
|
||||
if (isLocaleRtl) {
|
||||
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
||||
}
|
||||
@@ -56,6 +59,15 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||
>
|
||||
{intl.formatMessage(messages.currentGradeLabel)}
|
||||
</text>
|
||||
<text
|
||||
className="x-small"
|
||||
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
||||
x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`}
|
||||
y="35px"
|
||||
style={{ transform: `translateX(${currentGradeDirection}3.4em)` }}
|
||||
>
|
||||
{hasHiddenGrades ? ` + ${intl.formatMessage(messages.hiddenScoreLabel)}` : ''}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,24 +4,24 @@
|
||||
}
|
||||
|
||||
.grade-bar__base {
|
||||
fill: $light-300;
|
||||
fill: var(--pgn-color-light-300);
|
||||
}
|
||||
|
||||
.grade-bar__divider {
|
||||
fill: $primary-500;
|
||||
fill: var(--pgn-color-primary-500);
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.grade-bar--passing {
|
||||
fill: $primary-500;
|
||||
fill: var(--pgn-color-primary-500);
|
||||
}
|
||||
|
||||
.grade-bar--current-passing {
|
||||
fill: $success-500;
|
||||
fill: var(--pgn-color-success-500);
|
||||
}
|
||||
|
||||
.grade-bar--current-non-passing {
|
||||
fill: $accent-b;
|
||||
fill: var(--pgn-color-accent-b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,22 +31,22 @@
|
||||
|
||||
#minimum-grade-tooltip {
|
||||
.arrow::after {
|
||||
border-bottom-color: $primary-500;
|
||||
border-bottom-color: var(--pgn-color-primary-500);
|
||||
}
|
||||
}
|
||||
|
||||
#passing-grade-tooltip {
|
||||
background: $success-500;
|
||||
background: var(--pgn-color-success-500);
|
||||
|
||||
.arrow::after {
|
||||
border-top-color: $success-500;
|
||||
border-top-color: var(--pgn-color-success-500);
|
||||
}
|
||||
}
|
||||
|
||||
#non-passing-grade-tooltip {
|
||||
background: $accent-b;
|
||||
background: var(--pgn-color-accent-b);
|
||||
|
||||
.arrow::after {
|
||||
border-top-color: $accent-b;
|
||||
border-top-color: var(--pgn-color-accent-b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,12 @@ const GradeSummary = () => {
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
gradingPolicy: {
|
||||
assignmentPolicies,
|
||||
},
|
||||
assignmentTypeGradeSummary,
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const [allOfSomeAssignmentTypeIsLocked, setAllOfSomeAssignmentTypeIsLocked] = useState(false);
|
||||
|
||||
if (assignmentPolicies.length === 0) {
|
||||
if (assignmentTypeGradeSummary.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@openedx/paragon';
|
||||
import { Lock } from '@openedx/paragon/icons';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
@@ -16,9 +17,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
gradingPolicy: {
|
||||
assignmentPolicies,
|
||||
},
|
||||
assignmentTypeGradeSummary,
|
||||
gradesFeatureIsFullyLocked,
|
||||
sectionScores,
|
||||
} = useModel('progress', courseId);
|
||||
@@ -55,7 +54,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const gradeSummaryData = assignmentPolicies.map((assignment) => {
|
||||
const gradeSummaryData = assignmentTypeGradeSummary.map((assignment) => {
|
||||
const {
|
||||
averageGrade,
|
||||
numDroppable,
|
||||
@@ -80,13 +79,24 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignmentType);
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
let weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`;
|
||||
let gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`;
|
||||
|
||||
if (assignment.hasHiddenContribution === 'all') {
|
||||
gradeDisplay = <Lock data-testid="lock-icon" />;
|
||||
weightedGradeDisplay = <Lock data-testid="lock-icon" />;
|
||||
} else if (assignment.hasHiddenContribution === 'some') {
|
||||
gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`;
|
||||
weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: {
|
||||
footnoteId, footnoteMarker, type: assignmentType, locked,
|
||||
},
|
||||
weight: { weight: `${(weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
grade: { grade: `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
grade: { grade: gradeDisplay, locked },
|
||||
weightedGrade: { weightedGrade: weightedGradeDisplay, locked },
|
||||
};
|
||||
});
|
||||
const getAssignmentTypeCell = (value) => (
|
||||
@@ -102,6 +112,16 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="micro mb-3 pl-3 text-gray-700">
|
||||
<li>
|
||||
<b>{intl.formatMessage(messages.hiddenScoreLabel)}: </b>
|
||||
{intl.formatMessage(messages.hiddenScoreInfoText)}
|
||||
</li>
|
||||
<li>
|
||||
<b><Lock style={{ height: '15px' }} />: </b>
|
||||
{` ${intl.formatMessage(messages.hiddenScoreLockInfoText)}`}
|
||||
</li>
|
||||
</ul>
|
||||
<DataTable
|
||||
data={gradeSummaryData}
|
||||
itemCount={gradeSummaryData.length}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
DataTable,
|
||||
DataTableContext,
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Stack,
|
||||
@@ -17,18 +14,6 @@ import messages from '../messages';
|
||||
|
||||
const GradeSummaryTableFooter = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { data } = useContext(DataTableContext);
|
||||
|
||||
const rawGrade = data.reduce(
|
||||
(grade, currentValue) => {
|
||||
const { weightedGrade } = currentValue.weightedGrade;
|
||||
const percent = weightedGrade.replace(/%/g, '').trim();
|
||||
return grade + parseFloat(percent);
|
||||
},
|
||||
0,
|
||||
).toFixed(2);
|
||||
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
@@ -36,8 +21,16 @@ const GradeSummaryTableFooter = () => {
|
||||
isPassing,
|
||||
percent,
|
||||
},
|
||||
finalGrades,
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const getGradePercent = (grade) => {
|
||||
const percentage = grade * 100;
|
||||
return Number.isInteger(percentage) ? percentage.toFixed(0) : percentage.toFixed(2);
|
||||
};
|
||||
|
||||
const rawGrade = getGradePercent(finalGrades);
|
||||
|
||||
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
||||
const totalGrade = (percent * 100).toFixed(0);
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.',
|
||||
description: 'Alt text for the grade chart bar',
|
||||
},
|
||||
courseGradeFooterDueDateNotice: {
|
||||
id: 'progress.courseGrade.footer.dueDateNotice',
|
||||
defaultMessage: 'Some assignment scores are not yet included in your total grade. These grades will be released by {dueDate}.',
|
||||
description: 'This is shown when there are pending assignments with a due date in the future',
|
||||
},
|
||||
courseGradeFooterGenericPassing: {
|
||||
id: 'progress.courseGrade.footer.generic.passing',
|
||||
defaultMessage: 'You’re currently passing this course',
|
||||
@@ -148,6 +153,21 @@ const messages = defineMessages({
|
||||
+ "Your weighted grade is what's used to determine if you pass the course.",
|
||||
description: 'The content of (tip box) for the grade summary section',
|
||||
},
|
||||
hiddenScoreLabel: {
|
||||
id: 'progress.hiddenScoreLabel',
|
||||
defaultMessage: 'Hidden Scores',
|
||||
description: 'Text to indicate that some scores are hidden',
|
||||
},
|
||||
hiddenScoreInfoText: {
|
||||
id: 'progress.hiddenScoreInfoText',
|
||||
defaultMessage: 'Scores from assignments that count toward your final grade but some are not shown here.',
|
||||
description: 'Information text about hidden score label',
|
||||
},
|
||||
hiddenScoreLockInfoText: {
|
||||
id: 'progress.hiddenScoreLockInfoText',
|
||||
defaultMessage: 'Scores for an assignment type are hidden but still counted toward the course grade.',
|
||||
description: 'Information text about hidden score label when learners have limited access to grades feature',
|
||||
},
|
||||
noAccessToAssignmentType: {
|
||||
id: 'progress.noAcessToAssignmentType',
|
||||
defaultMessage: 'You do not have access to assignments of type {assignmentType}',
|
||||
|
||||
@@ -5,3 +5,15 @@ export const showUngradedAssignments = () => (
|
||||
getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === 'true'
|
||||
|| getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === true
|
||||
);
|
||||
|
||||
export const getLatestDueDateInFuture = (assignmentTypeGradeSummary) => {
|
||||
let latest = null;
|
||||
assignmentTypeGradeSummary.forEach((assignment) => {
|
||||
const assignmentLastGradePublishDate = assignment.lastGradePublishDate;
|
||||
if (assignmentLastGradePublishDate && (!latest || new Date(assignmentLastGradePublishDate) > new Date(latest))
|
||||
&& new Date(assignmentLastGradePublishDate) > new Date()) {
|
||||
latest = assignmentLastGradePublishDate;
|
||||
}
|
||||
});
|
||||
return latest;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -14,7 +14,8 @@ import { resetDeadlines } from '../data';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
const ShiftDatesAlert = ({ fetch, intl, model }) => {
|
||||
const ShiftDatesAlert = ({ fetch, model }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -59,8 +60,7 @@ const ShiftDatesAlert = ({ fetch, intl, model }) => {
|
||||
|
||||
ShiftDatesAlert.propTypes = {
|
||||
fetch: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
model: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ShiftDatesAlert);
|
||||
export default ShiftDatesAlert;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SuggestedScheduleHeader = ({ intl }) => (
|
||||
<p className="large">
|
||||
{intl.formatMessage(messages.suggestedSchedule)}
|
||||
</p>
|
||||
);
|
||||
|
||||
SuggestedScheduleHeader.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
const SuggestedScheduleHeader = () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<p className="large">
|
||||
{intl.formatMessage(messages.suggestedSchedule)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(SuggestedScheduleHeader);
|
||||
export default SuggestedScheduleHeader;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
|
||||
const UpgradeToCompleteAlert = ({ logUpgradeLinkClick }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -33,7 +34,7 @@ const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert className="bg-light-200">
|
||||
<Alert id="upgrade-complete-alert" className="bg-light-200">
|
||||
<Row className="w-100 m-0">
|
||||
<Col xs={12} md={9} className="small p-0 pr-md-2">
|
||||
<Alert.Heading>{intl.formatMessage(messages.upgradeToCompleteHeader)}</Alert.Heading>
|
||||
@@ -58,7 +59,6 @@ const UpgradeToCompleteAlert = ({ intl, logUpgradeLinkClick }) => {
|
||||
};
|
||||
|
||||
UpgradeToCompleteAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
logUpgradeLinkClick: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -66,4 +66,4 @@ UpgradeToCompleteAlert.defaultProps = {
|
||||
logUpgradeLinkClick: () => {},
|
||||
};
|
||||
|
||||
export default injectIntl(UpgradeToCompleteAlert);
|
||||
export default UpgradeToCompleteAlert;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import messages from './messages';
|
||||
|
||||
const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
|
||||
const UpgradeToShiftDatesAlert = ({ logUpgradeLinkClick, model }) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
courseId,
|
||||
} = useSelector(state => state.courseHome);
|
||||
@@ -35,7 +36,7 @@ const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert className="bg-light-200">
|
||||
<Alert id="upgrade-shift-dates-alert" className="bg-light-200">
|
||||
<Row className="w-100 m-0">
|
||||
<Col xs={12} md={9} className="small p-0 pr-md-2">
|
||||
<strong>{intl.formatMessage(messages.missedDeadlines)}</strong>
|
||||
@@ -60,7 +61,6 @@ const UpgradeToShiftDatesAlert = ({ intl, logUpgradeLinkClick, model }) => {
|
||||
};
|
||||
|
||||
UpgradeToShiftDatesAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
logUpgradeLinkClick: PropTypes.func,
|
||||
model: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -69,4 +69,4 @@ UpgradeToShiftDatesAlert.defaultProps = {
|
||||
logUpgradeLinkClick: () => {},
|
||||
};
|
||||
|
||||
export default injectIntl(UpgradeToShiftDatesAlert);
|
||||
export default UpgradeToShiftDatesAlert;
|
||||
|
||||
20
src/course-tabs/CourseTabLink.tsx
Normal file
20
src/course-tabs/CourseTabLink.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
interface CourseTabLinkProps {
|
||||
slug: string;
|
||||
activeTabSlug?: string;
|
||||
url: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const CourseTabLink = ({
|
||||
slug, activeTabSlug, url, title,
|
||||
}: CourseTabLinkProps) => (
|
||||
<a
|
||||
href={url}
|
||||
className={classNames('nav-item flex-shrink-0 nav-link', { active: slug === activeTabSlug })}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
);
|
||||
25
src/course-tabs/CourseTabLinksList.tsx
Normal file
25
src/course-tabs/CourseTabLinksList.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { CourseTabLink } from '@src/course-tabs/CourseTabLink';
|
||||
import React from 'react';
|
||||
|
||||
interface CourseTabLinkListProps {
|
||||
tabs: Array<{
|
||||
title: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
}>,
|
||||
activeTabSlug?: string;
|
||||
}
|
||||
|
||||
export const CourseTabLinksList = ({ tabs, activeTabSlug }: CourseTabLinkListProps) => (
|
||||
<>
|
||||
{tabs.map(({ url, title, slug }) => (
|
||||
<CourseTabLink
|
||||
key={slug}
|
||||
url={url}
|
||||
slug={slug}
|
||||
title={title}
|
||||
activeTabSlug={activeTabSlug}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -1,16 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
import Tabs from '../generic/tabs/Tabs';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CourseTabLinksSlot } from '../plugin-slots/CourseTabLinksSlot';
|
||||
import { CoursewareSearch, CoursewareSearchToggle } from '../course-home/courseware-search';
|
||||
import { useCoursewareSearchState } from '../course-home/courseware-search/hooks';
|
||||
|
||||
import Tabs from '../generic/tabs/Tabs';
|
||||
import messages from './messages';
|
||||
|
||||
interface CourseTabsNavigationProps {
|
||||
activeTabSlug?: string;
|
||||
className?: string | null;
|
||||
tabs: Array<{
|
||||
title: string;
|
||||
slug: string;
|
||||
url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const CourseTabsNavigation = ({
|
||||
activeTabSlug, className, tabs, intl,
|
||||
}) => {
|
||||
activeTabSlug = undefined,
|
||||
className = null,
|
||||
tabs,
|
||||
}:CourseTabsNavigationProps) => {
|
||||
const intl = useIntl();
|
||||
const { show } = useCoursewareSearchState();
|
||||
|
||||
return (
|
||||
@@ -22,15 +35,7 @@ const CourseTabsNavigation = ({
|
||||
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>
|
||||
))}
|
||||
<CourseTabLinksSlot tabs={tabs} activeTabSlug={activeTabSlug} />
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="search-toggle">
|
||||
@@ -43,20 +48,4 @@ const CourseTabsNavigation = ({
|
||||
);
|
||||
};
|
||||
|
||||
CourseTabsNavigation.propTypes = {
|
||||
activeTabSlug: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
tabs: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
slug: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
CourseTabsNavigation.defaultProps = {
|
||||
activeTabSlug: undefined,
|
||||
className: null,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseTabsNavigation);
|
||||
export default CourseTabsNavigation;
|
||||
@@ -5,13 +5,13 @@
|
||||
.nav a,
|
||||
.nav button {
|
||||
&:hover {
|
||||
background-color: $light-400;
|
||||
background-color: var(--pgn-color-light-400);
|
||||
}
|
||||
}
|
||||
|
||||
.nav a {
|
||||
&:not(.active):hover {
|
||||
background-color: $light-400;
|
||||
background-color: var(--pgn-color-light-400);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getConfig, history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { waitForElementToBeRemoved, fireEvent } from '@testing-library/dom';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { waitForElementToBeRemoved } from '@testing-library/dom';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
@@ -193,15 +193,13 @@ describe('CoursewareContainer', () => {
|
||||
expect(courseHeader.querySelector('.course-title')).toHaveTextContent(courseHomeMetadata.title);
|
||||
}
|
||||
|
||||
function assertSequenceNavigation(container, expectedUnitCount = 3) {
|
||||
// Ensure we had appropriate sequence navigation buttons. We should only have one unit.
|
||||
function assertNoSequenceNavigation(container) {
|
||||
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
|
||||
expect(sequenceNavButtons).toHaveLength(expectedUnitCount + 2);
|
||||
expect(sequenceNavButtons).toHaveLength(0);
|
||||
|
||||
expect(sequenceNavButtons[0]).toHaveTextContent('Previous');
|
||||
// Prove this button is rendering an SVG tasks icon, meaning it's a unit/vertical.
|
||||
expect(sequenceNavButtons[1].querySelector('svg')).toHaveClass('fa-tasks');
|
||||
expect(sequenceNavButtons[sequenceNavButtons.length - 1]).toHaveTextContent('Next');
|
||||
expect(container.querySelector('button, a')).not.toHaveTextContent('Previous');
|
||||
expect(container.querySelector('svg.fa-tasks')).toBeNull();
|
||||
expect(container.querySelector('button, a')).not.toHaveTextContent('Next');
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -221,10 +219,10 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
|
||||
history.push(`/course/${courseId}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -244,10 +242,10 @@ describe('CoursewareContainer', () => {
|
||||
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/courseware/resume/${courseId}`).reply(200, {});
|
||||
|
||||
history.push(`/course/${courseId}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -274,29 +272,12 @@ describe('CoursewareContainer', () => {
|
||||
setUpMockRequests({ courseBlocks });
|
||||
});
|
||||
|
||||
// describe('when the URL contains a unit ID', () => {
|
||||
// it('should ignore the section ID and redirect based on the unit ID', async () => {
|
||||
// const urlUnit = unitTree[1][1][1];
|
||||
// setUrl(sectionTree[1].id, urlUnit.id);
|
||||
// const container = await loadContainer();
|
||||
// assertLoadedHeader(container);
|
||||
// assertSequenceNavigation(container, 2);
|
||||
// assertLocation(container, sequenceTree[1][1].id, urlUnit.id);
|
||||
// });
|
||||
|
||||
// it('should ignore invalid unit IDs and redirect to the course root', async () => {
|
||||
// setUrl(sectionTree[1].id, 'foobar');
|
||||
// await loadContainer();
|
||||
// expect(global.location.href).toEqual(`http://localhost/course/${courseId}`);
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('when the URL does not contain a unit ID', () => {
|
||||
it('should choose a unit within the section\'s first sequence', async () => {
|
||||
setUrl(sectionTree[1].id);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container, 2);
|
||||
assertNoSequenceNavigation(container);
|
||||
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
|
||||
});
|
||||
});
|
||||
@@ -342,37 +323,16 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// describe('when the URL only contains a unit ID', () => {
|
||||
// const { courseBlocks, unitTree, sequenceTree } = buildBinaryCourseBlocks(courseId, courseMetadata.name);
|
||||
|
||||
// beforeEach(async () => {
|
||||
// setUpMockRequests({ courseBlocks });
|
||||
// });
|
||||
|
||||
// it('should insert the sequence ID into the URL', async () => {
|
||||
// const unit = unitTree[1][0][1];
|
||||
// history.push(`/course/${courseId}/${unit.id}`);
|
||||
// const container = await loadContainer();
|
||||
|
||||
// assertLoadedHeader(container);
|
||||
// assertSequenceNavigation(container, 2);
|
||||
// const expectedSequenceId = sequenceTree[1][0].id;
|
||||
// const expectedUrl = `http://localhost/course/${courseId}/${expectedSequenceId}/${unit.id}`;
|
||||
// expect(global.location.href).toEqual(expectedUrl);
|
||||
// expect(container.querySelector('.fake-unit')).toHaveTextContent(unit.id);
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('when the URL contains a course ID and sequence ID', () => {
|
||||
const sequenceBlock = defaultSequenceBlock;
|
||||
const unitBlocks = defaultUnitBlocks;
|
||||
|
||||
it('should pick the first unit if position was not defined (activeUnitIndex becomes 0)', async () => {
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -388,10 +348,10 @@ describe('CoursewareContainer', () => {
|
||||
setUpMockRequests({ sequenceMetadatas: [sequenceMetadata] });
|
||||
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -405,47 +365,27 @@ describe('CoursewareContainer', () => {
|
||||
|
||||
it('should load the specified unit', async () => {
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[2].id}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[2].id);
|
||||
});
|
||||
|
||||
it('should navigate between units and check block completion', async () => {
|
||||
axiosMock.onPost(`${courseId}/xblock/${sequenceBlock.id}/handler/get_completion`).reply(200, {
|
||||
complete: true,
|
||||
});
|
||||
it('should render the sequence_navigation plugin slot correctly', async () => {
|
||||
axiosMock
|
||||
.onPost(`${courseId}/xblock/${sequenceBlock.id}/handler/get_completion`)
|
||||
.reply(200, { complete: true });
|
||||
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
|
||||
const container = await waitFor(() => loadContainer());
|
||||
await loadContainer();
|
||||
|
||||
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
|
||||
const sequenceNextButton = sequenceNavButtons[4];
|
||||
expect(sequenceNextButton).toHaveTextContent('Next');
|
||||
fireEvent.click(sequenceNextButton);
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseId}/${sequenceBlock.id}/${unitBlocks[1].id}`);
|
||||
expect(screen.getByTestId('org.openedx.frontend.learning.sequence_navigation.v1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// describe('when the current sequence is an exam', () => {
|
||||
// const { location } = window;
|
||||
|
||||
// beforeEach(() => {
|
||||
// delete window.location;
|
||||
// window.location = {
|
||||
// assign: jest.fn(),
|
||||
// };
|
||||
// });
|
||||
|
||||
// afterEach(() => {
|
||||
// window.location = location;
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
describe('when receiving a course_access error_code', () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ jest.mock('react-router-dom', () => ({
|
||||
useLocation: () => ({
|
||||
search: '?consentPath=/some-path',
|
||||
}),
|
||||
useSearchParams: () => [new URLSearchParams('?consentPath=/some-path'), () => {}],
|
||||
}));
|
||||
|
||||
describe('RedirectPage component', () => {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
generatePath, useParams, useLocation,
|
||||
generatePath, useParams, useLocation, useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import queryString from 'query-string';
|
||||
import { REDIRECT_MODES } from '../constants';
|
||||
|
||||
const RedirectPage = ({
|
||||
pattern, mode,
|
||||
}) => {
|
||||
interface Props {
|
||||
pattern: string;
|
||||
mode: string;
|
||||
}
|
||||
|
||||
const RedirectPage = ({ pattern = '', mode }: Props) => {
|
||||
const { courseId } = useParams();
|
||||
const location = useLocation();
|
||||
const { consentPath } = queryString.parse(location?.search);
|
||||
const [searchParams] = useSearchParams();
|
||||
const consentPath = searchParams.get('consentPath') ?? '';
|
||||
|
||||
const {
|
||||
LMS_BASE_URL,
|
||||
@@ -39,13 +41,4 @@ const RedirectPage = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
RedirectPage.propTypes = {
|
||||
pattern: PropTypes.string,
|
||||
mode: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
RedirectPage.defaultProps = {
|
||||
pattern: null,
|
||||
};
|
||||
|
||||
export default RedirectPage;
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
|
||||
import { AlertList } from '@src/generic/user-messages';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { getCoursewareOutlineSidebarSettings } from '../data/selectors';
|
||||
import Chat from './chat/Chat';
|
||||
import { LearnerToolsSlot } from '../../plugin-slots/LearnerToolsSlot';
|
||||
import SidebarProvider from './sidebar/SidebarContextProvider';
|
||||
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
|
||||
import { NotificationsDiscussionsSidebarTriggerSlot } from '../../plugin-slots/NotificationsDiscussionsSidebarTriggerSlot';
|
||||
@@ -37,8 +36,6 @@ const Course = ({
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
||||
const { enableNavigationSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
const navigationDisabled = enableNavigationSidebar || (sequence?.navigationDisabled ?? false);
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -62,7 +59,7 @@ const Course = ({
|
||||
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
|
||||
celebrations && !celebrations.streakLengthToCelebrate && celebrations.weeklyGoal,
|
||||
);
|
||||
const shouldDisplayChat = windowWidth >= breakpoints.medium.minWidth;
|
||||
const shouldDisplayLearnerTools = windowWidth >= breakpoints.medium.minWidth;
|
||||
const daysPerWeek = course?.courseGoals?.selectedGoal?.daysPerWeek;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -84,28 +81,20 @@ const Course = ({
|
||||
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="position-relative d-flex align-items-xl-center mb-4 mt-1 flex-column flex-xl-row">
|
||||
{navigationDisabled || (
|
||||
<>
|
||||
<CourseBreadcrumbsSlot
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
<CourseBreadcrumbsSlot
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
isStaff={isStaff}
|
||||
unitId={unitId}
|
||||
/>
|
||||
{shouldDisplayLearnerTools && (
|
||||
<LearnerToolsSlot
|
||||
enrollmentMode={course.enrollmentMode}
|
||||
isStaff={isStaff}
|
||||
courseId={courseId}
|
||||
unitId={unitId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{shouldDisplayChat && (
|
||||
<>
|
||||
<Chat
|
||||
enabled={course.learningAssistantEnabled}
|
||||
enrollmentMode={course.enrollmentMode}
|
||||
isStaff={isStaff}
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={course.showCalculator || course.notes.enabled}
|
||||
unitId={unitId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="w-100 d-flex align-items-center">
|
||||
<CourseOutlineMobileSidebarTriggerSlot />
|
||||
|
||||
@@ -13,17 +13,25 @@ import Course from './Course';
|
||||
import setupDiscussionSidebar from './test-utils';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
|
||||
...jest.requireActual('@edx/frontend-lib-special-exams/dist/data/thunks.js'),
|
||||
checkExamEntry: () => jest.fn(),
|
||||
}));
|
||||
const mockChatTestId = 'fake-chat';
|
||||
jest.mock('@edx/frontend-lib-special-exams', () => {
|
||||
const actual = jest.requireActual('@edx/frontend-lib-special-exams');
|
||||
return {
|
||||
...actual,
|
||||
__esModule: true,
|
||||
// Mock the default export (SequenceExamWrapper) to just render children
|
||||
// eslint-disable-next-line react/prop-types
|
||||
default: ({ children }) => <div data-testid="sequence-exam-wrapper">{children}</div>,
|
||||
};
|
||||
});
|
||||
const mockLearnerToolsTestId = 'fake-learner-tools';
|
||||
jest.mock(
|
||||
'./chat/Chat',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
() => function ({ courseId }) {
|
||||
return <div className="fake-chat" data-testid={mockChatTestId}>Chat contents {courseId} </div>;
|
||||
},
|
||||
'../../plugin-slots/LearnerToolsSlot',
|
||||
() => ({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
LearnerToolsSlot({ courseId }) {
|
||||
return <div className="fake-learner-tools" data-testid={mockLearnerToolsTestId}>LearnerTools contents {courseId} </div>;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const recordFirstSectionCelebration = jest.fn();
|
||||
@@ -50,7 +58,11 @@ describe('Course', () => {
|
||||
global.innerWidth = breakpoints.extraLarge.minWidth;
|
||||
});
|
||||
|
||||
it('loads learning sequence', async () => {
|
||||
// This was passing when it shouldn't have been because of improper
|
||||
// waitFor use. With the React 18 upgrade it no longer improperly passes
|
||||
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
|
||||
// for details.
|
||||
it.skip('loads learning sequence', () => {
|
||||
render(<Course {...mockData} />, { wrapWithRouter: true });
|
||||
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
|
||||
waitFor(() => {
|
||||
@@ -94,7 +106,11 @@ describe('Course', () => {
|
||||
expect(screen.queryByRole('navigation', { name: 'breadcrumb' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays first section celebration modal', async () => {
|
||||
// This was passing when it shouldn't have been because of improper
|
||||
// waitFor use. With the React 18 upgrade it no longer improperly passes
|
||||
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
|
||||
// for details.
|
||||
it.skip('displays first section celebration modal', async () => {
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { firstSection: true } });
|
||||
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
|
||||
const { courseware, models } = testStore.getState();
|
||||
@@ -116,7 +132,11 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('displays weekly goal celebration modal', async () => {
|
||||
// This was passing when it shouldn't have been because of improper
|
||||
// waitFor use. With the React 18 upgrade it no longer improperly passes
|
||||
// so we are skipping it. See https://github.com/openedx/frontend-app-learning/issues/1669
|
||||
// for details.
|
||||
it.skip('displays weekly goal celebration modal', async () => {
|
||||
const courseHomeMetadata = Factory.build('courseHomeMetadata', { celebrations: { weeklyGoal: true } });
|
||||
const testStore = await initializeTestStore({ courseHomeMetadata }, false);
|
||||
const { courseware, models } = testStore.getState();
|
||||
@@ -136,18 +156,6 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('displays notification trigger and toggles active class on click', async () => {
|
||||
render(<Course {...mockData} />, { wrapWithRouter: true });
|
||||
|
||||
waitFor(() => {
|
||||
const notificationTrigger = screen.getByRole('button', { name: /Show notification tray/i });
|
||||
expect(notificationTrigger).toBeInTheDocument();
|
||||
expect(notificationTrigger.parentNode).not.toHaveClass('sidebar-active', { exact: true });
|
||||
fireEvent.click(notificationTrigger);
|
||||
expect(notificationTrigger.parentNode).toHaveClass('sidebar-active');
|
||||
});
|
||||
});
|
||||
|
||||
it('handles click to open/close discussions sidebar', async () => {
|
||||
await setupDiscussionSidebar();
|
||||
|
||||
@@ -202,7 +210,7 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
it('doesn\'t renders course breadcrumbs by default', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build(
|
||||
'block',
|
||||
@@ -210,7 +218,7 @@ describe('Course', () => {
|
||||
{ courseId: courseMetadata.id },
|
||||
));
|
||||
const testStore = await initializeTestStore({
|
||||
courseMetadata, unitBlocks, enableNavigationSidebar: { enable_navigation_sidebar: false },
|
||||
courseMetadata, unitBlocks,
|
||||
}, false);
|
||||
const { courseware, models } = testStore.getState();
|
||||
const { courseId, sequenceId } = courseware;
|
||||
@@ -226,10 +234,10 @@ describe('Course', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
|
||||
});
|
||||
// expect the section and sequence "titles" to be loaded in as breadcrumb labels.
|
||||
waitFor(() => {
|
||||
expect(screen.findByText(Object.values(models.sections)[0].title)).toBeInTheDocument();
|
||||
expect(screen.findByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
|
||||
// expect the section and sequence "titles" not to be loaded in as breadcrumb labels.
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(Object.values(models.sections)[0].title)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(Object.values(models.sequences)[0].title)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,28 +368,27 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('displays chat when screen is wide enough (browser)', async () => {
|
||||
it('displays learner tools when screen is wide enough (browser)', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
learning_assistant_enabled: true,
|
||||
enrollment: { mode: 'verified' },
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
const { courseware } = testStore.getState();
|
||||
const { courseware, models } = testStore.getState();
|
||||
const { courseId, sequenceId } = courseware;
|
||||
const testData = {
|
||||
...mockData,
|
||||
courseId,
|
||||
sequenceId,
|
||||
unitId: Object.values(models.units)[0].id,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
const chat = screen.queryByTestId(mockChatTestId);
|
||||
waitFor(() => expect(chat).toBeInTheDocument());
|
||||
const learnerTools = screen.queryByTestId(mockLearnerToolsTestId);
|
||||
await waitFor(() => expect(learnerTools).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('does not display chat when screen is too narrow (mobile)', async () => {
|
||||
it('does not display learner tools when screen is too narrow (mobile)', async () => {
|
||||
global.innerWidth = breakpoints.extraSmall.minWidth;
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
learning_assistant_enabled: true,
|
||||
enrollment: { mode: 'verified' },
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
@@ -393,7 +400,7 @@ describe('Course', () => {
|
||||
sequenceId,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
const chat = screen.queryByTestId(mockChatTestId);
|
||||
await expect(chat).not.toBeInTheDocument();
|
||||
const learnerTools = screen.queryByTestId(mockLearnerToolsTestId);
|
||||
await expect(learnerTools).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import React from 'react';
|
||||
import { screen, render } from '@testing-library/react';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useModel, useModels } from '../../generic/model-store';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
jest.mock('@edx/frontend-platform');
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
// Remove When Fully rolled out>>>
|
||||
jest.mock('../../generic/model-store');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
|
||||
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
|
||||
// ^^^^Remove When Fully rolled out
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
ReactComponent,
|
||||
}),
|
||||
Provider: ({ children }) => children,
|
||||
useSelector: () => 'loaded',
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
Link: jest.fn().mockImplementation(({ to, children }) => (
|
||||
<a href={to}>{children}</a>
|
||||
)),
|
||||
}));
|
||||
|
||||
useModels.mockImplementation((name) => {
|
||||
if (name === 'sections') {
|
||||
return [
|
||||
{
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
|
||||
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'],
|
||||
title: 'Introduction',
|
||||
},
|
||||
{
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'],
|
||||
title: 'Example Week 1: Getting Started',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
title: 'Lesson 1 - Getting Started',
|
||||
unitIds: [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
title: 'Homework - Question Styles',
|
||||
unitIds: [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
useModel.mockImplementation(() => ({
|
||||
sectionIds: ['block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations'],
|
||||
}));
|
||||
|
||||
describe('CourseBreadcrumbs', () => {
|
||||
jest.spyOn(React, 'useMemo').mockImplementation(() => [
|
||||
[
|
||||
{
|
||||
default: false,
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
|
||||
label: 'Introduction',
|
||||
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
label: 'Example Week 1: Getting Started',
|
||||
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations', label: "Lesson 2 - Let's Get Interactive!", default: true, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0',
|
||||
},
|
||||
{
|
||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e', label: 'Homework - Essays', default: false, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@fb79dcbad35b466a8c6364f8ffee9050',
|
||||
},
|
||||
],
|
||||
]);
|
||||
render(
|
||||
<IntlProvider>
|
||||
<BrowserRouter>
|
||||
<CourseBreadcrumbs
|
||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
||||
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
||||
isStaff
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
</IntlProvider>,
|
||||
);
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
||||
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
|
||||
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
|
||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
104
src/courseware/course/breadcrumbs/BreadcrumbItem.tsx
Normal file
104
src/courseware/course/breadcrumbs/BreadcrumbItem.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
useToggle,
|
||||
ModalPopup,
|
||||
Menu,
|
||||
Button,
|
||||
} from '@openedx/paragon';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import JumpNavMenuItem from '../JumpNavMenuItem';
|
||||
|
||||
interface Props {
|
||||
content: {
|
||||
default: boolean,
|
||||
id: string,
|
||||
label: string,
|
||||
sequences: {
|
||||
id: string,
|
||||
}[],
|
||||
} [];
|
||||
withSeparator: boolean | false,
|
||||
separator: string | '';
|
||||
courseId: string;
|
||||
sequenceId: string | '';
|
||||
unitId: string | '';
|
||||
isStaff: boolean | false;
|
||||
}
|
||||
|
||||
const BreadcrumbItem: React.FC<Props> = ({
|
||||
content,
|
||||
withSeparator,
|
||||
separator,
|
||||
courseId,
|
||||
sequenceId,
|
||||
unitId,
|
||||
isStaff,
|
||||
}) => {
|
||||
const defaultContent = content.filter(
|
||||
(destination: { default: boolean }) => destination.default,
|
||||
)[0] || { id: courseId, label: '', sequences: [] };
|
||||
|
||||
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const isPreview = pathname.startsWith('/preview');
|
||||
const baseUrl = defaultContent.sequences.length
|
||||
? `/course/${courseId}/${defaultContent.sequences[0].id}`
|
||||
: `/course/${courseId}/${defaultContent.id}`;
|
||||
const link = isPreview ? `/preview${baseUrl}` : baseUrl;
|
||||
return (
|
||||
<>
|
||||
{withSeparator && separator && (
|
||||
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>{separator}</li>
|
||||
)}
|
||||
|
||||
<li
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
data-testid="breadcrumb-item"
|
||||
>
|
||||
{showRegularLink ? (
|
||||
<Link
|
||||
className="text-primary-500"
|
||||
to={link}
|
||||
>
|
||||
{defaultContent.label}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
// @ts-ignore
|
||||
<Button className="text-primary-500 m-0 p-0" variant="link" onClick={open} ref={setTarget}>
|
||||
{defaultContent.label}
|
||||
</Button>
|
||||
}
|
||||
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
|
||||
<Menu>
|
||||
{content.map((item) => (
|
||||
<JumpNavMenuItem
|
||||
key={item.label}
|
||||
isDefault={item.default}
|
||||
sequences={item.sequences}
|
||||
courseId={courseId}
|
||||
title={item.label}
|
||||
currentSequence={sequenceId}
|
||||
currentUnit={unitId}
|
||||
onClick={close}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</ModalPopup>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BreadcrumbItem;
|
||||
@@ -1,107 +1,13 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useToggle, ModalPopup, Menu } from '@openedx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useModel, useModels } from '../../generic/model-store';
|
||||
import JumpNavMenuItem from './JumpNavMenuItem';
|
||||
|
||||
const CourseBreadcrumb = ({
|
||||
content,
|
||||
withSeparator,
|
||||
courseId,
|
||||
sequenceId,
|
||||
unitId,
|
||||
isStaff,
|
||||
}) => {
|
||||
const defaultContent = content.filter(
|
||||
(destination) => destination.default,
|
||||
)[0] || { id: courseId, label: '', sequences: [] };
|
||||
|
||||
const showRegularLink = getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff;
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
return (
|
||||
<>
|
||||
{withSeparator && (
|
||||
<li className="col-auto p-0 mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>/</li>
|
||||
)}
|
||||
|
||||
<li
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
data-testid="breadcrumb-item"
|
||||
>
|
||||
{showRegularLink ? (
|
||||
<Link
|
||||
className="text-primary-500"
|
||||
to={
|
||||
defaultContent.sequences.length
|
||||
? `/course/${courseId}/${defaultContent.sequences[0].id}`
|
||||
: `/course/${courseId}/${defaultContent.id}`
|
||||
}
|
||||
>
|
||||
{defaultContent.label}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
{
|
||||
// eslint-disable-next-line
|
||||
<a className="text-primary-500" onClick={open} ref={setTarget}>
|
||||
{defaultContent.label}
|
||||
</a>
|
||||
}
|
||||
<ModalPopup positionRef={target} isOpen={isOpen} onClose={close}>
|
||||
<Menu>
|
||||
{content.map((item) => (
|
||||
<JumpNavMenuItem
|
||||
key={item.label}
|
||||
isDefault={item.default}
|
||||
sequences={item.sequences}
|
||||
courseId={courseId}
|
||||
title={item.label}
|
||||
currentSequence={sequenceId}
|
||||
currentUnit={unitId}
|
||||
onClick={close}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</ModalPopup>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
};
|
||||
CourseBreadcrumb.propTypes = {
|
||||
content: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
default: PropTypes.bool,
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
sequenceId: PropTypes.string,
|
||||
unitId: PropTypes.string,
|
||||
withSeparator: PropTypes.bool,
|
||||
courseId: PropTypes.string,
|
||||
isStaff: PropTypes.bool,
|
||||
};
|
||||
|
||||
CourseBreadcrumb.defaultProps = {
|
||||
withSeparator: false,
|
||||
sequenceId: null,
|
||||
unitId: null,
|
||||
courseId: null,
|
||||
isStaff: null,
|
||||
};
|
||||
import { useModel, useModels } from '../../../generic/model-store';
|
||||
import BreadcrumbItem from './BreadcrumbItem';
|
||||
|
||||
const CourseBreadcrumbs = ({
|
||||
courseId,
|
||||
@@ -117,7 +23,7 @@ const CourseBreadcrumbs = ({
|
||||
);
|
||||
|
||||
const allSequencesInSections = Object.fromEntries(
|
||||
useModels('sections', course.sectionIds).map((section) => [
|
||||
useModels('sections', course.sectionIds)?.map((section) => [
|
||||
section.id,
|
||||
{
|
||||
default: section.id === sectionId,
|
||||
@@ -171,7 +77,7 @@ const CourseBreadcrumbs = ({
|
||||
</Link>
|
||||
</li>
|
||||
{links.map((content, i) => (
|
||||
<CourseBreadcrumb
|
||||
<BreadcrumbItem
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={i}
|
||||
courseId={courseId}
|
||||
@@ -179,6 +85,7 @@ const CourseBreadcrumbs = ({
|
||||
content={content}
|
||||
unitId={unitId}
|
||||
withSeparator
|
||||
separator="/"
|
||||
isStaff={isStaff}
|
||||
/>
|
||||
))}
|
||||
101
src/courseware/course/breadcrumbs/CourseBreadcrumbs.test.jsx
Normal file
101
src/courseware/course/breadcrumbs/CourseBreadcrumbs.test.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Factory } from 'rosie';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { initializeMockApp, initializeTestStore } from '@src/setupTest';
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
const props = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
||||
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
||||
isStaff: true,
|
||||
};
|
||||
|
||||
const courseMetadata = Factory.build('courseMetadata', { courseId: props.courseId, sectionIds: [props.sectionId] });
|
||||
const sequenceBlocks = [Factory.build(
|
||||
'block',
|
||||
{ type: 'sequential', id: props.sequenceId, title: 'Subsection' },
|
||||
{ courseId: props.courseId },
|
||||
)];
|
||||
const sectionBlocks = [Factory.build(
|
||||
'block',
|
||||
{
|
||||
type: 'chapter',
|
||||
id: props.sectionId,
|
||||
title: 'Section',
|
||||
children: [{ id: props.sequenceId }],
|
||||
},
|
||||
{ courseId: props.courseId },
|
||||
)];
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
describe('CourseBreadcrumbs', () => {
|
||||
let store = {};
|
||||
|
||||
const initTestStore = async () => {
|
||||
store = await initializeTestStore({ courseMetadata, sectionBlocks, sequenceBlocks });
|
||||
};
|
||||
|
||||
function renderWithProvider(pathname = '/course') {
|
||||
const { container } = render(
|
||||
<AppProvider store={store} wrapWithRouter={false}>
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter initialEntries={[{ pathname }]}>
|
||||
<CourseBreadcrumbs {...props} />
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
return container;
|
||||
}
|
||||
|
||||
describe('in live view', () => {
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider();
|
||||
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
|
||||
|
||||
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
|
||||
|
||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('section link does not include /preview/', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider();
|
||||
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
|
||||
const sectionLink = sectionBreadcrumb.closest('a').href;
|
||||
|
||||
expect(sectionLink.includes('/preview/')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('in live view', () => {
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider('/preview/courses');
|
||||
const courseHomeButtonDestination = screen.getAllByRole('link')[0].href;
|
||||
|
||||
expect(courseHomeButtonDestination).toBe('http://localhost/course/course-v1:edX+DemoX+Demo_Course/home');
|
||||
|
||||
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryAllByTestId('breadcrumb-item')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('section link does includes /preview/', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider('/preview/courses');
|
||||
const sectionBreadcrumb = screen.getByText(sectionBlocks[0].block_id);
|
||||
const sectionLink = sectionBreadcrumb.closest('a').href;
|
||||
|
||||
expect(sectionLink.includes('/preview/')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
3
src/courseware/course/breadcrumbs/index.js
Normal file
3
src/courseware/course/breadcrumbs/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||
|
||||
export default CourseBreadcrumbs;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
breakpoints,
|
||||
@@ -18,8 +18,9 @@ import { recordFirstSectionCelebration } from './utils';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const CelebrationModal = ({
|
||||
courseId, intl, isOpen, onClose, ...rest
|
||||
courseId, isOpen, onClose, ...rest
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { org, celebrations } = useModel('courseHomeMeta', courseId);
|
||||
const dispatch = useDispatch();
|
||||
const wideScreen = useWindowSize().width >= breakpoints.small.minWidth;
|
||||
@@ -66,9 +67,8 @@ const CelebrationModal = ({
|
||||
|
||||
CelebrationModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CelebrationModal);
|
||||
export default CelebrationModal;
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
justify-content: center;
|
||||
|
||||
button {
|
||||
@extend .btn-primary;
|
||||
font-size: 1.2rem;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, Button, Icon, StandardModal,
|
||||
} from '@openedx/paragon';
|
||||
@@ -12,8 +12,9 @@ import { recordWeeklyGoalCelebration } from './utils';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const WeeklyGoalCelebrationModal = ({
|
||||
courseId, daysPerWeek, intl, isOpen, onClose, ...rest
|
||||
courseId, daysPerWeek, isOpen, onClose, ...rest
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,9 +78,8 @@ const WeeklyGoalCelebrationModal = ({
|
||||
WeeklyGoalCelebrationModal.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
daysPerWeek: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(WeeklyGoalCelebrationModal);
|
||||
export default WeeklyGoalCelebrationModal;
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Xpert } from '@edx/frontend-lib-learning-assistant';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { ALLOW_UPSELL_MODES, VERIFIED_MODES } from '@src/constants';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const Chat = ({
|
||||
enabled,
|
||||
enrollmentMode,
|
||||
isStaff,
|
||||
courseId,
|
||||
contentToolsEnabled,
|
||||
unitId,
|
||||
}) => {
|
||||
const {
|
||||
activeAttempt, exam,
|
||||
} = useSelector(state => state.specialExams);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
|
||||
// If is disabled or taking an exam, we don't show the chat.
|
||||
if (!enabled || activeAttempt?.attempt_id || exam?.id) { return null; }
|
||||
|
||||
// If is not staff and doesn't have an enrollment, we don't show the chat.
|
||||
if (!isStaff && !enrollmentMode) { return null; }
|
||||
|
||||
const verifiedMode = VERIFIED_MODES.includes(enrollmentMode); // Enrollment verified
|
||||
const auditMode = (
|
||||
!isStaff
|
||||
&& !verifiedMode
|
||||
&& ALLOW_UPSELL_MODES.includes(enrollmentMode) // Can upgrade course
|
||||
&& getConfig().ENABLE_XPERT_AUDIT
|
||||
);
|
||||
// If user has no access, we don't show the chat.
|
||||
if (!isStaff && !(verifiedMode || auditMode)) { return null; }
|
||||
|
||||
// Date validation
|
||||
const {
|
||||
accessExpiration,
|
||||
start,
|
||||
end,
|
||||
} = course;
|
||||
|
||||
const utcDate = (new Date()).toISOString();
|
||||
const expiration = accessExpiration?.expirationDate || utcDate;
|
||||
const validDate = (
|
||||
(start ? start <= utcDate : true)
|
||||
&& (end ? end >= utcDate : true)
|
||||
&& (auditMode ? expiration >= utcDate : true)
|
||||
);
|
||||
// If date is invalid, we don't show the chat.
|
||||
if (!validDate) { return null; }
|
||||
|
||||
// Use a portal to ensure that component overlay does not compete with learning MFE styles.
|
||||
return createPortal(
|
||||
<Xpert
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={contentToolsEnabled}
|
||||
unitId={unitId}
|
||||
isUpgradeEligible={auditMode}
|
||||
/>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
Chat.propTypes = {
|
||||
isStaff: PropTypes.bool.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
enrollmentMode: PropTypes.string,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
contentToolsEnabled: PropTypes.bool.isRequired,
|
||||
unitId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Chat.defaultProps = {
|
||||
enrollmentMode: null,
|
||||
};
|
||||
|
||||
export default injectIntl(Chat);
|
||||
@@ -1,286 +0,0 @@
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import {
|
||||
initializeMockApp,
|
||||
initializeTestStore,
|
||||
render,
|
||||
screen,
|
||||
} from '../../../setupTest';
|
||||
|
||||
import Chat from './Chat';
|
||||
|
||||
// We do a partial mock to avoid mocking out other exported values (e.g. the reducer).
|
||||
// We mock out the Xpert component, because the Xpert component has its own rules for whether it renders
|
||||
// or not, and this includes the results of API calls it makes. We don't want to test those rules here, just
|
||||
// whether the Xpert is rendered by the Chat component in certain conditions. Instead of actually rendering
|
||||
// Xpert, we render and assert on a mocked component.
|
||||
const mockXpertTestId = 'xpert';
|
||||
|
||||
jest.mock('@edx/frontend-lib-learning-assistant', () => {
|
||||
const originalModule = jest.requireActual('@edx/frontend-lib-learning-assistant');
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
Xpert: () => (<div data-testid={mockXpertTestId}>mocked Xpert</div>),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn().mockReturnValue({ ENABLE_XPERT_AUDIT: false }),
|
||||
}));
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
let testCases = [];
|
||||
let enabledTestCases = [];
|
||||
let disabledTestCases = [];
|
||||
const enabledModes = [
|
||||
'professional', 'verified', 'no-id-professional', 'credit', 'masters', 'executive-education',
|
||||
'paid-executive-education', 'paid-bootcamp',
|
||||
];
|
||||
const disabledModes = [null, undefined, 'xyz', 'audit', 'honor', 'unpaid-executive-education', 'unpaid-bootcamp'];
|
||||
|
||||
describe('Chat', () => {
|
||||
let store;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = await initializeTestStore({
|
||||
specialExams: {
|
||||
activeAttempt: {
|
||||
attempt_id: null,
|
||||
},
|
||||
exam: {
|
||||
id: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Generate test cases.
|
||||
enabledTestCases = enabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: true }));
|
||||
disabledTestCases = disabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: false }));
|
||||
testCases = enabledTestCases.concat(disabledTestCases);
|
||||
|
||||
testCases.forEach(test => {
|
||||
it(
|
||||
`visibility determined by ${test.enrollmentMode} enrollment mode when enabled and not isStaff`,
|
||||
async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode={test.enrollmentMode}
|
||||
isStaff={false}
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
if (test.isVisible) {
|
||||
expect(chat).toBeInTheDocument();
|
||||
} else {
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Generate test cases.
|
||||
testCases = enabledModes.concat(disabledModes).map((mode) => ({ enrollmentMode: mode, isVisible: true }));
|
||||
testCases.forEach(test => {
|
||||
it('visibility determined by isStaff when enabled and any enrollment mode', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode={test.enrollmentMode}
|
||||
isStaff
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
if (test.isVisible) {
|
||||
expect(chat).toBeInTheDocument();
|
||||
} else {
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Generate the map function used for generating test cases by currying the map function.
|
||||
// In this test suite, visibility depends on whether the enrollment mode is a valid or invalid
|
||||
// enrollment mode for enabling the Chat when the user is not a staff member and the Chat is enabled. Instead of
|
||||
// defining two separate map functions that differ in only one case, curry the function.
|
||||
const generateMapFunction = (areEnabledModes) => (
|
||||
(mode) => (
|
||||
[
|
||||
{
|
||||
enrollmentMode: mode, isStaff: true, enabled: true, isVisible: true,
|
||||
},
|
||||
{
|
||||
enrollmentMode: mode, isStaff: true, enabled: false, isVisible: false,
|
||||
},
|
||||
{
|
||||
enrollmentMode: mode, isStaff: false, enabled: true, isVisible: areEnabledModes,
|
||||
},
|
||||
{
|
||||
enrollmentMode: mode, isStaff: false, enabled: false, isVisible: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// Generate test cases.
|
||||
enabledTestCases = enabledModes.map(generateMapFunction(true));
|
||||
disabledTestCases = disabledModes.map(generateMapFunction(false));
|
||||
testCases = enabledTestCases.concat(disabledTestCases);
|
||||
testCases = testCases.flat();
|
||||
testCases.forEach(test => {
|
||||
it(
|
||||
`visibility determined by ${test.enabled} enabled when ${test.isStaff} isStaff
|
||||
and ${test.enrollmentMode} enrollment mode`,
|
||||
async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode={test.enrollmentMode}
|
||||
isStaff={test.isStaff}
|
||||
enabled={test.enabled}
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
if (test.isVisible) {
|
||||
expect(chat).toBeInTheDocument();
|
||||
} else {
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('if course end date has passed, component should not be visible', async () => {
|
||||
store = await initializeTestStore({
|
||||
specialExams: {
|
||||
activeAttempt: {
|
||||
attempt_id: 1,
|
||||
},
|
||||
},
|
||||
courseMetadata: Factory.build('courseMetadata', {
|
||||
start: '2014-02-03T05:00:00Z',
|
||||
end: '2014-02-05T05:00:00Z',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="verified"
|
||||
isStaff
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('if learner has active exam attempt, component should not be visible', async () => {
|
||||
store = await initializeTestStore({
|
||||
specialExams: {
|
||||
activeAttempt: {
|
||||
attempt_id: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="verified"
|
||||
isStaff
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays component for audit learner if explicitly enabled', async () => {
|
||||
getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true }));
|
||||
|
||||
store = await initializeTestStore({
|
||||
courseMetadata: Factory.build('courseMetadata', {
|
||||
access_expiration: { expiration_date: '' },
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="audit"
|
||||
isStaff={false}
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display component for audit learner if access deadline has passed', async () => {
|
||||
getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true }));
|
||||
|
||||
store = await initializeTestStore({
|
||||
courseMetadata: Factory.build('courseMetadata', {
|
||||
access_expiration: { expiration_date: '2014-02-03T05:00:00Z' },
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="audit"
|
||||
isStaff={false}
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './Chat';
|
||||
@@ -1,403 +1,389 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faCalculator, faQuestionCircle, faTimesCircle, faEquals,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import messages from './messages';
|
||||
|
||||
class Calculator extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
equation: '',
|
||||
result: '',
|
||||
};
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
const Calculator = () => {
|
||||
const intl = useIntl();
|
||||
const [equation, setEquation] = useState('');
|
||||
const [result, setResult] = useState('');
|
||||
|
||||
async handleSubmit(event) {
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const urlEncoded = new URLSearchParams();
|
||||
urlEncoded.append('equation', this.state.equation);
|
||||
urlEncoded.append('equation', equation);
|
||||
|
||||
const response = await getAuthenticatedHttpClient().get(
|
||||
`${getConfig().LMS_BASE_URL}/calculate?${urlEncoded.toString()}`,
|
||||
);
|
||||
this.setState(() => ({ result: response.data.result }));
|
||||
}
|
||||
setResult(response.data.result);
|
||||
};
|
||||
|
||||
changeEquation(value) {
|
||||
this.setState(() => ({ equation: value }));
|
||||
}
|
||||
const changeEquation = (value) => {
|
||||
setEquation(value);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Collapsible.Advanced className="calculator">
|
||||
<div className="text-right">
|
||||
<Collapsible.Trigger tag="a" className="trigger btn">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faCalculator} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
{this.props.intl.formatMessage(messages['calculator.button.label'])}
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="calculator-content pt-4">
|
||||
<form onSubmit={this.handleSubmit} className="container-xl form-inline flex-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
aria-label={this.props.intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
className="form-control w-100"
|
||||
onChange={(event) => this.changeEquation(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary mx-3"
|
||||
aria-label={this.props.intl.formatMessage(messages['calculator.submit.button.label'])}
|
||||
type="submit"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEquals} aria-hidden="true" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
tabIndex="-1"
|
||||
readOnly
|
||||
aria-live="polite"
|
||||
placeholder={this.props.intl.formatMessage(messages['calculator.result.field.placeholder'])}
|
||||
aria-label={this.props.intl.formatMessage(messages['calculator.result.field.label'])}
|
||||
className="form-control w-50"
|
||||
value={this.state.result}
|
||||
/>
|
||||
</form>
|
||||
return (
|
||||
<Collapsible.Advanced className="calculator">
|
||||
<div className="text-right">
|
||||
<Collapsible.Trigger tag="a" className="trigger btn">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faCalculator} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
{intl.formatMessage(messages['calculator.button.label'])}
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="calculator-content pt-4">
|
||||
<form onSubmit={handleSubmit} className="container-xl form-inline flex-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
aria-label={intl.formatMessage(messages['calculator.input.field.label'])}
|
||||
className="form-control w-100"
|
||||
onChange={(event) => changeEquation(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary mx-3"
|
||||
aria-label={intl.formatMessage(messages['calculator.submit.button.label'])}
|
||||
type="submit"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEquals} aria-hidden="true" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
tabIndex="-1"
|
||||
readOnly
|
||||
aria-live="polite"
|
||||
placeholder={intl.formatMessage(messages['calculator.result.field.placeholder'])}
|
||||
aria-label={intl.formatMessage(messages['calculator.result.field.label'])}
|
||||
className="form-control w-50"
|
||||
value={result}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Collapsible.Advanced>
|
||||
<div className="container-xl">
|
||||
<Collapsible.Trigger className="btn btn-link btn-sm px-0 d-inline-flex align-items-center">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.button.label"
|
||||
defaultMessage="Calculator Instructions"
|
||||
/>
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="container-xl pt-3" style={{ maxHeight: '50vh', overflow: 'auto' }}>
|
||||
<Collapsible.Advanced>
|
||||
<div className="container-xl">
|
||||
<Collapsible.Trigger className="btn btn-link btn-sm px-0 d-inline-flex align-items-center">
|
||||
<Collapsible.Visible whenOpen>
|
||||
<FontAwesomeIcon icon={faTimesCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} aria-hidden="true" className="mr-2" />
|
||||
</Collapsible.Visible>
|
||||
<FormattedMessage
|
||||
tagName="h6"
|
||||
id="calculator.instructions"
|
||||
defaultMessage="For detailed information, see the {expressions_link}."
|
||||
description="Text that precedes the link which redirects to help page calculator"
|
||||
values={{
|
||||
expressions_link: (
|
||||
<a href={getConfig().SUPPORT_URL_CALCULATOR_MATH}>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.support.title"
|
||||
defaultMessage="Help Center"
|
||||
description="Anchor text for link which redirects to help page calculator"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
id="calculator.instructions.button.label"
|
||||
defaultMessage="Calculator Instructions"
|
||||
/>
|
||||
<p>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.useful.tips"
|
||||
defaultMessage="Useful tips:"
|
||||
description="Headline for the (list of tips) about using the calculator"
|
||||
/>
|
||||
</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li className="hint-item" id="hint-paren">
|
||||
<FormattedMessage
|
||||
id="calculator.hint1"
|
||||
defaultMessage="Use parentheses () to make expressions clear. You can use parentheses inside other parentheses."
|
||||
description="The text indicate that the calculator supports parentheses"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-spaces">
|
||||
<FormattedMessage
|
||||
id="calculator.hint2"
|
||||
defaultMessage="Do not use spaces in expressions."
|
||||
description="It indicate that using a space might cause un expected behavior"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-constants">
|
||||
<FormattedMessage
|
||||
id="calculator.hint3"
|
||||
defaultMessage="For constants, indicate multiplication explicitly (example: 5*c)."
|
||||
description="It indicate the style of math notation"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-maffixes">
|
||||
<FormattedMessage
|
||||
id="calculator.hint4"
|
||||
defaultMessage="For affixes, type the number and affix without a space (example: 5c)."
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-functions">
|
||||
<FormattedMessage
|
||||
id="calculator.hint5"
|
||||
defaultMessage="For functions, type the name of the function, then the expression in parentheses."
|
||||
description="It indicate how to use a math function, e.g. exp(4)."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<table className="table small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.heading"
|
||||
defaultMessage="To Use"
|
||||
description="Column header which indicate calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.type.heading"
|
||||
defaultMessage="Type"
|
||||
description="Column header which indicate the supported type(s) of a the calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.examples.heading"
|
||||
defaultMessage="Examples"
|
||||
description="Column header which list examples of calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers"
|
||||
defaultMessage="Numbers"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td>
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type1"
|
||||
defaultMessage="Integers"
|
||||
description="Type of numbers that is supported the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type2"
|
||||
defaultMessage="Fractions"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type3"
|
||||
defaultMessage="Decimals"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>2520</li>
|
||||
<li>2/3</li>
|
||||
<li>3.14, .98</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators"
|
||||
defaultMessage="Operators"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
{' + - * / '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type1"
|
||||
defaultMessage="(add, subtract, multiply, divide)"
|
||||
description="Type of opprators that are supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'^ '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type2"
|
||||
defaultMessage="(raise to a power)"
|
||||
description="It indicate that symbol (^) is being used to raise power, e.g. 2^2 = 4"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'|| '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type3"
|
||||
defaultMessage="(parallel resistors)"
|
||||
description="It indicate that the sympol (||) is being used to calculate (parallel resistor), it is a concept in electrical/electronic engineering"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>x+(2*y)/x-1</li>
|
||||
<li>x^(n+1)</li>
|
||||
<li>v_IN+v_OUT</li>
|
||||
<li>1||2</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.constants"
|
||||
defaultMessage="Constants"
|
||||
description="It indicate that the calculator support constants, e.g. the speed of light"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">e, pi</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20*e</li>
|
||||
<li>418*pi</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes"
|
||||
defaultMessage="Affixes"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes.type"
|
||||
defaultMessage="Percent sign (%)"
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20%</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.basic.functions"
|
||||
defaultMessage="Basic functions"
|
||||
description="It indicate that calculator supports mathematical function"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">abs, exp, fact, factorial, ln, log2, log10, sqrt</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>abs(x+y)</li>
|
||||
<li>sqrt(x^2-y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.trig.functions"
|
||||
defaultMessage="Trigonometric functions"
|
||||
description="Type of mathematical function that is supported by the calculator"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin, cos, tan, sec, csc, cot</li>
|
||||
<li>arcsin, sinh, arcsinh</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin(4x+y)</li>
|
||||
<li>arccsch(4x+y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto" />
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation"
|
||||
defaultMessage="Scientific notation"
|
||||
description="It indicate that calculator supports scientific notation"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type1"
|
||||
defaultMessage="{exponentSyntax} and the exponent"
|
||||
description="Type of scientific notation that is supported by the calculator"
|
||||
values={{
|
||||
exponentSyntax: '10^',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">10^-9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type2"
|
||||
defaultMessage="{notationSyntax} notation"
|
||||
description="It indicate that calculator supports (e) to be used in notation"
|
||||
values={{
|
||||
notationSyntax: 'e',
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type3"
|
||||
defaultMessage="{notationSyntax} and the exponent"
|
||||
description="An example for using (e) in notation"
|
||||
values={{
|
||||
notationSyntax: '1e',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">1e-9</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Calculator.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
</Collapsible.Trigger>
|
||||
</div>
|
||||
<Collapsible.Body className="container-xl pt-3" style={{ maxHeight: '50vh', overflow: 'auto' }}>
|
||||
<FormattedMessage
|
||||
tagName="h6"
|
||||
id="calculator.instructions"
|
||||
defaultMessage="For detailed information, see the {expressions_link}."
|
||||
description="Text that precedes the link which redirects to help page calculator"
|
||||
values={{
|
||||
expressions_link: (
|
||||
<a href={getConfig().SUPPORT_URL_CALCULATOR_MATH}>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.support.title"
|
||||
defaultMessage="Help Center"
|
||||
description="Anchor text for link which redirects to help page calculator"
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="calculator.instructions.useful.tips"
|
||||
defaultMessage="Useful tips:"
|
||||
description="Headline for the (list of tips) about using the calculator"
|
||||
/>
|
||||
</strong>
|
||||
</p>
|
||||
<ul>
|
||||
<li className="hint-item" id="hint-paren">
|
||||
<FormattedMessage
|
||||
id="calculator.hint1"
|
||||
defaultMessage="Use parentheses () to make expressions clear. You can use parentheses inside other parentheses."
|
||||
description="The text indicate that the calculator supports parentheses"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-spaces">
|
||||
<FormattedMessage
|
||||
id="calculator.hint2"
|
||||
defaultMessage="Do not use spaces in expressions."
|
||||
description="It indicate that using a space might cause un expected behavior"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-constants">
|
||||
<FormattedMessage
|
||||
id="calculator.hint3"
|
||||
defaultMessage="For constants, indicate multiplication explicitly (example: 5*c)."
|
||||
description="It indicate the style of math notation"
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-maffixes">
|
||||
<FormattedMessage
|
||||
id="calculator.hint4"
|
||||
defaultMessage="For affixes, type the number and affix without a space (example: 5c)."
|
||||
/>
|
||||
</li>
|
||||
<li className="hint-item" id="hint-howto-functions">
|
||||
<FormattedMessage
|
||||
id="calculator.hint5"
|
||||
defaultMessage="For functions, type the name of the function, then the expression in parentheses."
|
||||
description="It indicate how to use a math function, e.g. exp(4)."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<table className="pgn__data-table small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.heading"
|
||||
defaultMessage="To Use"
|
||||
description="Column header which indicate calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.type.heading"
|
||||
defaultMessage="Type"
|
||||
description="Column header which indicate the supported type(s) of a the calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<th scope="col">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.examples.heading"
|
||||
defaultMessage="Examples"
|
||||
description="Column header which list examples of calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers"
|
||||
defaultMessage="Numbers"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td>
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type1"
|
||||
defaultMessage="Integers"
|
||||
description="Type of numbers that is supported the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type2"
|
||||
defaultMessage="Fractions"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.numbers.type3"
|
||||
defaultMessage="Decimals"
|
||||
description="Type of numbers that is supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>2520</li>
|
||||
<li>2/3</li>
|
||||
<li>3.14, .98</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators"
|
||||
defaultMessage="Operators"
|
||||
description="A calculator functionality"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>
|
||||
{' + - * / '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type1"
|
||||
defaultMessage="(add, subtract, multiply, divide)"
|
||||
description="Type of opprators that are supported by the calculator"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'^ '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type2"
|
||||
defaultMessage="(raise to a power)"
|
||||
description="It indicate that symbol (^) is being used to raise power, e.g. 2^2 = 4"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
{'|| '}
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.operators.type3"
|
||||
defaultMessage="(parallel resistors)"
|
||||
description="It indicate that the sympol (||) is being used to calculate (parallel resistor), it is a concept in electrical/electronic engineering"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>x+(2*y)/x-1</li>
|
||||
<li>x^(n+1)</li>
|
||||
<li>v_IN+v_OUT</li>
|
||||
<li>1||2</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.constants"
|
||||
defaultMessage="Constants"
|
||||
description="It indicate that the calculator support constants, e.g. the speed of light"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">e, pi</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20*e</li>
|
||||
<li>418*pi</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes"
|
||||
defaultMessage="Affixes"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.affixes.type"
|
||||
defaultMessage="Percent sign (%)"
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>20%</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.basic.functions"
|
||||
defaultMessage="Basic functions"
|
||||
description="It indicate that calculator supports mathematical function"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">abs, exp, fact, factorial, ln, log2, log10, sqrt</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>abs(x+y)</li>
|
||||
<li>sqrt(x^2-y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.trig.functions"
|
||||
defaultMessage="Trigonometric functions"
|
||||
description="Type of mathematical function that is supported by the calculator"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin, cos, tan, sec, csc, cot</li>
|
||||
<li>arcsin, sinh, arcsinh</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td dir="auto">
|
||||
<ul className="list-unstyled m-0">
|
||||
<li>sin(4x+y)</li>
|
||||
<li>arccsch(4x+y)</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation"
|
||||
defaultMessage="Scientific notation"
|
||||
description="It indicate that calculator supports scientific notation"
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type1"
|
||||
defaultMessage="{exponentSyntax} and the exponent"
|
||||
description="Type of scientific notation that is supported by the calculator"
|
||||
values={{
|
||||
exponentSyntax: '10^',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">10^-9</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type2"
|
||||
defaultMessage="{notationSyntax} notation"
|
||||
description="It indicate that calculator supports (e) to be used in notation"
|
||||
values={{
|
||||
notationSyntax: 'e',
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<td dir="auto">
|
||||
<FormattedMessage
|
||||
id="calculator.instruction.table.to.use.scientific.notation.type3"
|
||||
defaultMessage="{notationSyntax} and the exponent"
|
||||
description="An example for using (e) in notation"
|
||||
values={{
|
||||
notationSyntax: '1e',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td dir="auto">1e-9</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(Calculator);
|
||||
export default Calculator;
|
||||
|
||||
@@ -4,4 +4,19 @@
|
||||
background-color: #f1f1f1;
|
||||
box-shadow: 0 -1px 0 0 #ddd;
|
||||
}
|
||||
|
||||
table {
|
||||
tr {
|
||||
border-bottom: var(--pgn-size-border-width) solid var(--pgn-color-border);
|
||||
}
|
||||
|
||||
thead tr {
|
||||
border-bottom: calc(2 * var(--pgn-size-border-width)) solid var(--pgn-color-border);
|
||||
border-top: var(--pgn-size-border-width) solid var(--pgn-color-border);
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background-color: #f1f1f1;
|
||||
border: solid 1px #ddd;
|
||||
background-color: #f1f1f1 !important;
|
||||
border: solid 1px #ddd !important;
|
||||
border-bottom: none;
|
||||
border-top-left-radius: .3rem;
|
||||
border-top-right-radius: .3rem;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import messages from './messages';
|
||||
@@ -15,47 +13,41 @@ function toggleNotes() {
|
||||
iframe.contentWindow.postMessage('tools.toggleNotes', getConfig().LMS_BASE_URL);
|
||||
}
|
||||
|
||||
class NotesVisibility extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: props.course.notes.visible,
|
||||
};
|
||||
this.visibilityUrl = `${getConfig().LMS_BASE_URL}/courses/${props.course.id}/edxnotes/visibility/`;
|
||||
}
|
||||
const NotesVisibility = ({ course }) => {
|
||||
const intl = useIntl();
|
||||
const [visible, setVisible] = useState(course.notes.visible);
|
||||
const visibilityUrl = `${getConfig().LMS_BASE_URL}/courses/${course.id}/edxnotes/visibility/`;
|
||||
|
||||
handleClick = () => {
|
||||
const data = { visibility: !this.state.visible };
|
||||
const handleClick = () => {
|
||||
const data = { visibility: !visible };
|
||||
getAuthenticatedHttpClient().put(
|
||||
this.visibilityUrl,
|
||||
visibilityUrl,
|
||||
data,
|
||||
).then(() => {
|
||||
this.setState((state) => ({ visible: !state.visible }));
|
||||
setVisible(!visible);
|
||||
toggleNotes();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const message = this.state.visible ? 'notes.button.hide' : 'notes.button.show';
|
||||
return (
|
||||
<button
|
||||
className={`trigger btn ${this.state.visible ? 'text-secondary' : 'text-success'} mx-2 `}
|
||||
role="switch"
|
||||
type="button"
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleClick}
|
||||
tabIndex="-1"
|
||||
aria-checked={this.state.visible ? 'true' : 'false'}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt} aria-hidden="true" className="mr-2" />
|
||||
{this.props.intl.formatMessage(messages[message])}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
const message = visible ? 'notes.button.hide' : 'notes.button.show';
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`trigger btn ${visible ? 'text-secondary' : 'text-success'} mx-2 `}
|
||||
role="switch"
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleClick}
|
||||
tabIndex="-1"
|
||||
aria-checked={visible ? 'true' : 'false'}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt} aria-hidden="true" className="mr-2" />
|
||||
{intl.formatMessage(messages[message])}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
NotesVisibility.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
course: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
notes: PropTypes.shape({
|
||||
@@ -64,4 +56,4 @@ NotesVisibility.propTypes = {
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(NotesVisibility);
|
||||
export default NotesVisibility;
|
||||
|
||||
@@ -4,9 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import {
|
||||
FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
@@ -16,7 +14,8 @@ import { useModel } from '../../../generic/model-store';
|
||||
import messages from './messages';
|
||||
import { logClick } from './utils';
|
||||
|
||||
const CatalogSuggestion = ({ intl, variant }) => {
|
||||
const CatalogSuggestion = ({ variant }) => {
|
||||
const intl = useIntl();
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const { org } = useModel('courseHomeMeta', courseId);
|
||||
const { administrator } = getAuthenticatedUser();
|
||||
@@ -48,8 +47,7 @@ const CatalogSuggestion = ({ intl, variant }) => {
|
||||
};
|
||||
|
||||
CatalogSuggestion.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
variant: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CatalogSuggestion);
|
||||
export default CatalogSuggestion;
|
||||
|
||||
@@ -2,9 +2,7 @@ import React, { useEffect } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLinkedinIn } from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
import {
|
||||
FormattedDate, FormattedMessage, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
@@ -20,23 +18,24 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import CelebrationMobile from './assets/celebration_456x328.gif';
|
||||
import CelebrationDesktop from './assets/celebration_750x540.gif';
|
||||
import certificate from '../../../generic/assets/edX_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/edX_locked_certificate.png';
|
||||
import certificate from '../../../generic/assets/openedx_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/openedx_locked_certificate.png';
|
||||
import { FormattedPricing } from '../../../generic/upgrade-button';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
import { requestCert } from '../../../course-home/data/thunks';
|
||||
import ProgramCompletion from './ProgramCompletion';
|
||||
import DashboardFootnote from './DashboardFootnote';
|
||||
import UpgradeFootnote from './UpgradeFootnote';
|
||||
import SocialIcons from '../../social-share/SocialIcons';
|
||||
import { logClick, logVisit } from './utils';
|
||||
import { DashboardLink, IdVerificationSupportLink, ProfileLink } from '../../../shared/links';
|
||||
import CourseRecommendationsSlot from '../../../plugin-slots/CourseRecommendationsSlot';
|
||||
import DashboardFootnote from './DashboardFootnote';
|
||||
import { CourseRecommendationsSlot } from '../../../plugin-slots/CourseExitPluginSlots';
|
||||
|
||||
const LINKEDIN_BLUE = '#2867B2';
|
||||
|
||||
const CourseCelebration = ({ intl }) => {
|
||||
const CourseCelebration = () => {
|
||||
const intl = useIntl();
|
||||
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||
const { courseId } = useSelector(state => state.courseware);
|
||||
const dispatch = useDispatch();
|
||||
@@ -364,8 +363,4 @@ const CourseCelebration = ({ intl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CourseCelebration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseCelebration);
|
||||
export default CourseCelebration;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user