Compare commits
29 Commits
open-relea
...
cdeery/AA-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c9899be5e | ||
|
|
93873cd845 | ||
|
|
3def37d28c | ||
|
|
d7e7b8c873 | ||
|
|
b401f7bb34 | ||
|
|
952d5d89e7 | ||
|
|
15d9c9f1bd | ||
|
|
b5e22c983d | ||
|
|
4d3b68d287 | ||
|
|
2607cec626 | ||
|
|
ec18af1ec0 | ||
|
|
ee62aab0e7 | ||
|
|
3cb55b320a | ||
|
|
d2e26bf8cf | ||
|
|
95fb4ec859 | ||
|
|
03f0ec4aec | ||
|
|
9dc447fa85 | ||
|
|
0286304c39 | ||
|
|
d88c4ade78 | ||
|
|
e3b8143677 | ||
|
|
09384b317e | ||
|
|
2172ea4041 | ||
|
|
7e3aee7147 | ||
|
|
4f187390a0 | ||
|
|
5b3053d3bb | ||
|
|
dae1e03e73 | ||
|
|
a32c79984d | ||
|
|
8d0596a32e | ||
|
|
91b1229a29 |
2
.env
2
.env
@@ -20,7 +20,6 @@ LOGO_URL=''
|
|||||||
LOGO_TRADEMARK_URL=''
|
LOGO_TRADEMARK_URL=''
|
||||||
LOGO_WHITE_URL=''
|
LOGO_WHITE_URL=''
|
||||||
FAVICON_URL=''
|
FAVICON_URL=''
|
||||||
LEGACY_THEME_NAME=''
|
|
||||||
MARKETING_SITE_BASE_URL=''
|
MARKETING_SITE_BASE_URL=''
|
||||||
ORDER_HISTORY_URL=''
|
ORDER_HISTORY_URL=''
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||||
@@ -39,4 +38,3 @@ TWITTER_URL=''
|
|||||||
USER_INFO_COOKIE_NAME=''
|
USER_INFO_COOKIE_NAME=''
|
||||||
SESSION_COOKIE_DOMAIN=''
|
SESSION_COOKIE_DOMAIN=''
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
|||||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
LEGACY_THEME_NAME=''
|
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||||
PORT=2000
|
PORT=2000
|
||||||
@@ -39,4 +38,3 @@ TWITTER_URL='https://twitter.com/edXOnline'
|
|||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
SESSION_COOKIE_DOMAIN='localhost'
|
SESSION_COOKIE_DOMAIN='localhost'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
|||||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
LEGACY_THEME_NAME=''
|
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||||
PORT=2000
|
PORT=2000
|
||||||
@@ -38,4 +37,3 @@ TWITTER_HASHTAG='myedxjourney'
|
|||||||
TWITTER_URL='https://twitter.com/edXOnline'
|
TWITTER_URL='https://twitter.com/edXOnline'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
ENABLE_JUMPNAV='true'
|
ENABLE_JUMPNAV='true'
|
||||||
ENABLE_NOTICES=''
|
|
||||||
|
|||||||
10
.github/workflows/commitlint.yml
vendored
10
.github/workflows/commitlint.yml
vendored
@@ -1,10 +0,0 @@
|
|||||||
# Run commitlint on the commit messages in a pull request.
|
|
||||||
|
|
||||||
name: Lint Commit Messages
|
|
||||||
|
|
||||||
on:
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
commitlint:
|
|
||||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
|
||||||
8009
package-lock.json
generated
8009
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -33,21 +33,20 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||||
"@edx/frontend-component-footer": "10.1.6",
|
"@edx/frontend-component-footer": "10.1.6",
|
||||||
"@edx/frontend-enterprise-utils": "1.1.1",
|
"@edx/frontend-enterprise-utils": "1.0.0",
|
||||||
"@edx/frontend-lib-special-exams": "1.14.1",
|
"@edx/frontend-lib-special-exams": "1.13.2",
|
||||||
"@edx/frontend-platform": "1.14.3",
|
"@edx/frontend-platform": "1.12.7",
|
||||||
"@edx/paragon": "16.19.0",
|
"@edx/paragon": "16.13.3",
|
||||||
"@edx/frontend-component-header": "^2.4.3",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"@fortawesome/react-fontawesome": "0.1.15",
|
"@fortawesome/react-fontawesome": "0.1.15",
|
||||||
"@pact-foundation/pact": "9.16.4",
|
"@pact-foundation/pact": "9.16.1",
|
||||||
"@reduxjs/toolkit": "1.6.2",
|
"@reduxjs/toolkit": "1.6.1",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"core-js": "3.18.3",
|
"core-js": "3.16.4",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "2.2.1",
|
||||||
"lodash.camelcase": "4.3.0",
|
"lodash.camelcase": "4.3.0",
|
||||||
"prop-types": "15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-redux": "7.2.5",
|
"react-redux": "7.2.5",
|
||||||
"react-router": "5.2.1",
|
"react-router": "5.2.1",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router-dom": "5.2.1",
|
||||||
"react-share": "4.4.0",
|
"react-share": "4.4.0",
|
||||||
"redux": "4.1.1",
|
"redux": "4.1.1",
|
||||||
"regenerator-runtime": "0.13.9",
|
"regenerator-runtime": "0.13.9",
|
||||||
@@ -65,17 +64,17 @@
|
|||||||
"util": "0.12.4"
|
"util": "0.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/frontend-build": "9.0.5",
|
"@edx/frontend-build": "8.0.4",
|
||||||
"@testing-library/dom": "7.16.3",
|
"@testing-library/dom": "7.16.3",
|
||||||
"@testing-library/jest-dom": "5.14.1",
|
"@testing-library/jest-dom": "5.14.1",
|
||||||
"@testing-library/react": "10.3.0",
|
"@testing-library/react": "10.3.0",
|
||||||
"@testing-library/user-event": "13.4.1",
|
"@testing-library/user-event": "13.2.1",
|
||||||
"axios-mock-adapter": "1.20.0",
|
"axios-mock-adapter": "1.20.0",
|
||||||
"codecov": "3.8.3",
|
"codecov": "3.8.3",
|
||||||
"es-check": "6.0.0",
|
"es-check": "6.0.0",
|
||||||
"glob": "7.2.0",
|
"glob": "7.1.7",
|
||||||
"husky": "7.0.2",
|
"husky": "7.0.2",
|
||||||
"jest": "27.2.5",
|
"jest": "27.0.6",
|
||||||
"jest-chain": "1.1.5",
|
"jest-chain": "1.1.5",
|
||||||
"reactifex": "1.1.1",
|
"reactifex": "1.1.1",
|
||||||
"rosie": "2.1.0"
|
"rosie": "2.1.0"
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-us">
|
<html lang="en-us">
|
||||||
<head>
|
<head>
|
||||||
|
<title>Course | <%= process.env.SITE_NAME %></title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||||
|
|
||||||
<% if (process.env.NEW_RELIC_ACCOUNT_ID) { %>
|
<% if (process.env.NEW_RELIC_ACCOUNT_ID) { %>
|
||||||
<!-- NewRelic agent 1209 -->
|
<!-- NewRelic agent 1209 -->
|
||||||
<script>
|
<script>
|
||||||
@@ -11,11 +16,6 @@
|
|||||||
</script>
|
</script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<title>Course | <%= process.env.SITE_NAME %></title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
|
||||||
|
|
||||||
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
|
<% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %>
|
||||||
<script src="https://www.edx.org/optimizelyjs/<%= htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
<script src="https://www.edx.org/optimizelyjs/<%= htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
} from '@edx/paragon';
|
} from '@edx/paragon';
|
||||||
import { Check, ArrowForward } from '@edx/paragon/icons';
|
import { Check, ArrowForward } from '@edx/paragon/icons';
|
||||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { sendActivationEmail } from '../../courseware/data';
|
import { sendActivationEmail } from '../../courseware/data';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
function AccountActivationAlert() {
|
function AccountActivationAlert({
|
||||||
|
intl,
|
||||||
|
}) {
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showSpinner, setShowSpinner] = useState(false);
|
const [showSpinner, setShowSpinner] = useState(false);
|
||||||
const [showCheck, setShowCheck] = useState(false);
|
const [showCheck, setShowCheck] = useState(false);
|
||||||
@@ -29,22 +32,12 @@ function AccountActivationAlert() {
|
|||||||
if (showAccountActivationAlert !== undefined) {
|
if (showAccountActivationAlert !== undefined) {
|
||||||
Cookies.remove('show-account-activation-popup', { path: '/', domain: process.env.SESSION_COOKIE_DOMAIN });
|
Cookies.remove('show-account-activation-popup', { path: '/', domain: process.env.SESSION_COOKIE_DOMAIN });
|
||||||
// extra check to make sure cookie was removed before updating the state. Updating the state without removal
|
// extra check to make sure cookie was removed before updating the state. Updating the state without removal
|
||||||
// of cookie would make it infinit rendering
|
// of cookie would make it infinite rendering
|
||||||
if (Cookies.get('show-account-activation-popup') === undefined) {
|
if (Cookies.get('show-account-activation-popup') === undefined) {
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = (
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage
|
|
||||||
id="account-activation.alert.title"
|
|
||||||
defaultMessage="Activate your account so you can log back in"
|
|
||||||
description="Title for account activation alert which is shown after the registration"
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -64,7 +57,7 @@ function AccountActivationAlert() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const children = () => {
|
const children = () => {
|
||||||
let bodyContent = null;
|
let bodyContent;
|
||||||
const message = (
|
const message = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="account-activation.alert.message"
|
id="account-activation.alert.message"
|
||||||
@@ -123,7 +116,7 @@ function AccountActivationAlert() {
|
|||||||
return (
|
return (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={showModal}
|
isOpen={showModal}
|
||||||
title={title}
|
title={intl.formatMessage(messages.accountActivationAlertTitle)}
|
||||||
footerNode={button}
|
footerNode={button}
|
||||||
onClose={() => ({})}
|
onClose={() => ({})}
|
||||||
>
|
>
|
||||||
@@ -132,4 +125,8 @@ function AccountActivationAlert() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AccountActivationAlert.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default injectIntl(AccountActivationAlert);
|
export default injectIntl(AccountActivationAlert);
|
||||||
|
|||||||
11
src/alerts/logistration-alert/messages.js
Normal file
11
src/alerts/logistration-alert/messages.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
accountActivationAlertTitle: {
|
||||||
|
id: 'account-activation.alert.title',
|
||||||
|
defaultMessage: 'Activate your account so you can log back in',
|
||||||
|
description: 'Title for account activation alert which is shown after the registration',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
34
src/course-header/AnonymousUserMenu.jsx
Normal file
34
src/course-header/AnonymousUserMenu.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Button } from '@edx/paragon';
|
||||||
|
|
||||||
|
import genericMessages from '../generic/messages';
|
||||||
|
|
||||||
|
function AnonymousUserMenu({ intl }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="mr-3"
|
||||||
|
variant="outline-primary"
|
||||||
|
href={`${getConfig().LMS_BASE_URL}/register?next=${encodeURIComponent(global.location.href)}`}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(genericMessages.registerSentenceCase)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
href={`${getLoginRedirectUrl(global.location.href)}`}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(genericMessages.signInSentenceCase)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnonymousUserMenu.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(AnonymousUserMenu);
|
||||||
76
src/course-header/AuthenticatedUserDropdown.jsx
Normal file
76
src/course-header/AuthenticatedUserDropdown.jsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { Dropdown } from '@edx/paragon';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function AuthenticatedUserDropdown({ enterpriseLearnerPortalLink, intl, username }) {
|
||||||
|
let dashboardMenuItem = (
|
||||||
|
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||||
|
{intl.formatMessage(messages.dashboard)}
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
if (enterpriseLearnerPortalLink && Object.keys(enterpriseLearnerPortalLink).length > 0) {
|
||||||
|
dashboardMenuItem = (
|
||||||
|
<Dropdown.Item href={enterpriseLearnerPortalLink.href}>
|
||||||
|
{enterpriseLearnerPortalLink.content}
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>{intl.formatMessage(messages.help)}</a>
|
||||||
|
<Dropdown className="user-dropdown">
|
||||||
|
<Dropdown.Toggle variant="outline-primary">
|
||||||
|
<FontAwesomeIcon icon={faUserCircle} className="d-md-none" size="lg" />
|
||||||
|
<span data-hj-suppress className="d-none d-md-inline">
|
||||||
|
{username}
|
||||||
|
</span>
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu className="dropdown-menu-right">
|
||||||
|
{dashboardMenuItem}
|
||||||
|
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
|
||||||
|
{intl.formatMessage(messages.profile)}
|
||||||
|
</Dropdown.Item>
|
||||||
|
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||||
|
{intl.formatMessage(messages.account)}
|
||||||
|
</Dropdown.Item>
|
||||||
|
{!enterpriseLearnerPortalLink && (
|
||||||
|
// Users should only see Order History if they do not have an available
|
||||||
|
// learner portal, because an available learner portal currently means
|
||||||
|
// that they access content via Subscriptions, in which context an "order"
|
||||||
|
// is not relevant.
|
||||||
|
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||||
|
{intl.formatMessage(messages.orderHistory)}
|
||||||
|
</Dropdown.Item>
|
||||||
|
)}
|
||||||
|
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||||
|
{intl.formatMessage(messages.signOut)}
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticatedUserDropdown.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
enterpriseLearnerPortalLink: PropTypes.shape({
|
||||||
|
type: PropTypes.string,
|
||||||
|
href: PropTypes.string,
|
||||||
|
content: PropTypes.string,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
AuthenticatedUserDropdown.defaultProps = {
|
||||||
|
enterpriseLearnerPortalLink: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(AuthenticatedUserDropdown);
|
||||||
99
src/course-header/Header.jsx
Normal file
99
src/course-header/Header.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useEnterpriseConfig } from '@edx/frontend-enterprise-utils';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
import { AppContext } from '@edx/frontend-platform/react';
|
||||||
|
|
||||||
|
import AnonymousUserMenu from './AnonymousUserMenu';
|
||||||
|
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
function LinkedLogo({
|
||||||
|
href,
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
...attributes
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<a href={href} {...attributes}>
|
||||||
|
<img className="d-block" src={src} alt={alt} />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedLogo.propTypes = {
|
||||||
|
href: PropTypes.string.isRequired,
|
||||||
|
src: PropTypes.string.isRequired,
|
||||||
|
alt: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function Header({
|
||||||
|
courseOrg, courseNumber, courseTitle, intl, showUserDropdown,
|
||||||
|
}) {
|
||||||
|
const { authenticatedUser } = useContext(AppContext);
|
||||||
|
|
||||||
|
const { enterpriseLearnerPortalLink, enterpriseCustomerBrandingConfig } = useEnterpriseConfig(
|
||||||
|
authenticatedUser,
|
||||||
|
getConfig().ENTERPRISE_LEARNER_PORTAL_HOSTNAME,
|
||||||
|
getConfig().LMS_BASE_URL,
|
||||||
|
);
|
||||||
|
|
||||||
|
let headerLogo = (
|
||||||
|
<LinkedLogo
|
||||||
|
className="logo"
|
||||||
|
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||||
|
src={getConfig().LOGO_URL}
|
||||||
|
alt={getConfig().SITE_NAME}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
if (enterpriseCustomerBrandingConfig && Object.keys(enterpriseCustomerBrandingConfig).length > 0) {
|
||||||
|
headerLogo = (
|
||||||
|
<LinkedLogo
|
||||||
|
className="logo"
|
||||||
|
href={enterpriseCustomerBrandingConfig.logoDestination}
|
||||||
|
src={enterpriseCustomerBrandingConfig.logo}
|
||||||
|
alt={enterpriseCustomerBrandingConfig.logoAltText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="course-header">
|
||||||
|
<a className="sr-only sr-only-focusable" href="#main-content">{intl.formatMessage(messages.skipNavLink)}</a>
|
||||||
|
<div className="container-xl py-2 d-flex align-items-center">
|
||||||
|
{headerLogo}
|
||||||
|
<div className="flex-grow-1 course-title-lockup" style={{ lineHeight: 1 }}>
|
||||||
|
<span className="d-block small m-0">{courseOrg} {courseNumber}</span>
|
||||||
|
<span className="d-block m-0 font-weight-bold course-title">{courseTitle}</span>
|
||||||
|
</div>
|
||||||
|
{showUserDropdown && authenticatedUser && (
|
||||||
|
<AuthenticatedUserDropdown
|
||||||
|
enterpriseLearnerPortalLink={enterpriseLearnerPortalLink}
|
||||||
|
username={authenticatedUser.username}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showUserDropdown && !authenticatedUser && (
|
||||||
|
<AnonymousUserMenu />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Header.propTypes = {
|
||||||
|
courseOrg: PropTypes.string,
|
||||||
|
courseNumber: PropTypes.string,
|
||||||
|
courseTitle: PropTypes.string,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
showUserDropdown: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
Header.defaultProps = {
|
||||||
|
courseOrg: null,
|
||||||
|
courseNumber: null,
|
||||||
|
courseTitle: null,
|
||||||
|
showUserDropdown: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(Header);
|
||||||
29
src/course-header/Header.test.jsx
Normal file
29
src/course-header/Header.test.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
authenticatedUser, initializeMockApp, render, screen,
|
||||||
|
} from '../setupTest';
|
||||||
|
import { Header } from './index';
|
||||||
|
|
||||||
|
describe('Header', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
// We need to mock AuthService to implicitly use `getAuthenticatedUser` within `AppContext.Provider`.
|
||||||
|
await initializeMockApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays user button', () => {
|
||||||
|
render(<Header />);
|
||||||
|
expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays course data', () => {
|
||||||
|
const courseData = {
|
||||||
|
courseOrg: 'course-org',
|
||||||
|
courseNumber: 'course-number',
|
||||||
|
courseTitle: 'course-title',
|
||||||
|
};
|
||||||
|
render(<Header {...courseData} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(`${courseData.courseOrg} ${courseData.courseNumber}`)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(courseData.courseTitle)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
/* eslint-disable import/prefer-default-export */
|
export { default as Header } from './Header';
|
||||||
export { default as CourseTabsNavigation } from './CourseTabsNavigation';
|
export { default as CourseTabsNavigation } from './CourseTabsNavigation';
|
||||||
46
src/course-header/messages.js
Normal file
46
src/course-header/messages.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
courseMaterial: {
|
||||||
|
id: 'learn.navigation.course.tabs.label',
|
||||||
|
defaultMessage: 'Course Material',
|
||||||
|
description: 'The accessible label for course tabs navigation',
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
id: 'header.menu.dashboard.label',
|
||||||
|
defaultMessage: 'Dashboard',
|
||||||
|
description: 'The text for the user menu Dashboard navigation link.',
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
id: 'header.help.label',
|
||||||
|
defaultMessage: 'Help',
|
||||||
|
description: 'The text for the link to the Help Center',
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
id: 'header.menu.profile.label',
|
||||||
|
defaultMessage: 'Profile',
|
||||||
|
description: 'The text for the user menu Profile navigation link.',
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
id: 'header.menu.account.label',
|
||||||
|
defaultMessage: 'Account',
|
||||||
|
description: 'The text for the user menu Account navigation link.',
|
||||||
|
},
|
||||||
|
orderHistory: {
|
||||||
|
id: 'header.menu.orderHistory.label',
|
||||||
|
defaultMessage: 'Order History',
|
||||||
|
description: 'The text for the user menu Order History navigation link.',
|
||||||
|
},
|
||||||
|
skipNavLink: {
|
||||||
|
id: 'header.navigation.skipNavLink',
|
||||||
|
defaultMessage: 'Skip to main content.',
|
||||||
|
description: 'A link used by screen readers to allow users to skip to the main content of the page.',
|
||||||
|
},
|
||||||
|
signOut: {
|
||||||
|
id: 'header.menu.signOut.label',
|
||||||
|
defaultMessage: 'Sign Out',
|
||||||
|
description: 'The label for the user menu Sign Out action.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -40,6 +40,9 @@ Factory.define('outlineTabData')
|
|||||||
course_goals: {
|
course_goals: {
|
||||||
goal_options: [],
|
goal_options: [],
|
||||||
selected_goal: null,
|
selected_goal: null,
|
||||||
|
number_of_days_goals_enabled: false,
|
||||||
|
days_per_week: null,
|
||||||
|
subscribed_to_reminders: null,
|
||||||
},
|
},
|
||||||
course_tools: [
|
course_tools: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -432,8 +432,11 @@ Object {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"courseGoals": Object {
|
"courseGoals": Object {
|
||||||
|
"daysPerWeek": null,
|
||||||
"goalOptions": Array [],
|
"goalOptions": Array [],
|
||||||
|
"numberOfDaysGoalsEnabled": false,
|
||||||
"selectedGoal": null,
|
"selectedGoal": null,
|
||||||
|
"subscribedToReminders": null,
|
||||||
},
|
},
|
||||||
"courseTools": Array [
|
"courseTools": Array [
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -391,6 +391,15 @@ export async function postCourseGoals(courseId, goalKey) {
|
|||||||
return getAuthenticatedHttpClient().post(url.href, { course_id: courseId, goal_key: goalKey });
|
return getAuthenticatedHttpClient().post(url.href, { course_id: courseId, goal_key: goalKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postWeeklyCourseGoals(courseId, daysPerWeek, subscribedToReminders) {
|
||||||
|
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`);
|
||||||
|
return getAuthenticatedHttpClient().post(url.href, {
|
||||||
|
course_id: courseId,
|
||||||
|
days_per_week: daysPerWeek,
|
||||||
|
subscribed_to_reminders: subscribedToReminders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function postDismissWelcomeMessage(courseId) {
|
export async function postDismissWelcomeMessage(courseId) {
|
||||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`);
|
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/dismiss_welcome_message`);
|
||||||
await getAuthenticatedHttpClient().post(url.href, { course_id: courseId });
|
await getAuthenticatedHttpClient().post(url.href, { course_id: courseId });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export {
|
|||||||
fetchProgressTab,
|
fetchProgressTab,
|
||||||
resetDeadlines,
|
resetDeadlines,
|
||||||
saveCourseGoal,
|
saveCourseGoal,
|
||||||
|
saveWeeklyCourseGoal,
|
||||||
} from './thunks';
|
} from './thunks';
|
||||||
|
|
||||||
export { reducer } from './slice';
|
export { reducer } from './slice';
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getProgressTabData,
|
getProgressTabData,
|
||||||
postCourseDeadlines,
|
postCourseDeadlines,
|
||||||
postCourseGoals,
|
postCourseGoals,
|
||||||
|
postWeeklyCourseGoals,
|
||||||
postDismissWelcomeMessage,
|
postDismissWelcomeMessage,
|
||||||
postRequestCert,
|
postRequestCert,
|
||||||
} from './api';
|
} from './api';
|
||||||
@@ -113,6 +114,10 @@ export async function saveCourseGoal(courseId, goalKey) {
|
|||||||
return postCourseGoals(courseId, goalKey);
|
return postCourseGoals(courseId, goalKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveWeeklyCourseGoal(courseId, daysPerWeek, subscribedToReminders) {
|
||||||
|
return postWeeklyCourseGoals(courseId, daysPerWeek, subscribedToReminders);
|
||||||
|
}
|
||||||
|
|
||||||
export function processEvent(eventData, getTabData) {
|
export function processEvent(eventData, getTabData) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
// Pulling this out early so the data doesn't get camelCased and is easier
|
// Pulling this out early so the data doesn't get camelCased and is easier
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
import { Header } from '../../course-header';
|
||||||
import PageLoading from '../../generic/PageLoading';
|
import PageLoading from '../../generic/PageLoading';
|
||||||
import { unsubscribeFromCourseGoal } from '../data/api';
|
import { unsubscribeFromCourseGoal } from '../data/api';
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function LmsHtmlFragment({
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
<base href="${getConfig().LMS_BASE_URL}" target="_parent">
|
||||||
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
|
<link rel="stylesheet" href="/static/css/bootstrap/lms-main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="${className}">${html}</body>
|
<body class="${className}">${html}</body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { sendTrackEvent, sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
|
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
@@ -10,6 +10,8 @@ import { AlertList } from '../../generic/user-messages';
|
|||||||
import CourseDates from './widgets/CourseDates';
|
import CourseDates from './widgets/CourseDates';
|
||||||
import CourseGoalCard from './widgets/CourseGoalCard';
|
import CourseGoalCard from './widgets/CourseGoalCard';
|
||||||
import CourseHandouts from './widgets/CourseHandouts';
|
import CourseHandouts from './widgets/CourseHandouts';
|
||||||
|
import StartOrResumeCourseCard from './widgets/StartOrResumeCourseCard';
|
||||||
|
import WeeklyLearningGoalCard from './widgets/WeeklyLearningGoalCard';
|
||||||
import CourseTools from './widgets/CourseTools';
|
import CourseTools from './widgets/CourseTools';
|
||||||
import { fetchOutlineTab } from '../data';
|
import { fetchOutlineTab } from '../data';
|
||||||
import genericMessages from '../../generic/messages';
|
import genericMessages from '../../generic/messages';
|
||||||
@@ -54,13 +56,13 @@ function OutlineTab({ intl }) {
|
|||||||
courseGoals: {
|
courseGoals: {
|
||||||
goalOptions,
|
goalOptions,
|
||||||
selectedGoal,
|
selectedGoal,
|
||||||
|
weeklyLearningGoalEnabled,
|
||||||
} = {},
|
} = {},
|
||||||
datesBannerInfo,
|
datesBannerInfo,
|
||||||
datesWidget: {
|
datesWidget: {
|
||||||
courseDateBlocks,
|
courseDateBlocks,
|
||||||
},
|
},
|
||||||
resumeCourse: {
|
resumeCourse: {
|
||||||
hasVisitedCourse,
|
|
||||||
url: resumeCourseUrl,
|
url: resumeCourseUrl,
|
||||||
},
|
},
|
||||||
offer,
|
offer,
|
||||||
@@ -68,7 +70,7 @@ function OutlineTab({ intl }) {
|
|||||||
verifiedMode,
|
verifiedMode,
|
||||||
} = useModel('outline', courseId);
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
const [courseGoalToDisplay, setCourseGoalToDisplay] = useState(selectedGoal);
|
const [deprecatedCourseGoalToDisplay, setDeprecatedCourseGoalToDisplay] = useState(selectedGoal);
|
||||||
const [goalToastHeader, setGoalToastHeader] = useState('');
|
const [goalToastHeader, setGoalToastHeader] = useState('');
|
||||||
const [expandAll, setExpandAll] = useState(false);
|
const [expandAll, setExpandAll] = useState(false);
|
||||||
|
|
||||||
@@ -77,14 +79,6 @@ function OutlineTab({ intl }) {
|
|||||||
courserun_key: courseId,
|
courserun_key: courseId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const logResumeCourseClick = () => {
|
|
||||||
sendTrackingLogEvent('edx.course.home.resume_course.clicked', {
|
|
||||||
...eventProperties,
|
|
||||||
event_type: hasVisitedCourse ? 'resume' : 'start',
|
|
||||||
url: resumeCourseUrl,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Below the course title alerts (appearing in the order listed here)
|
// Below the course title alerts (appearing in the order listed here)
|
||||||
const courseStartAlert = useCourseStartAlert(courseId);
|
const courseStartAlert = useCourseStartAlert(courseId);
|
||||||
const courseEndAlert = useCourseEndAlert(courseId);
|
const courseEndAlert = useCourseEndAlert(courseId);
|
||||||
@@ -132,13 +126,6 @@ function OutlineTab({ intl }) {
|
|||||||
<div className="col-12 col-sm-auto p-0">
|
<div className="col-12 col-sm-auto p-0">
|
||||||
<div role="heading" aria-level="1" className="h2">{title}</div>
|
<div role="heading" aria-level="1" className="h2">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
{resumeCourseUrl && (
|
|
||||||
<div className="col-12 col-sm-auto p-0">
|
|
||||||
<Button variant="brand" block href={resumeCourseUrl} onClick={() => logResumeCourseClick()}>
|
|
||||||
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
|
{/** [MM-P2P] Experiment (className for optimizely trigger) */}
|
||||||
<div className="row course-outline-tab">
|
<div className="row course-outline-tab">
|
||||||
@@ -172,15 +159,18 @@ function OutlineTab({ intl }) {
|
|||||||
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
|
<UpgradeToShiftDatesAlert model="outline" logUpgradeLinkClick={logUpgradeToShiftDatesLinkClick} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!courseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
{!deprecatedCourseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
||||||
<CourseGoalCard
|
<CourseGoalCard
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
goalOptions={goalOptions}
|
goalOptions={goalOptions}
|
||||||
title={title}
|
title={title}
|
||||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
setGoalToDisplay={(newGoal) => { setDeprecatedCourseGoalToDisplay(newGoal); }}
|
||||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{resumeCourseUrl && (
|
||||||
|
<StartOrResumeCourseCard />
|
||||||
|
)}
|
||||||
<WelcomeMessage courseId={courseId} />
|
<WelcomeMessage courseId={courseId} />
|
||||||
{rootCourseId && (
|
{rootCourseId && (
|
||||||
<>
|
<>
|
||||||
@@ -211,15 +201,22 @@ function OutlineTab({ intl }) {
|
|||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
username={username}
|
username={username}
|
||||||
/>
|
/>
|
||||||
{courseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
{deprecatedCourseGoalToDisplay && goalOptions && goalOptions.length > 0 && (
|
||||||
<UpdateGoalSelector
|
<UpdateGoalSelector
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
goalOptions={goalOptions}
|
goalOptions={goalOptions}
|
||||||
selectedGoal={courseGoalToDisplay}
|
selectedGoal={deprecatedCourseGoalToDisplay}
|
||||||
setGoalToDisplay={(newGoal) => { setCourseGoalToDisplay(newGoal); }}
|
setGoalToDisplay={(newGoal) => { setDeprecatedCourseGoalToDisplay(newGoal); }}
|
||||||
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
setGoalToastHeader={(newHeader) => { setGoalToastHeader(newHeader); }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{weeklyLearningGoalEnabled && (
|
||||||
|
<WeeklyLearningGoalCard
|
||||||
|
daysPerWeek={selectedGoal && 'daysPerWeek' in selectedGoal ? selectedGoal.daysPerWeek : null}
|
||||||
|
subscribedToReminders={selectedGoal && 'subscribedToReminders' in selectedGoal ? selectedGoal.subscribedToReminders : false}
|
||||||
|
courseId={courseId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<CourseTools
|
<CourseTools
|
||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|||||||
import MockAdapter from 'axios-mock-adapter';
|
import MockAdapter from 'axios-mock-adapter';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory';
|
import { buildMinimalCourseBlocks } from '../../shared/data/__factories__/courseBlocks.factory';
|
||||||
import {
|
import {
|
||||||
@@ -413,6 +414,68 @@ describe('Outline Tab', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Weekly Learning Goals', () => {
|
||||||
|
it('does not render weekly learning goal if weeklyLearningGoalEnabled is false', async () => {
|
||||||
|
await fetchAndRender();
|
||||||
|
expect(screen.queryByTestId('weekly-learning-goal-card')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('weekly learning goal is not set', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
setTabData({
|
||||||
|
course_goals: {
|
||||||
|
weekly_learning_goal_enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fetchAndRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders weekly learning goal card', async () => {
|
||||||
|
expect(screen.queryByTestId('weekly-learning-goal-card')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders startOrResumeCourseCard', async () => {
|
||||||
|
expect(screen.queryByTestId('start-resume-card')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables the subscribe button if no goal is set', async () => {
|
||||||
|
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the API when a button is clicked', async () => {
|
||||||
|
expect(screen.queryByText(messages.casualGoalButtonText.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.casualGoalButtonText.defaultMessage).closest('button')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// click on Casual goal
|
||||||
|
const button = await screen.getByText(messages.casualGoalButtonText.defaultMessage).closest('button');
|
||||||
|
fireEvent.click(button);
|
||||||
|
// Verify the request was made
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(axiosMock.history.post[0].url).toMatch(goalUrl);
|
||||||
|
// subscribe is turned on automatically
|
||||||
|
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","days_per_week":3,"subscribed_to_reminders":true}`);
|
||||||
|
// verify that the additional info about subscriptions shows up
|
||||||
|
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByLabelText(messages.setGoalReminder.defaultMessage)).toBeEnabled();
|
||||||
|
|
||||||
|
// Click on subscribe to reminders
|
||||||
|
const subscriptionSwitch = await screen.getByRole('switch', { name: messages.setGoalReminder.defaultMessage });
|
||||||
|
expect(subscriptionSwitch).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(subscriptionSwitch);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(axiosMock.history.post[1].url).toMatch(goalUrl);
|
||||||
|
expect(axiosMock.history.post[1].data)
|
||||||
|
.toMatch(`{"course_id":"${courseId}","days_per_week":3,"subscribed_to_reminders":false}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify that the additional info about subscriptions gets hidden
|
||||||
|
expect(screen.queryByText(messages.goalReminderDetail.defaultMessage)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Course Handouts', () => {
|
describe('Course Handouts', () => {
|
||||||
it('renders title when handouts are available', async () => {
|
it('renders title when handouts are available', async () => {
|
||||||
await fetchAndRender();
|
await fetchAndRender();
|
||||||
|
|||||||
@@ -66,6 +66,15 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Open',
|
defaultMessage: 'Open',
|
||||||
description: 'A button to open the given section of the course outline',
|
description: 'A button to open the given section of the course outline',
|
||||||
},
|
},
|
||||||
|
startBlurb: {
|
||||||
|
id: 'learning.outline.startBlurb',
|
||||||
|
defaultMessage: 'Begin your course today',
|
||||||
|
},
|
||||||
|
resumeBlurb: {
|
||||||
|
id: 'learning.outline.resumeBlurb',
|
||||||
|
defaultMessage: 'Pick up where you left off',
|
||||||
|
description: 'Text describing to the learner that they can return to the last section they visited within the course.',
|
||||||
|
},
|
||||||
resume: {
|
resume: {
|
||||||
id: 'learning.outline.resume',
|
id: 'learning.outline.resume',
|
||||||
defaultMessage: 'Resume course',
|
defaultMessage: 'Resume course',
|
||||||
@@ -74,6 +83,14 @@ const messages = defineMessages({
|
|||||||
id: 'learning.outline.setGoal',
|
id: 'learning.outline.setGoal',
|
||||||
defaultMessage: 'To start, set a course goal by selecting the option below that best describes your learning plan.',
|
defaultMessage: 'To start, set a course goal by selecting the option below that best describes your learning plan.',
|
||||||
},
|
},
|
||||||
|
setWeeklyGoal: {
|
||||||
|
id: 'learning.outline.setWeeklyGoal',
|
||||||
|
defaultMessage: 'Set a weekly learning goal',
|
||||||
|
},
|
||||||
|
setWeeklyGoalDetail: {
|
||||||
|
id: 'learning.outline.setWeeklyGoalDetail',
|
||||||
|
defaultMessage: 'Setting a goal motivates you to finish the course. You can always change it later.',
|
||||||
|
},
|
||||||
start: {
|
start: {
|
||||||
id: 'learning.outline.start',
|
id: 'learning.outline.start',
|
||||||
defaultMessage: 'Start Course',
|
defaultMessage: 'Start Course',
|
||||||
@@ -112,6 +129,46 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Welcome to',
|
defaultMessage: 'Welcome to',
|
||||||
description: 'This precedes the title of the course',
|
description: 'This precedes the title of the course',
|
||||||
},
|
},
|
||||||
|
setLearningGoalButtonScreenReaderText: {
|
||||||
|
id: 'learning.outline.goalButton.casual.title',
|
||||||
|
defaultMessage: 'Set a learning goal style.',
|
||||||
|
description: 'screen reader text informing learner they can select an intensity of learning goal',
|
||||||
|
},
|
||||||
|
casualGoalButtonTitle: {
|
||||||
|
id: 'learning.outline.goalButton.screenReader.text',
|
||||||
|
defaultMessage: 'Casual',
|
||||||
|
description: 'A very short description of the least intense of three learning goals',
|
||||||
|
},
|
||||||
|
casualGoalButtonText: {
|
||||||
|
id: 'learning.outline.goalButton.casual.text',
|
||||||
|
defaultMessage: '1 day a week',
|
||||||
|
},
|
||||||
|
regularGoalButtonTitle: {
|
||||||
|
id: 'learning.outline.goalButton.regular.title',
|
||||||
|
defaultMessage: 'Regular',
|
||||||
|
description: 'A very short description of the middle option of three learning goals, Casual, Regular and Intense',
|
||||||
|
},
|
||||||
|
regularGoalButtonText: {
|
||||||
|
id: 'learning.outline.goalButton.regular.text',
|
||||||
|
defaultMessage: '3 days a week',
|
||||||
|
},
|
||||||
|
intenseGoalButtonTitle: {
|
||||||
|
id: 'learning.outline.goalButton.intense.title',
|
||||||
|
defaultMessage: 'Intense',
|
||||||
|
description: 'A very short description of the most intensive option of three learning goals, Casual, Regular and Intense',
|
||||||
|
},
|
||||||
|
intenseGoalButtonText: {
|
||||||
|
id: 'learning.outline.goalButton.intense.text',
|
||||||
|
defaultMessage: '5 days a week',
|
||||||
|
},
|
||||||
|
setGoalReminder: {
|
||||||
|
id: 'learning.outline.setGoalReminder',
|
||||||
|
defaultMessage: 'Set a goal reminder',
|
||||||
|
},
|
||||||
|
goalReminderDetail: {
|
||||||
|
id: 'learning.outline.goalReminderDetail',
|
||||||
|
defaultMessage: 'If we notice you’re not quite at your goal, we’ll send you an email reminder.',
|
||||||
|
},
|
||||||
proctoringInfoPanel: {
|
proctoringInfoPanel: {
|
||||||
id: 'learning.proctoringPanel.header',
|
id: 'learning.proctoringPanel.header',
|
||||||
defaultMessage: 'This course contains proctored exams',
|
defaultMessage: 'This course contains proctored exams',
|
||||||
@@ -220,6 +277,11 @@ const messages = defineMessages({
|
|||||||
id: 'learning.proctoringPanel.onboardingButtonPastDue',
|
id: 'learning.proctoringPanel.onboardingButtonPastDue',
|
||||||
defaultMessage: 'Onboarding Past Due',
|
defaultMessage: 'Onboarding Past Due',
|
||||||
},
|
},
|
||||||
|
accountActivationAlertTitle: {
|
||||||
|
id: 'account-activation.alert.title',
|
||||||
|
defaultMessage: 'Activate your account so you can log back in',
|
||||||
|
description: 'Title for account activation alert which is shown after the registration',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
51
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal file
51
src/course-home/outline-tab/widgets/FlagButton.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
function FlagButton({
|
||||||
|
ButtonIcon,
|
||||||
|
srText,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
isEnabled,
|
||||||
|
handleSelect,
|
||||||
|
}) {
|
||||||
|
const [isSelected, setIsSelected] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames('col flex-grow-1 p-3 border border-light rounded bg-white', { 'border-dark': isEnabled || isSelected })}
|
||||||
|
onMouseEnter={() => setIsSelected(true)}
|
||||||
|
onMouseLeave={() => setIsSelected(false)}
|
||||||
|
onClick={() => handleSelect()}
|
||||||
|
>
|
||||||
|
<div className=" justify-content-center">
|
||||||
|
{ButtonIcon}
|
||||||
|
</div>
|
||||||
|
<span className="sr-only sr-only-focusable">{srText}</span>
|
||||||
|
<div className="text-center small">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="text-center micro">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlagButton.propTypes = {
|
||||||
|
ButtonIcon: PropTypes.element.isRequired,
|
||||||
|
srText: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
text: PropTypes.string,
|
||||||
|
isEnabled: PropTypes.bool,
|
||||||
|
handleSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
FlagButton.defaultProps = {
|
||||||
|
isEnabled: false,
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlagButton;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, Card } from '@edx/paragon';
|
||||||
|
import { injectIntl, intlShape } 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';
|
||||||
|
|
||||||
|
function StartOrResumeCourseCard({ intl }) {
|
||||||
|
const {
|
||||||
|
courseId,
|
||||||
|
} = useSelector(state => state.courseHome);
|
||||||
|
|
||||||
|
const {
|
||||||
|
org,
|
||||||
|
} = useModel('courseHomeMeta', courseId);
|
||||||
|
|
||||||
|
const eventProperties = {
|
||||||
|
org_key: org,
|
||||||
|
courserun_key: courseId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
resumeCourse: {
|
||||||
|
hasVisitedCourse,
|
||||||
|
url: resumeCourseUrl,
|
||||||
|
},
|
||||||
|
|
||||||
|
} = useModel('outline', courseId);
|
||||||
|
|
||||||
|
const logResumeCourseClick = () => {
|
||||||
|
sendTrackingLogEvent('edx.course.home.resume_course.clicked', {
|
||||||
|
...eventProperties,
|
||||||
|
event_type: hasVisitedCourse ? 'resume' : 'start',
|
||||||
|
url: resumeCourseUrl,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="mb-3" data-testid="start-resume-card">
|
||||||
|
<Card.Body>
|
||||||
|
<div className="row w-100 m-0 ">
|
||||||
|
<h2 className="h4 col-auto flex-grow-1">{intl.formatMessage(messages.startBlurb)}</h2>
|
||||||
|
<div className="col col-auto p-0 justify-content-end">
|
||||||
|
<Button
|
||||||
|
variant="brand"
|
||||||
|
block
|
||||||
|
href={resumeCourseUrl}
|
||||||
|
onClick={() => logResumeCourseClick()}
|
||||||
|
>
|
||||||
|
{hasVisitedCourse ? intl.formatMessage(messages.resume) : intl.formatMessage(messages.start)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StartOrResumeCourseCard.propTypes = {
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default injectIntl(StartOrResumeCourseCard);
|
||||||
173
src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx
Normal file
173
src/course-home/outline-tab/widgets/WeeklyLearningGoalCard.jsx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Form, Card, Icon } from '@edx/paragon';
|
||||||
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import { Email } from '@edx/paragon/icons';
|
||||||
|
import { ReactComponent as FlagIntenseIcon } from '@edx/paragon/icons/svg/flag.svg';
|
||||||
|
import { ReactComponent as FlagCasualIcon } from './flag_outline.svg';
|
||||||
|
import { ReactComponent as FlagRegularIcon } from './flag_gray.svg';
|
||||||
|
import messages from '../messages';
|
||||||
|
import FlagButton from './FlagButton';
|
||||||
|
|
||||||
|
import { saveWeeklyCourseGoal } from '../../data';
|
||||||
|
|
||||||
|
function WeeklyLearningGoalCard({
|
||||||
|
daysPerWeek,
|
||||||
|
subscribedToReminders,
|
||||||
|
courseId,
|
||||||
|
intl,
|
||||||
|
}) {
|
||||||
|
const [daysPerWeekGoal, setDaysPerWeekGoal] = useState(daysPerWeek);
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
const [isGetReminderSelected, setGetReminderSelected] = useState(subscribedToReminders);
|
||||||
|
const weeklyLearningGoalLevels = {
|
||||||
|
CASUAL: 3,
|
||||||
|
REGULAR: 4,
|
||||||
|
INTENSE: 5,
|
||||||
|
};
|
||||||
|
Object.freeze(weeklyLearningGoalLevels);
|
||||||
|
|
||||||
|
function handleSelect(days) {
|
||||||
|
// Set the subscription button if this is the first time selecting a goal
|
||||||
|
const selectReminders = daysPerWeekGoal === null ? true : isGetReminderSelected;
|
||||||
|
setGetReminderSelected(selectReminders);
|
||||||
|
setDaysPerWeekGoal(days);
|
||||||
|
saveWeeklyCourseGoal(courseId, days, selectReminders);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubscribeToReminders(event) {
|
||||||
|
const isGetReminderChecked = event.target.checked;
|
||||||
|
setGetReminderSelected(isGetReminderChecked);
|
||||||
|
saveWeeklyCourseGoal(courseId, daysPerWeekGoal, isGetReminderChecked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row w-100 m-0 p-0">
|
||||||
|
<Card className="mb-3" data-testid="weekly-learning-goal-card">
|
||||||
|
<Card.Body>
|
||||||
|
<Card.Title>
|
||||||
|
<h4 className="m-0">{intl.formatMessage(messages.setWeeklyGoal)}</h4>
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Text>
|
||||||
|
{intl.formatMessage(messages.setWeeklyGoalDetail)}
|
||||||
|
</Card.Text>
|
||||||
|
<div
|
||||||
|
className="row w-100 m-0 p-0 justify-content-around"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor={weeklyLearningGoalLevels.CASUAL}
|
||||||
|
className="col-auto col-md-12 col-xl-auto m-0 p-0 pb-md-3 pb-xl-0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={weeklyLearningGoalLevels.CASUAL}
|
||||||
|
name="learningGoal"
|
||||||
|
radioGroup="learningGoal"
|
||||||
|
value={weeklyLearningGoalLevels.CASUAL}
|
||||||
|
onChange={() => handleSelect(weeklyLearningGoalLevels.CASUAL)}
|
||||||
|
tabIndex="0"
|
||||||
|
checked={weeklyLearningGoalLevels.CASUAL === daysPerWeekGoal}
|
||||||
|
className="position-absolute invisible"
|
||||||
|
/>
|
||||||
|
<FlagButton
|
||||||
|
ButtonIcon={<FlagCasualIcon />}
|
||||||
|
srText={intl.formatMessage(messages.setLearningGoalButtonScreenReaderText)}
|
||||||
|
title={intl.formatMessage(messages.casualGoalButtonTitle)}
|
||||||
|
text={intl.formatMessage(messages.casualGoalButtonText)}
|
||||||
|
isEnabled={weeklyLearningGoalLevels.CASUAL === daysPerWeekGoal}
|
||||||
|
handleSelect={() => { handleSelect(weeklyLearningGoalLevels.CASUAL); }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
htmlFor={weeklyLearningGoalLevels.REGULAR}
|
||||||
|
className="col-auto col-md-12 col-xl-auto m-0 p-0 pb-md-3 pb-xl-0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={weeklyLearningGoalLevels.REGULAR}
|
||||||
|
name="learningGoal"
|
||||||
|
radioGroup="learningGoal"
|
||||||
|
value={weeklyLearningGoalLevels.REGULAR}
|
||||||
|
onChange={() => handleSelect(weeklyLearningGoalLevels.REGULAR)}
|
||||||
|
tabIndex="-1"
|
||||||
|
checked={weeklyLearningGoalLevels.REGULAR === daysPerWeekGoal}
|
||||||
|
className="position-absolute invisible"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FlagButton
|
||||||
|
ButtonIcon={<FlagRegularIcon />}
|
||||||
|
srText={intl.formatMessage(messages.setLearningGoalButtonScreenReaderText)}
|
||||||
|
title={intl.formatMessage(messages.regularGoalButtonTitle)}
|
||||||
|
text={intl.formatMessage(messages.regularGoalButtonText)}
|
||||||
|
isEnabled={weeklyLearningGoalLevels.REGULAR === daysPerWeekGoal}
|
||||||
|
handleSelect={() => { handleSelect(weeklyLearningGoalLevels.REGULAR); }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
htmlFor={weeklyLearningGoalLevels.INTENSE}
|
||||||
|
className="col-auto col-md-12 col-xl-auto m-0 p-0 pb-md-3 pb-xl-0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={weeklyLearningGoalLevels.INTENSE}
|
||||||
|
name="learningGoal"
|
||||||
|
radioGroup="learningGoal"
|
||||||
|
value={weeklyLearningGoalLevels.INTENSE}
|
||||||
|
onChange={() => handleSelect(weeklyLearningGoalLevels.INTENSE)}
|
||||||
|
tabIndex="-1"
|
||||||
|
checked={weeklyLearningGoalLevels.INTENSE === daysPerWeekGoal}
|
||||||
|
className="position-absolute invisible"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FlagButton
|
||||||
|
ButtonIcon={<FlagIntenseIcon />}
|
||||||
|
srText={intl.formatMessage(messages.setLearningGoalButtonScreenReaderText)}
|
||||||
|
title={intl.formatMessage(messages.intenseGoalButtonTitle)}
|
||||||
|
text={intl.formatMessage(messages.intenseGoalButtonText)}
|
||||||
|
isEnabled={weeklyLearningGoalLevels.INTENSE === daysPerWeekGoal}
|
||||||
|
handleSelect={() => { handleSelect(weeklyLearningGoalLevels.INTENSE); }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="row p-3">
|
||||||
|
<Form.Switch
|
||||||
|
checked={isGetReminderSelected}
|
||||||
|
onChange={(event) => handleSubscribeToReminders(event)}
|
||||||
|
disabled={!daysPerWeekGoal}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.setGoalReminder)}
|
||||||
|
</Form.Switch>
|
||||||
|
</div>
|
||||||
|
</Card.Body>
|
||||||
|
{isGetReminderSelected && (
|
||||||
|
<Card.Footer className="border-0 px-2.5">
|
||||||
|
<div className="row w-100 m-0 small align-center">
|
||||||
|
<div className="d-flex align-items-center pr-1.5">
|
||||||
|
<Icon src={Email} />
|
||||||
|
</div>
|
||||||
|
<div className="col align-center">
|
||||||
|
{intl.formatMessage(messages.goalReminderDetail)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Footer>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WeeklyLearningGoalCard.propTypes = {
|
||||||
|
daysPerWeek: PropTypes.number,
|
||||||
|
subscribedToReminders: PropTypes.bool,
|
||||||
|
courseId: PropTypes.string.isRequired,
|
||||||
|
intl: intlShape.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
WeeklyLearningGoalCard.defaultProps = {
|
||||||
|
daysPerWeek: null,
|
||||||
|
subscribedToReminders: false,
|
||||||
|
};
|
||||||
|
export default injectIntl(WeeklyLearningGoalCard);
|
||||||
18
src/course-home/outline-tab/widgets/flag_gray.svg
Normal file
18
src/course-home/outline-tab/widgets/flag_gray.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="15"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 15 17"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg11"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M9.4 2L9 0H0V17H2V10H7.6L8 12H15V2H9.4ZM13 10H9.64L9.24 8H2V2H7.36L7.76 4H13V10Z"
|
||||||
|
fill="#002B2B"
|
||||||
|
id="path9" />
|
||||||
|
<path
|
||||||
|
style="fill:#808080;fill-rule:evenodd;stroke-width:0.0150977"
|
||||||
|
d="M 9.6594698,9.9871226 C 9.6577909,9.9829707 9.5654776,9.5311723 9.4543296,8.9831261 L 9.2522415,7.9866785 5.6376662,7.9790074 2.0230906,7.9713362 V 4.9970494 2.0227625 l 2.6636151,0.00771 2.6636151,0.00771 0.1968204,0.9888987 0.1968205,0.9888988 h 2.6200263 2.620026 v 2.9893428 2.9893428 h -1.660746 c -0.91341,0 -1.6621194,-0.0034 -1.6637982,-0.00755 z"
|
||||||
|
id="path302" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 801 B |
3
src/course-home/outline-tab/widgets/flag_outline.svg
Normal file
3
src/course-home/outline-tab/widgets/flag_outline.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="15" height="17" viewBox="0 0 15 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.4 2L9 0H0V17H2V10H7.6L8 12H15V2H9.4ZM13 10H9.64L9.24 8H2V2H7.36L7.76 4H13V10Z" fill="#002B2B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 211 B |
@@ -2,9 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import {
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { OverlayTrigger, Popover } from '@edx/paragon';
|
import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
import { useModel } from '../../../../generic/model-store';
|
||||||
@@ -25,14 +23,6 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
|||||||
|
|
||||||
const currentGrade = Number((visiblePercent * 100).toFixed(0));
|
const currentGrade = Number((visiblePercent * 100).toFixed(0));
|
||||||
|
|
||||||
let currentGradeDirection = currentGrade < 50 ? '' : '-';
|
|
||||||
|
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
|
||||||
|
|
||||||
if (isLocaleRtl) {
|
|
||||||
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
@@ -47,16 +37,16 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
<circle cx={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`} cy="50%" r="8.5" fill="transparent" />
|
<circle cx={`${Math.min(...[currentGrade, 100])}%`} cy="50%" r="8.5" fill="transparent" />
|
||||||
<rect className="grade-bar__divider" x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`} style={{ transform: 'translateY(2.61em)' }} />
|
<rect className="grade-bar__divider" x={`${Math.min(...[currentGrade, 100])}%`} style={{ transform: 'translateY(2.61em)' }} />
|
||||||
</g>
|
</g>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
<text
|
<text
|
||||||
className="x-small"
|
className="x-small"
|
||||||
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
||||||
x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`}
|
x={`${Math.min(...[currentGrade, 100])}%`}
|
||||||
y="20px"
|
y="20px"
|
||||||
style={{ transform: `translateX(${currentGradeDirection}3.4em)` }}
|
style={{ transform: `translateX(${currentGrade < 50 ? '' : '-'}3.4em)` }}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.currentGradeLabel)}
|
{intl.formatMessage(messages.currentGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
getLocale, injectIntl, intlShape, isRtl,
|
|
||||||
} from '@edx/frontend-platform/i18n';
|
|
||||||
import { OverlayTrigger, Popover } from '@edx/paragon';
|
import { OverlayTrigger, Popover } from '@edx/paragon';
|
||||||
|
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
|
|
||||||
function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
||||||
const isLocaleRtl = isRtl(getLocale());
|
|
||||||
|
|
||||||
let passingGradeDirection = passingGrade < 50 ? '' : '-';
|
|
||||||
|
|
||||||
if (isLocaleRtl) {
|
|
||||||
passingGradeDirection = passingGrade < 50 ? '-' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
@@ -31,17 +21,17 @@ function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
<circle cx={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`} cy="50%" r="8.5" fill="transparent" />
|
<circle cx={`${passingGrade}%`} cy="50%" r="8.5" fill="transparent" />
|
||||||
<circle className="grade-bar--passing" cx={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`} cy="50%" r="4.5" />
|
<circle className="grade-bar--passing" cx={`${passingGrade}%`} cy="50%" r="4.5" />
|
||||||
</g>
|
</g>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
|
|
||||||
<text
|
<text
|
||||||
className="x-small"
|
className="x-small"
|
||||||
textAnchor={passingGrade < 50 ? 'start' : 'end'}
|
textAnchor={passingGrade < 50 ? 'start' : 'end'}
|
||||||
x={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`}
|
x={`${passingGrade}%`}
|
||||||
y="90px"
|
y="90px"
|
||||||
style={{ transform: `translateX(${passingGradeDirection}3.4em)` }}
|
style={{ transform: `translateX(${passingGrade < 50 ? '' : '-'}3.4em)` }}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.passingGradeLabel)}
|
{intl.formatMessage(messages.passingGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
courseMaterial: {
|
|
||||||
id: 'learn.navigation.course.tabs.label',
|
|
||||||
defaultMessage: 'Course Material',
|
|
||||||
description: 'The accessible label for course tabs navigation',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default messages;
|
|
||||||
@@ -159,7 +159,7 @@ describe('CoursewareContainer', () => {
|
|||||||
const courseId = defaultCourseId;
|
const courseId = defaultCourseId;
|
||||||
|
|
||||||
function assertLoadedHeader(container) {
|
function assertLoadedHeader(container) {
|
||||||
const courseHeader = container.querySelector('.learning-header');
|
const courseHeader = container.querySelector('.course-header');
|
||||||
// Ensure the course number and org appear - this proves we loaded course metadata properly.
|
// Ensure the course number and org appear - this proves we loaded course metadata properly.
|
||||||
expect(courseHeader).toHaveTextContent(courseMetadata.number);
|
expect(courseHeader).toHaveTextContent(courseMetadata.number);
|
||||||
expect(courseHeader).toHaveTextContent(courseMetadata.org);
|
expect(courseHeader).toHaveTextContent(courseMetadata.org);
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ function Course({
|
|||||||
courseId={courseId}
|
courseId={courseId}
|
||||||
sectionId={section ? section.id : null}
|
sectionId={section ? section.id : null}
|
||||||
sequenceId={sequenceId}
|
sequenceId={sequenceId}
|
||||||
isStaff={course ? course.isStaff : null}
|
|
||||||
unitId={unitId}
|
|
||||||
//* * [MM-P2P] Experiment */
|
//* * [MM-P2P] Experiment */
|
||||||
mmp2p={MMP2P}
|
mmp2p={MMP2P}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,17 +5,32 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faHome } from '@fortawesome/free-solid-svg-icons';
|
import { faHome } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { SelectMenu } from '@edx/paragon';
|
import { Hyperlink, MenuItem, SelectMenu } from '@edx/paragon';
|
||||||
import { Link } from 'react-router-dom';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
|
import {
|
||||||
|
sendTrackingLogEvent,
|
||||||
|
sendTrackEvent,
|
||||||
|
} from '@edx/frontend-platform/analytics';
|
||||||
import { useModel, useModels } from '../../generic/model-store';
|
import { useModel, useModels } from '../../generic/model-store';
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p';
|
import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p';
|
||||||
import JumpNavMenuItem from './JumpNavMenuItem';
|
|
||||||
|
|
||||||
function CourseBreadcrumb({
|
function CourseBreadcrumb({
|
||||||
content, withSeparator, courseId, unitId, isStaff,
|
content, withSeparator,
|
||||||
}) {
|
}) {
|
||||||
const defaultContent = content.filter(destination => destination.default)[0] || { id: courseId, label: '' };
|
const defaultContent = content.filter(destination => destination.default)[0];
|
||||||
|
const administrator = getAuthenticatedUser() ? getAuthenticatedUser().administrator : false;
|
||||||
|
function logEvent(target) {
|
||||||
|
const eventName = 'edx.ui.lms.jump_nav.selected';
|
||||||
|
const payload = {
|
||||||
|
target_name: target.label,
|
||||||
|
id: target.id,
|
||||||
|
current_id: defaultContent.id,
|
||||||
|
widget_placement: 'breadcrumb',
|
||||||
|
};
|
||||||
|
sendTrackEvent(eventName, payload);
|
||||||
|
sendTrackingLogEvent(eventName, payload);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -29,25 +44,22 @@ function CourseBreadcrumb({
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff
|
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !administrator
|
||||||
? (
|
? (
|
||||||
<Link
|
<a className="text-primary-500" href={defaultContent.url}>{defaultContent.label}
|
||||||
className="text-primary-500"
|
</a>
|
||||||
to={`/course/${courseId}/${defaultContent.id}`}
|
|
||||||
>
|
|
||||||
{defaultContent.label}
|
|
||||||
</Link>
|
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<SelectMenu isLink defaultMessage={defaultContent.label}>
|
<SelectMenu isLink defaultMessage={defaultContent.label}>
|
||||||
{content.map(item => (
|
{content.map(item => (
|
||||||
<JumpNavMenuItem
|
<MenuItem
|
||||||
isDefault={item.default}
|
as={Hyperlink}
|
||||||
sequences={item.sequences}
|
defaultSelected={item.default}
|
||||||
courseId={courseId}
|
destination={item.url}
|
||||||
title={item.label}
|
onClick={logEvent(item)}
|
||||||
currentUnit={unitId}
|
>
|
||||||
/>
|
{item.label}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</SelectMenu>
|
</SelectMenu>
|
||||||
)}
|
)}
|
||||||
@@ -60,75 +72,59 @@ CourseBreadcrumb.propTypes = {
|
|||||||
content: PropTypes.arrayOf(
|
content: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
default: PropTypes.bool,
|
default: PropTypes.bool,
|
||||||
|
url: PropTypes.string,
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
).isRequired,
|
).isRequired,
|
||||||
unitId: PropTypes.string,
|
|
||||||
withSeparator: PropTypes.bool,
|
withSeparator: PropTypes.bool,
|
||||||
courseId: PropTypes.string,
|
|
||||||
isStaff: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
CourseBreadcrumb.defaultProps = {
|
CourseBreadcrumb.defaultProps = {
|
||||||
withSeparator: false,
|
withSeparator: false,
|
||||||
unitId: null,
|
|
||||||
courseId: null,
|
|
||||||
isStaff: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CourseBreadcrumbs({
|
export default function CourseBreadcrumbs({
|
||||||
courseId,
|
courseId,
|
||||||
sectionId,
|
sectionId,
|
||||||
sequenceId,
|
sequenceId,
|
||||||
unitId,
|
|
||||||
isStaff,
|
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p,
|
mmp2p,
|
||||||
}) {
|
}) {
|
||||||
const course = useModel('coursewareMeta', courseId);
|
const course = useModel('coursewareMeta', courseId);
|
||||||
const courseStatus = useSelector(state => state.courseware.courseStatus);
|
const courseStatus = useSelector(state => state.courseware.courseStatus);
|
||||||
|
const sections = course ? Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, section])) : null;
|
||||||
|
const possibleSequences = sections && sectionId ? sections[sectionId].sequenceIds : [];
|
||||||
|
const sequences = Object.fromEntries(useModels('sequences', possibleSequences).map(sequence => [sequence.id, sequence]));
|
||||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||||
|
|
||||||
const allSequencesInSections = Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, {
|
|
||||||
default: section.id === sectionId,
|
|
||||||
title: section.title,
|
|
||||||
sequences: useModels('sequences', section.sequenceIds),
|
|
||||||
}]));
|
|
||||||
|
|
||||||
const links = useMemo(() => {
|
const links = useMemo(() => {
|
||||||
const chapters = [];
|
const temp = [];
|
||||||
const sequentials = [];
|
|
||||||
if (courseStatus === 'loaded' && sequenceStatus === 'loaded') {
|
if (courseStatus === 'loaded' && sequenceStatus === 'loaded') {
|
||||||
Object.entries(allSequencesInSections).forEach(([id, section]) => {
|
temp.push(course.sectionIds.map(id => ({
|
||||||
chapters.push({
|
id,
|
||||||
id,
|
label: sections[id].title,
|
||||||
label: section.title,
|
default: (id === sectionId),
|
||||||
default: section.default,
|
// navigate to first sequence in section, (TODO: navigate to first incomplete sequence in section)
|
||||||
sequences: section.sequences,
|
url: `${getConfig().BASE_URL}/course/${courseId}/${sections[id].sequenceIds[0]}`,
|
||||||
});
|
})));
|
||||||
if (section.default) {
|
temp.push(sections[sectionId].sequenceIds.map(id => ({
|
||||||
section.sequences.forEach(sequence => {
|
id,
|
||||||
sequentials.push({
|
label: sequences[id].title,
|
||||||
id: sequence.id,
|
default: id === sequenceId,
|
||||||
label: sequence.title,
|
// first unit it section (TODO: navigate to first incomplete in sequence)
|
||||||
default: sequence.id === sequenceId,
|
url: `${getConfig().BASE_URL}/course/${courseId}/${sequences[id].id}/${sequences[id].unitIds[0]}`,
|
||||||
sequences: [sequence],
|
})));
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return [chapters, sequentials];
|
return temp;
|
||||||
}, [courseStatus, sequenceStatus, allSequencesInSections]);
|
}, [courseStatus, sections, sequences]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav aria-label="breadcrumb" className="my-4 d-inline-block col-sm-10">
|
<nav aria-label="breadcrumb" className="my-4 d-inline-block col-sm-10">
|
||||||
<ol className="list-unstyled d-flex flex-nowrap align-items-center m-0">
|
<ol className="list-unstyled d-flex align-items-center m-0">
|
||||||
<li className="list-unstyled d-flex m-0">
|
<li>
|
||||||
<Link
|
<a
|
||||||
|
href={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course/`}
|
||||||
className="flex-shrink-0 text-primary"
|
className="flex-shrink-0 text-primary"
|
||||||
to={`/course/${courseId}/home`}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faHome} className="mr-2" />
|
<FontAwesomeIcon icon={faHome} className="mr-2" />
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -136,16 +132,12 @@ export default function CourseBreadcrumbs({
|
|||||||
description="The course home link in breadcrumbs nav"
|
description="The course home link in breadcrumbs nav"
|
||||||
defaultMessage="Course"
|
defaultMessage="Course"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{links.map(content => (
|
{links.map(content => (
|
||||||
<CourseBreadcrumb
|
<CourseBreadcrumb
|
||||||
courseId={courseId}
|
|
||||||
sequenceId={sequenceId}
|
|
||||||
content={content}
|
content={content}
|
||||||
unitId={unitId}
|
|
||||||
withSeparator
|
withSeparator
|
||||||
isStaff={isStaff}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{/** [MM-P2P] Experiment */}
|
{/** [MM-P2P] Experiment */}
|
||||||
@@ -161,8 +153,6 @@ CourseBreadcrumbs.propTypes = {
|
|||||||
courseId: PropTypes.string.isRequired,
|
courseId: PropTypes.string.isRequired,
|
||||||
sectionId: PropTypes.string,
|
sectionId: PropTypes.string,
|
||||||
sequenceId: PropTypes.string,
|
sequenceId: PropTypes.string,
|
||||||
unitId: PropTypes.string,
|
|
||||||
isStaff: PropTypes.bool,
|
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p: PropTypes.shape({
|
mmp2p: PropTypes.shape({
|
||||||
state: PropTypes.shape({
|
state: PropTypes.shape({
|
||||||
@@ -174,8 +164,6 @@ CourseBreadcrumbs.propTypes = {
|
|||||||
CourseBreadcrumbs.defaultProps = {
|
CourseBreadcrumbs.defaultProps = {
|
||||||
sectionId: null,
|
sectionId: null,
|
||||||
sequenceId: null,
|
sequenceId: null,
|
||||||
unitId: null,
|
|
||||||
isStaff: null,
|
|
||||||
/** [MM-P2P] Experiment */
|
/** [MM-P2P] Experiment */
|
||||||
mmp2p: {},
|
mmp2p: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, render } from '@testing-library/react';
|
import { screen, render, fireEvent } from '@testing-library/react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { useModel, useModels } from '../../generic/model-store';
|
import { useModel, useModels } from '../../generic/model-store';
|
||||||
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
import CourseBreadcrumbs from './CourseBreadcrumbs';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform');
|
jest.mock('@edx/frontend-platform');
|
||||||
|
jest.mock('react-redux');
|
||||||
jest.mock('@edx/frontend-platform/analytics');
|
jest.mock('@edx/frontend-platform/analytics');
|
||||||
|
|
||||||
// Remove When Fully rolled out>>>
|
// Remove When Fully rolled out>>>
|
||||||
@@ -16,16 +17,7 @@ getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
|
|||||||
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
|
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
|
||||||
// ^^^^Remove When Fully rolled out
|
// ^^^^Remove When Fully rolled out
|
||||||
|
|
||||||
jest.mock('react-redux', () => ({
|
useSelector.mockImplementation(() => 'loaded');
|
||||||
connect: (mapStateToProps, mapDispatchToProps) => (ReactComponent) => ({
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
ReactComponent,
|
|
||||||
}),
|
|
||||||
Provider: ({ children }) => children,
|
|
||||||
useSelector: () => 'loaded',
|
|
||||||
}));
|
|
||||||
|
|
||||||
useModels.mockImplementation((name) => {
|
useModels.mockImplementation((name) => {
|
||||||
if (name === 'sections') {
|
if (name === 'sections') {
|
||||||
return [
|
return [
|
||||||
@@ -106,20 +98,20 @@ describe('CourseBreadcrumbs', () => {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
render(
|
render(
|
||||||
<BrowserRouter>
|
<CourseBreadcrumbs
|
||||||
<CourseBreadcrumbs
|
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
||||||
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
||||||
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
/>,
|
||||||
isStaff
|
|
||||||
/>
|
|
||||||
</BrowserRouter>,
|
|
||||||
);
|
);
|
||||||
it('renders course breadcrumbs as expected', async () => {
|
it('renders course breadcrumbs as expected, handles clicks', 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.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
|
||||||
expect(screen.queryAllByRole('button')).toHaveLength(2);
|
expect(screen.queryAllByRole('button')).toHaveLength(2);
|
||||||
|
const sectionButton = screen.getByText('Example Week 1: Getting Started');
|
||||||
|
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
||||||
|
fireEvent.click(sectionButton);
|
||||||
|
expect(screen.queryAllByRole('link')).toHaveLength(2);
|
||||||
|
const menuItem = screen.queryAllByRole('link')[0];
|
||||||
|
fireEvent.click(menuItem);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
/* eslint-disable consistent-return */
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { history } from '@edx/frontend-platform';
|
|
||||||
import { MenuItem } from '@edx/paragon';
|
|
||||||
|
|
||||||
import {
|
|
||||||
sendTrackingLogEvent,
|
|
||||||
sendTrackEvent,
|
|
||||||
} from '@edx/frontend-platform/analytics';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { checkBlockCompletion } from '../data';
|
|
||||||
|
|
||||||
export default function JumpNavMenuItem({
|
|
||||||
title,
|
|
||||||
courseId,
|
|
||||||
currentUnit,
|
|
||||||
sequences,
|
|
||||||
isDefault,
|
|
||||||
|
|
||||||
}) {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
function logEvent(targetUrl) {
|
|
||||||
const eventName = 'edx.ui.lms.jump_nav.selected';
|
|
||||||
const payload = {
|
|
||||||
target_name: title,
|
|
||||||
id: targetUrl,
|
|
||||||
current_id: courseId,
|
|
||||||
widget_placement: 'breadcrumb',
|
|
||||||
};
|
|
||||||
sendTrackEvent(eventName, payload);
|
|
||||||
sendTrackingLogEvent(eventName, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lazyloadUrl() {
|
|
||||||
if (isDefault) {
|
|
||||||
return `/course/${courseId}/${currentUnit}`;
|
|
||||||
}
|
|
||||||
const destinationString = sequences.forEach(sequence => sequence.unitIds.forEach(unitId => {
|
|
||||||
const complete = dispatch(checkBlockCompletion(
|
|
||||||
courseId,
|
|
||||||
sequence.id, unitId,
|
|
||||||
))
|
|
||||||
.then(value => value);
|
|
||||||
if (!complete) { return `/course/${courseId}/${unitId}`; }
|
|
||||||
}));
|
|
||||||
return destinationString || `/course/${courseId}/${sequences[0].unitIds[0]}`;
|
|
||||||
}
|
|
||||||
function handleClick() {
|
|
||||||
const url = lazyloadUrl();
|
|
||||||
logEvent(url);
|
|
||||||
history.push(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
defaultSelected={isDefault}
|
|
||||||
onClick={() => handleClick()}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sequenceShape = PropTypes.shape({
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
unitIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
sectionId: PropTypes.string.isRequired,
|
|
||||||
isTimeLimited: PropTypes.bool,
|
|
||||||
isProctored: PropTypes.bool,
|
|
||||||
legacyWebUrl: PropTypes.string,
|
|
||||||
});
|
|
||||||
|
|
||||||
JumpNavMenuItem.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
sequences: PropTypes.arrayOf(sequenceShape).isRequired,
|
|
||||||
isDefault: PropTypes.bool.isRequired,
|
|
||||||
courseId: PropTypes.string.isRequired,
|
|
||||||
currentUnit: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { screen, render } from '@testing-library/react';
|
|
||||||
import JumpNavMenuItem from './JumpNavMenuItem';
|
|
||||||
import { fireEvent } from '../../setupTest';
|
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform');
|
|
||||||
jest.mock('@edx/frontend-platform/analytics');
|
|
||||||
|
|
||||||
const mockCheckBlock = jest.fn(() => Promise.resolve(true)); // check all units
|
|
||||||
|
|
||||||
jest.mock('react-redux', () => ({
|
|
||||||
useDispatch: () => mockCheckBlock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockData = {
|
|
||||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
|
||||||
sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
|
||||||
|
|
||||||
title: 'Demo Menu Item',
|
|
||||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
|
||||||
currentUnit: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
|
||||||
sequences: [
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
|
|
||||||
blockType: 'sequential',
|
|
||||||
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',
|
|
||||||
],
|
|
||||||
legacyWebUrl: 'http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5?experience=legacy',
|
|
||||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
|
|
||||||
title: 'Homework - Question Styles',
|
|
||||||
legacyWebUrl: 'http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions?experience=legacy',
|
|
||||||
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',
|
|
||||||
],
|
|
||||||
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isDefault: false,
|
|
||||||
};
|
|
||||||
describe('JumpNavMenuItem', () => {
|
|
||||||
render(
|
|
||||||
<JumpNavMenuItem
|
|
||||||
{...mockData}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
it('renders menu Item as expected with button and Text and handles clicks', () => {
|
|
||||||
expect(screen.queryAllByRole('button')).toHaveLength(1);
|
|
||||||
expect(screen.getByText('Demo Menu Item'));
|
|
||||||
const navButton = screen.queryAllByRole('button')[0];
|
|
||||||
fireEvent.click(navButton);
|
|
||||||
expect(mockCheckBlock).toBeCalledTimes(14); // number of units to check on load.
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
@@ -11,7 +11,6 @@ import CourseInProgress from './CourseInProgress';
|
|||||||
import CourseNonPassing from './CourseNonPassing';
|
import CourseNonPassing from './CourseNonPassing';
|
||||||
import { COURSE_EXIT_MODES, getCourseExitMode } from './utils';
|
import { COURSE_EXIT_MODES, getCourseExitMode } from './utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { unsubscribeFromGoalReminders } from './data/thunks';
|
|
||||||
|
|
||||||
import { useModel } from '../../../generic/model-store';
|
import { useModel } from '../../../generic/model-store';
|
||||||
|
|
||||||
@@ -19,13 +18,10 @@ function CourseExit({ intl }) {
|
|||||||
const { courseId } = useSelector(state => state.courseware);
|
const { courseId } = useSelector(state => state.courseware);
|
||||||
const {
|
const {
|
||||||
certificateData,
|
certificateData,
|
||||||
courseExitPageIsActive,
|
|
||||||
courseGoals,
|
|
||||||
enrollmentMode,
|
|
||||||
hasScheduledContent,
|
hasScheduledContent,
|
||||||
isEnrolled,
|
isEnrolled,
|
||||||
isMasquerading,
|
|
||||||
userHasPassingGrade,
|
userHasPassingGrade,
|
||||||
|
courseExitPageIsActive,
|
||||||
} = useModel('coursewareMeta', courseId);
|
} = useModel('coursewareMeta', courseId);
|
||||||
|
|
||||||
const mode = getCourseExitMode(
|
const mode = getCourseExitMode(
|
||||||
@@ -36,15 +32,6 @@ function CourseExit({ intl }) {
|
|||||||
courseExitPageIsActive,
|
courseExitPageIsActive,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Audit users cannot fully complete a course, so we will
|
|
||||||
// unsubscribe them from goal reminders once they reach the course exit page
|
|
||||||
// to avoid spamming them with goal reminder emails
|
|
||||||
if (courseGoals && enrollmentMode === 'audit' && !isMasquerading) {
|
|
||||||
useEffect(() => {
|
|
||||||
unsubscribeFromGoalReminders(courseId);
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = null;
|
let body = null;
|
||||||
if (mode === COURSE_EXIT_MODES.nonPassing) {
|
if (mode === COURSE_EXIT_MODES.nonPassing) {
|
||||||
body = (<CourseNonPassing />);
|
body = (<CourseNonPassing />);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter';
|
|||||||
import { Factory } from 'rosie';
|
import { Factory } from 'rosie';
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { waitFor } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { fetchCourse } from '../../data';
|
import { fetchCourse } from '../../data';
|
||||||
import { buildSimpleCourseBlocks } from '../../../shared/data/__factories__/courseBlocks.factory';
|
import { buildSimpleCourseBlocks } from '../../../shared/data/__factories__/courseBlocks.factory';
|
||||||
@@ -378,25 +377,4 @@ describe('Course Exit Pages', () => {
|
|||||||
expect(screen.getByRole('link', { name: 'View course schedule' })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: 'View course schedule' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unsubscribes the user when loading the course exit page', async () => {
|
|
||||||
setMetadata({
|
|
||||||
enrollment: {
|
|
||||||
mode: 'audit',
|
|
||||||
courseGoals: {
|
|
||||||
goal_options: [],
|
|
||||||
selected_goal: {
|
|
||||||
days_per_week: 1,
|
|
||||||
subscribed_to_reminders: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await fetchAndRender(<CourseExit />);
|
|
||||||
const url = `${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`;
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(axiosMock.history.post[0].url).toMatch(url);
|
|
||||||
expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${defaultMetadata.id}","subscribed_to_reminders":false}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function filterRecommendationsList(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCourseRecommendations(courseKey) {
|
export default async function getCourseRecommendations(courseKey) {
|
||||||
const discoveryApiUrl = getConfig().DISCOVERY_API_BASE_URL;
|
const discoveryApiUrl = getConfig().DISCOVERY_API_BASE_URL;
|
||||||
if (!discoveryApiUrl) {
|
if (!discoveryApiUrl) {
|
||||||
return [];
|
return [];
|
||||||
@@ -36,11 +36,3 @@ export async function getCourseRecommendations(courseKey) {
|
|||||||
]);
|
]);
|
||||||
return filterRecommendationsList(camelCaseObject(recommendationsResponse), camelCaseObject(enrollmentsResponse));
|
return filterRecommendationsList(camelCaseObject(recommendationsResponse), camelCaseObject(enrollmentsResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postUnsubscribeFromGoalReminders(courseId) {
|
|
||||||
const url = new URL(`${getConfig().LMS_BASE_URL}/api/course_home/save_course_goal`);
|
|
||||||
return getAuthenticatedHttpClient().post(url.href, {
|
|
||||||
course_id: courseId,
|
|
||||||
subscribed_to_reminders: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import {
|
|||||||
fetchCourseRecommendationsRequest,
|
fetchCourseRecommendationsRequest,
|
||||||
fetchCourseRecommendationsSuccess,
|
fetchCourseRecommendationsSuccess,
|
||||||
} from './slice';
|
} from './slice';
|
||||||
import {
|
import getCourseRecommendations from './api';
|
||||||
getCourseRecommendations,
|
|
||||||
postUnsubscribeFromGoalReminders,
|
|
||||||
} from './api';
|
|
||||||
import { updateModel } from '../../../../generic/model-store';
|
import { updateModel } from '../../../../generic/model-store';
|
||||||
|
|
||||||
export default function fetchCourseRecommendations(courseKey, courseId) {
|
export default function fetchCourseRecommendations(courseKey, courseId) {
|
||||||
@@ -30,7 +27,3 @@ export default function fetchCourseRecommendations(courseKey, courseId) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unsubscribeFromGoalReminders(courseId, daysPerWeek, subscribedToReminders) {
|
|
||||||
return postUnsubscribeFromGoalReminders(courseId, daysPerWeek, subscribedToReminders);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ import { getConfig, history } from '@edx/frontend-platform';
|
|||||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||||
import { ActionRow, Alert, Button } from '@edx/paragon';
|
import { ActionRow, Alert, Button } from '@edx/paragon';
|
||||||
|
|
||||||
import { useModel } from '../../../../generic/model-store';
|
|
||||||
import { saveIntegritySignature } from '../../../data';
|
import { saveIntegritySignature } from '../../../data';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
|
||||||
function HonorCode({ intl, courseId }) {
|
function HonorCode({ intl, courseId }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { isMasquerading } = useModel('coursewareMeta', courseId);
|
|
||||||
const siteName = getConfig().SITE_NAME;
|
const siteName = getConfig().SITE_NAME;
|
||||||
const honorCodeUrl = `${getConfig().TERMS_OF_SERVICE_URL}#honor-code`;
|
const honorCodeUrl = `${process.env.TERMS_OF_SERVICE_URL}#honor-code`;
|
||||||
|
|
||||||
const handleCancel = () => history.push(`/course/${courseId}/home`);
|
const handleCancel = () => history.push(`/course/${courseId}/home`);
|
||||||
|
|
||||||
const handleAgree = () => dispatch(saveIntegritySignature(courseId, isMasquerading));
|
const handleAgree = () => {
|
||||||
|
dispatch(saveIntegritySignature(courseId));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert variant="light" aria-live="off">
|
<Alert variant="light" aria-live="off">
|
||||||
|
|||||||
@@ -8,13 +8,6 @@ Factory.define('courseMetadata')
|
|||||||
.attrs({
|
.attrs({
|
||||||
content_type_gating_enabled: false,
|
content_type_gating_enabled: false,
|
||||||
course_expired_message: null,
|
course_expired_message: null,
|
||||||
course_goals: {
|
|
||||||
goal_options: [],
|
|
||||||
selected_goal: {
|
|
||||||
days_per_week: 1,
|
|
||||||
subscribed_to_reminders: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
end: null,
|
end: null,
|
||||||
enrollment_start: null,
|
enrollment_start: null,
|
||||||
enrollment_end: null,
|
enrollment_end: null,
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ function normalizeMetadata(metadata) {
|
|||||||
accessExpiration: camelCaseObject(data.access_expiration),
|
accessExpiration: camelCaseObject(data.access_expiration),
|
||||||
canShowUpgradeSock: data.can_show_upgrade_sock,
|
canShowUpgradeSock: data.can_show_upgrade_sock,
|
||||||
contentTypeGatingEnabled: data.content_type_gating_enabled,
|
contentTypeGatingEnabled: data.content_type_gating_enabled,
|
||||||
courseGoals: data.course_goals,
|
|
||||||
id: data.id,
|
id: data.id,
|
||||||
title: data.name,
|
title: data.name,
|
||||||
number: data.number,
|
number: data.number,
|
||||||
|
|||||||
@@ -237,13 +237,7 @@ export function fetchSequence(sequenceId) {
|
|||||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Some errors are expected - for example, CoursewareContainer may request sequence metadata for a unit and rely
|
logError(error);
|
||||||
// on the request failing to notice that it actually does have a unit (mostly so it doesn't have to know anything
|
|
||||||
// about the opaque key structure). In such cases, the backend gives us a 422.
|
|
||||||
const isExpected = error.response && error.response.status === 422;
|
|
||||||
if (!isExpected) {
|
|
||||||
logError(error);
|
|
||||||
}
|
|
||||||
dispatch(fetchSequenceFailure({ sequenceId }));
|
dispatch(fetchSequenceFailure({ sequenceId }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -253,7 +247,7 @@ export function checkBlockCompletion(courseId, sequenceId, unitId) {
|
|||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
const { models } = getState();
|
const { models } = getState();
|
||||||
if (models.units[unitId].complete) {
|
if (models.units[unitId].complete) {
|
||||||
return {}; // do nothing. Things don't get uncompleted after they are completed.
|
return; // do nothing. Things don't get uncompleted after they are completed.
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -265,11 +259,9 @@ export function checkBlockCompletion(courseId, sequenceId, unitId) {
|
|||||||
complete: isComplete,
|
complete: isComplete,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
return isComplete;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error);
|
logError(error);
|
||||||
}
|
}
|
||||||
return {};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,15 +301,10 @@ export function saveSequencePosition(courseId, sequenceId, activeUnitIndex) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveIntegritySignature(courseId, isMasquerading) {
|
export function saveIntegritySignature(courseId) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
try {
|
try {
|
||||||
// If the request is made by a staff user masquerading as a learner,
|
await postIntegritySignature(courseId);
|
||||||
// don't actually create a signature for them on the backend. Only
|
|
||||||
// frontend state will be updated.
|
|
||||||
if (!isMasquerading) {
|
|
||||||
await postIntegritySignature(courseId);
|
|
||||||
}
|
|
||||||
dispatch(updateModel({
|
dispatch(updateModel({
|
||||||
modelType: 'coursewareMeta',
|
modelType: 'coursewareMeta',
|
||||||
model: {
|
model: {
|
||||||
|
|||||||
@@ -11,6 +11,16 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'register',
|
defaultMessage: 'register',
|
||||||
description: 'Text in a link, prompting the user to create an account. Used in "learning.logistration.alert"',
|
description: 'Text in a link, prompting the user to create an account. Used in "learning.logistration.alert"',
|
||||||
},
|
},
|
||||||
|
registerSentenceCase: {
|
||||||
|
id: 'general.register.sentenceCase',
|
||||||
|
defaultMessage: 'Register',
|
||||||
|
description: 'Text in a button, prompting the user to register.',
|
||||||
|
},
|
||||||
|
signInLowercase: {
|
||||||
|
id: 'learning.logistration.login', // ID left for historical purposes
|
||||||
|
defaultMessage: 'sign in',
|
||||||
|
description: 'Text in a link, prompting the user to log in. Used in "learning.logistration.alert"',
|
||||||
|
},
|
||||||
signInSentenceCase: {
|
signInSentenceCase: {
|
||||||
id: 'general.signIn.sentenceCase',
|
id: 'general.signIn.sentenceCase',
|
||||||
defaultMessage: 'Sign in',
|
defaultMessage: 'Sign in',
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { getNotices } from './api';
|
|
||||||
/**
|
|
||||||
* This component uses the platform-plugin-notices plugin to function.
|
|
||||||
* If the user has an unacknowledged notice, they will be rerouted off
|
|
||||||
* course home and onto a full-screen notice page. If the plugin is not
|
|
||||||
* installed, or there are no notices, we just passthrough this component.
|
|
||||||
*/
|
|
||||||
const NoticesProvider = ({ children }) => {
|
|
||||||
const [isRedirected, setIsRedirected] = useState();
|
|
||||||
useEffect(async () => {
|
|
||||||
if (getConfig().ENABLE_NOTICES) {
|
|
||||||
const data = await getNotices();
|
|
||||||
if (data && data.results && data.results.length > 0) {
|
|
||||||
const { results } = data;
|
|
||||||
setIsRedirected(true);
|
|
||||||
window.location.replace(`${results[0]}?next=${window.location.href}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{isRedirected === true ? null : children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
NoticesProvider.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NoticesProvider;
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import {
|
|
||||||
initializeMockApp,
|
|
||||||
render,
|
|
||||||
act,
|
|
||||||
} from '../../setupTest';
|
|
||||||
import NoticesProvider from './NoticesProvider';
|
|
||||||
import { getNotices } from './api';
|
|
||||||
|
|
||||||
jest.mock('./api', () => ({
|
|
||||||
getNotices: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform', () => ({
|
|
||||||
getConfig: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('NoticesProvider', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
jest.resetModules();
|
|
||||||
await initializeMockApp();
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildAndRender() {
|
|
||||||
render(
|
|
||||||
<NoticesProvider>
|
|
||||||
<div />
|
|
||||||
</NoticesProvider>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('does not call api if ENABLE_NOTICES is false', () => {
|
|
||||||
getConfig.mockImplementation(() => ({ ENABLE_NOTICES: false }));
|
|
||||||
buildAndRender();
|
|
||||||
expect(getNotices).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redirects user on notice returned from API', async () => {
|
|
||||||
const redirectUrl = 'http://example.com/test_route';
|
|
||||||
getConfig.mockImplementation(() => ({ ENABLE_NOTICES: true }));
|
|
||||||
getNotices.mockImplementation(() => ({ results: [redirectUrl] }));
|
|
||||||
delete window.location;
|
|
||||||
window.location = { replace: jest.fn() };
|
|
||||||
process.env.ENABLE_NOTICES = true;
|
|
||||||
await act(async () => buildAndRender());
|
|
||||||
expect(window.location.replace).toHaveBeenCalledWith(`${redirectUrl}?next=${window.location.href}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not redirect on no data', async () => {
|
|
||||||
getNotices.mockImplementation(() => ({}));
|
|
||||||
getConfig.mockImplementation(() => ({ ENABLE_NOTICES: true }));
|
|
||||||
delete window.location;
|
|
||||||
window.location = { replace: jest.fn() };
|
|
||||||
process.env.ENABLE_NOTICES = true;
|
|
||||||
await act(async () => buildAndRender());
|
|
||||||
expect(window.location.replace).toHaveBeenCalledTimes(0);
|
|
||||||
expect(window.location.toString() === 'http://localhost/');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
|
||||||
import { logError, logInfo } from '@edx/frontend-platform/logging';
|
|
||||||
|
|
||||||
export const getNotices = async () => {
|
|
||||||
const authenticatedUser = getAuthenticatedUser();
|
|
||||||
const url = new URL(`${getConfig().LMS_BASE_URL}/notices/api/v1/unacknowledged`);
|
|
||||||
if (authenticatedUser) {
|
|
||||||
try {
|
|
||||||
const { data } = await getAuthenticatedHttpClient().get(url.href, {});
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
// we will just swallow error, as that probably means the notices app is not installed.
|
|
||||||
// Notices are not necessary for the rest of courseware to function.
|
|
||||||
const { customAttributes: { httpErrorStatus } } = e;
|
|
||||||
if (httpErrorStatus === 404) {
|
|
||||||
logInfo(`${e}. This probably happened because the notices plugin is not installed on platform.`);
|
|
||||||
} else {
|
|
||||||
logError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './NoticesProvider';
|
|
||||||
@@ -1,399 +1,399 @@
|
|||||||
{
|
{
|
||||||
"learning.accessExpiration.deadline": "Mettez à niveau avant le {date} pour obtenir un accès illimité au cours tant qu'il existe sur le site.",
|
"learning.accessExpiration.deadline": "Upgrade by {date} to get unlimited access to the course as long as it exists on the site.",
|
||||||
"learning.accessExpiration.header": "L’accès à l’audit expire le {date}",
|
"learning.accessExpiration.header": "Audit Access Expires {date}",
|
||||||
"learning.accessExpiration.body": "Vous perdez tout accès à ce cours, y compris votre progression, le {date}.",
|
"learning.accessExpiration.body": "You lose all access to this course, including your progress, on {date}.",
|
||||||
"instructorToolbar.pageBanner.courseHasExpired": "Cet apprenant n'a plus accès à ce cours. Leur accès a expiré le {date}.",
|
"instructorToolbar.pageBanner.courseHasExpired": "This learner no longer has access to this course. Their access expired on {date}.",
|
||||||
"learning.accessExpiration.upgradeNow": "Mettre à jour dès maintenant",
|
"learning.accessExpiration.upgradeNow": "Upgrade now",
|
||||||
"learning.outline.alert.start.short": "Le cours commence dans {timeRemaining} à {courseStartTime}.",
|
"learning.outline.alert.start.short": "Course starts {timeRemaining} at {courseStartTime}.",
|
||||||
"learning.outline.alert.end.long": "Ce cours se termine dans {timeRemaining}, le {courseEndDate}.",
|
"learning.outline.alert.end.long": "This course is ending {timeRemaining} on {courseEndDate}.",
|
||||||
"learning.outline.alert.end.calendar": "N'oubliez pas d'ajouter un rappel dans le calendrier!",
|
"learning.outline.alert.end.calendar": "Don’t forget to add a calendar reminder!",
|
||||||
"instructorToolbar.pageBanner.courseHasNotStarted": "Cet apprenant n'a pas encore accès à ce cours. Le cours commencera le {date}.",
|
"instructorToolbar.pageBanner.courseHasNotStarted": "This learner does not yet have access to this course. The course starts on {date}.",
|
||||||
"learning.enrollment.alert": "Vous devez être inscrit à ce cours pour voir le contenu du cours.",
|
"learning.enrollment.alert": "You must be enrolled in the course to see course content.",
|
||||||
"learning.staff.enrollment.alert": "Vous regardez ce cours en tant que membre de l'équipe, vous n'êtes pas inscrits.",
|
"learning.staff.enrollment.alert": "You are viewing this course as staff, and are not enrolled.",
|
||||||
"learning.enrollment.enrollNow.Inline": "Inscrivez-vous maintenant.",
|
"learning.enrollment.enrollNow.Inline": "Enroll now",
|
||||||
"learning.enrollment.enrollNow.Sentence": "Inscrivez-vous maintenant.",
|
"learning.enrollment.enrollNow.Sentence": "Enroll now.",
|
||||||
"learning.enrollment.success": "Vous vous êtes inscrits avec succès à ce cours!",
|
"learning.enrollment.success": "You've successfully enrolled in this course!",
|
||||||
"account-activation.alert.title": "Activez votre compte afin de pouvoir vous reconnecter",
|
"account-activation.alert.title": "Activate your account so you can log back in",
|
||||||
"account-activation.alert.button": "Continuer vers {siteName}",
|
"account-activation.alert.button": "Continue to {siteName}",
|
||||||
"account-activation.alert.message": "Nous avons envoyé un courriel à {boldEmail} contenant un lien pour activer votre compte. Il est introuvable ? Vérifiez votre dossier de pourriel ou\n {sendEmailTag}.",
|
"account-activation.alert.message": "We sent an email to {boldEmail} with a link to activate your account. Can’t find it? Check your spam folder or\n {sendEmailTag}.",
|
||||||
"account-activation.resend.link": "renvoyer le courriel",
|
"account-activation.resend.link": "resend the email",
|
||||||
"learning.logistration.alert": "Pour afficher le contenu du cours, {signIn} ou {register}.",
|
"learning.logistration.alert": "To see course content, {signIn} or {register}.",
|
||||||
"learn.navigation.course.tabs.label": "Matériel de cours",
|
"learn.navigation.course.tabs.label": "Course Material",
|
||||||
"header.menu.dashboard.label": "Tableau de bord",
|
"header.menu.dashboard.label": "Dashboard",
|
||||||
"header.help.label": "Aide",
|
"header.help.label": "Help",
|
||||||
"header.menu.profile.label": "Profil",
|
"header.menu.profile.label": "Profile",
|
||||||
"header.menu.account.label": "Compte",
|
"header.menu.account.label": "Account",
|
||||||
"header.menu.orderHistory.label": "Historique des commandes",
|
"header.menu.orderHistory.label": "Order History",
|
||||||
"header.navigation.skipNavLink": "Passer au contenu principal",
|
"header.navigation.skipNavLink": "Skip to main content.",
|
||||||
"header.menu.signOut.label": "Se déconnecter",
|
"header.menu.signOut.label": "Sign Out",
|
||||||
"learning.dates.badge.completed": "Terminé",
|
"learning.dates.badge.completed": "Completed",
|
||||||
"learning.dates.badge.dueNext": "À venir",
|
"learning.dates.badge.dueNext": "Due next",
|
||||||
"learning.dates.badge.pastDue": "En retard",
|
"learning.dates.badge.pastDue": "Past due",
|
||||||
"learning.dates.title": "Dates importantes",
|
"learning.dates.title": "Important dates",
|
||||||
"learning.dates.badge.today": "Aujourd'hui",
|
"learning.dates.badge.today": "Today",
|
||||||
"learning.dates.badge.unreleased": "Pas encore publié",
|
"learning.dates.badge.unreleased": "Not yet released",
|
||||||
"learning.dates.badge.verifiedOnly": "Vérifié seulement",
|
"learning.dates.badge.verifiedOnly": "Verified only",
|
||||||
"learning.goals.unsubscribe.contact": "contacter le support",
|
"learning.goals.unsubscribe.contact": "contact support",
|
||||||
"learning.goals.unsubscribe.description": "Vous ne recevrez plus de rappels par courriel au sujet de votre objectif pour {courseTitle}.",
|
"learning.goals.unsubscribe.description": "You will no longer receive email reminders about your goal for {courseTitle}.",
|
||||||
"learning.goals.unsubscribe.errorHeader": "Quelque chose s'est mal passé",
|
"learning.goals.unsubscribe.errorHeader": "Something went wrong",
|
||||||
"learning.goals.unsubscribe.goToDashboard": "Aller au tableau de bord",
|
"learning.goals.unsubscribe.goToDashboard": "Go to dashboard",
|
||||||
"learning.goals.unsubscribe.header": "Vous vous êtes désabonné des rappels d'objectifs",
|
"learning.goals.unsubscribe.header": "You’ve unsubscribed from goal reminders",
|
||||||
"learning.goals.unsubscribe.loading": "Désinscription...",
|
"learning.goals.unsubscribe.loading": "Unsubscribing…",
|
||||||
"learning.goals.unsubscribe.errorDescription": "Nous n'avons pas pu vous désinscrire des courriels de rappel d'objectif. Veuillez réessayer plus tard ou {contactSupport} pour obtenir de l'aide.",
|
"learning.goals.unsubscribe.errorDescription": "We were unable to unsubscribe you from goal reminder emails. Please try again later or {contactSupport} for help.",
|
||||||
"learning.outline.alert.cert.when": "Ce cours se termine le {courseEndDateFormatted}. Les notes finales et les attestations\n devraient être disponibles après {certificateAvailableDate}.",
|
"learning.outline.alert.cert.when": "This course ends on {courseEndDateFormatted}. Final grades and certificates are\n scheduled to be available after {certificateAvailableDate}.",
|
||||||
"cert.alert.earned.unavailable.header": "Votre note et votre attestation seront bientôt prêtes !",
|
"cert.alert.earned.unavailable.header": "Your grade and certificate will be ready soon!",
|
||||||
"cert.alert.earned.ready.header": "Félicitations ! Votre attestation est prête.",
|
"cert.alert.earned.ready.header": "Congratulations! Your certificate is ready.",
|
||||||
"cert.alert.notPassing.header": "Vous n'êtes pas éligible pour une attestation",
|
"cert.alert.notPassing.header": "You are not eligible for a certificate",
|
||||||
"cert.alert.notPassing.button": "Voir les notes",
|
"cert.alert.notPassing.button": "View grades",
|
||||||
"learning.outline.alert.end.short": "Le cours termine dans {timeRemaining} à {courseEndTime}.",
|
"learning.outline.alert.end.short": "This course is ending {timeRemaining} at {courseEndTime}.",
|
||||||
"alert.enroll": " pour accéder au cours complet.",
|
"alert.enroll": " to access the full course.",
|
||||||
"learning.privateCourse.signInOrRegister": "{signIn} ou {register}, puis inscrivez-vous à ce cours.",
|
"learning.privateCourse.signInOrRegister": "{signIn} or {register} and then enroll in this course.",
|
||||||
"learning.outline.alert.scheduled-content.heading": "Plus de contenu sera bientôt disponible!",
|
"learning.outline.alert.scheduled-content.heading": "More content is coming soon!",
|
||||||
"learning.outline.alert.scheduled-content.body": "Ce cours aura plus de contenu publié à une date future. Surveillez les mises à jour par courriel ou revenez voir ce cours pour les mises à jour.",
|
"learning.outline.alert.scheduled-content.body": "This course will have more content released at a future date. Look out for email updates or check back on this course for updates.",
|
||||||
"learning.outline.alert.scheduled-content.button": "Voir l'horaire du cours",
|
"learning.outline.alert.scheduled-content.button": "View Course Schedule",
|
||||||
"learning.outline.dates.all": "Voir toutes les dates de cours",
|
"learning.outline.dates.all": "View all course dates",
|
||||||
"learning.outline.collapseAll": "Tout replier",
|
"learning.outline.collapseAll": "Collapse all",
|
||||||
"learning.outline.completedAssignment": "Terminé",
|
"learning.outline.completedAssignment": "Completed",
|
||||||
"learning.outline.completedSection": "Section complétée",
|
"learning.outline.completedSection": "Completed section",
|
||||||
"learning.outline.dates": "Dates importantes",
|
"learning.outline.dates": "Important dates",
|
||||||
"learning.outline.editGoal": "Modifier l'objectif",
|
"learning.outline.editGoal": "Edit goal",
|
||||||
"learning.outline.expandAll": "Tout développer",
|
"learning.outline.expandAll": "Expand all",
|
||||||
"learning.outline.goal": "Objectif",
|
"learning.outline.goal": "Goal",
|
||||||
"learning.outline.goalUnsure": "Décider plus tard",
|
"learning.outline.goalUnsure": "Not sure yet",
|
||||||
"learning.outline.handouts": "Documents de cours",
|
"learning.outline.handouts": "Course Handouts",
|
||||||
"learning.outline.incompleteAssignment": "Inachevé",
|
"learning.outline.incompleteAssignment": "Incomplete",
|
||||||
"learning.outline.incompleteSection": "Section incomplète",
|
"learning.outline.incompleteSection": "Incomplete section",
|
||||||
"learning.outline.learnMore": "En savoir plus",
|
"learning.outline.learnMore": "Learn More",
|
||||||
"learning.outline.altText.openSection": "Ouvrir",
|
"learning.outline.altText.openSection": "Open",
|
||||||
"learning.outline.resume": "Poursuivre le cours",
|
"learning.outline.resume": "Resume course",
|
||||||
"learning.outline.setGoal": "Pour commencer, définissez un objectif de cours en sélectionnant l'option ci-dessous qui décrit le mieux votre plan d'apprentissage.",
|
"learning.outline.setGoal": "To start, set a course goal by selecting the option below that best describes your learning plan.",
|
||||||
"learning.outline.start": "Commencer le cours",
|
"learning.outline.start": "Start Course",
|
||||||
"learning.outline.tools": "Outils de cours",
|
"learning.outline.tools": "Course Tools",
|
||||||
"learning.outline.upgradeButton": "Mise-à-jour ({symbol}{price})",
|
"learning.outline.upgradeButton": "Upgrade ({symbol}{price})",
|
||||||
"learning.outline.upgradeTitle": "Obtenir un certificat vérifié",
|
"learning.outline.upgradeTitle": "Pursue a verified certificate",
|
||||||
"learning.outline.certificateAlt": "Exemple d'attestation",
|
"learning.outline.certificateAlt": "Example Certificate",
|
||||||
"learning.outline.welcomeMessage": "Message de bienvenue",
|
"learning.outline.welcomeMessage": "Welcome Message",
|
||||||
"learning.outline.welcomeMessageShowMoreButton": "Montrer plus",
|
"learning.outline.welcomeMessageShowMoreButton": "Show More",
|
||||||
"learning.outline.welcomeMessageShowLessButton": "Montrer moins",
|
"learning.outline.welcomeMessageShowLessButton": "Show Less",
|
||||||
"learning.outline.goalWelcome": "Bienvenue sur",
|
"learning.outline.goalWelcome": "Welcome to",
|
||||||
"learning.proctoringPanel.header": "Ce cours contient des examens surveillés",
|
"learning.proctoringPanel.header": "This course contains proctored exams",
|
||||||
"learning.proctoringPanel.status.notStarted": "Non démarré",
|
"learning.proctoringPanel.status.notStarted": "Not Started",
|
||||||
"learning.proctoringPanel.status.started": "Débuté",
|
"learning.proctoringPanel.status.started": "Started",
|
||||||
"learning.proctoringPanel.status.submitted": "Soumis",
|
"learning.proctoringPanel.status.submitted": "Submitted",
|
||||||
"learning.proctoringPanel.status.verified": "Vérifié",
|
"learning.proctoringPanel.status.verified": "Verified",
|
||||||
"learning.proctoringPanel.status.rejected": "Rejeté",
|
"learning.proctoringPanel.status.rejected": "Rejected",
|
||||||
"learning.proctoringPanel.status.error": "Erreur",
|
"learning.proctoringPanel.status.error": "Error",
|
||||||
"learning.proctoringPanel.status.otherCourseApproved": "Approuvé dans un autre cours",
|
"learning.proctoringPanel.status.otherCourseApproved": "Approved in Another Course",
|
||||||
"learning.proctoringPanel.status.expiringSoon": "Expire bientôt",
|
"learning.proctoringPanel.status.expiringSoon": "Expiring Soon",
|
||||||
"learning.proctoringPanel.status": "Statut actuel d'intégration :",
|
"learning.proctoringPanel.status": "Current Onboarding Status:",
|
||||||
"learning.proctoringPanel.message.notStarted": "Vous n'avez pas commencé votre examen d'intégration.",
|
"learning.proctoringPanel.message.notStarted": "You have not started your onboarding exam.",
|
||||||
"learning.proctoringPanel.message.started": "Vous avez commencé votre examen d'intégration.",
|
"learning.proctoringPanel.message.started": "You have started your onboarding exam.",
|
||||||
"learning.proctoringPanel.message.submitted": "Vous avez soumis votre examen d'intégration.",
|
"learning.proctoringPanel.message.submitted": "You have submitted your onboarding exam.",
|
||||||
"learning.proctoringPanel.message.verified": "Votre examen d'intégration a été approuvé pour ce cours.",
|
"learning.proctoringPanel.message.verified": "Your onboarding exam has been approved in this course.",
|
||||||
"learning.proctoringPanel.message.rejected": "Votre examen d'intégration a été rejeté. Veuillez réessayer l'intégration.",
|
"learning.proctoringPanel.message.rejected": "Your onboarding exam has been rejected. Please retry onboarding.",
|
||||||
"learning.proctoringPanel.message.error": "Une erreur s'est produite lors de votre examen d'intégration. Veuillez réessayer l'intégration.",
|
"learning.proctoringPanel.message.error": "An error has occurred during your onboarding exam. Please retry onboarding.",
|
||||||
"learning.proctoringPanel.message.otherCourseApproved": "Votre examen d'intégration a été approuvé dans un autre cours.",
|
"learning.proctoringPanel.message.otherCourseApproved": "Your onboarding exam has been approved in another course.",
|
||||||
"learning.proctoringPanel.detail.otherCourseApproved": "Si votre appareil a changé, nous vous recommandons de passer l'examen d'intégration de ce cours afin de vous assurer que votre configuration répond toujours aux exigences de surveillance.",
|
"learning.proctoringPanel.detail.otherCourseApproved": "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.",
|
||||||
"learning.proctoringPanel.message.expiringSoon": "Votre profil d'intégration a été approuvé dans un autre cours. Cependant, votre statut d'intégration expire bientôt. Veuillez compléter à nouveau l'intégration afin que vous soyez en mesure de continuer à passer des examens surveillés.",
|
"learning.proctoringPanel.message.expiringSoon": "Your onboarding profile has been approved in another course. However, your onboarding status is expiring soon. Please complete onboarding again to ensure that you will be able to continue taking proctored exams.",
|
||||||
"learning.proctoringPanel.generalInfo": "Vous devez terminer le processus d'intégration avant de passer un examen surveillé.",
|
"learning.proctoringPanel.generalInfo": "You must complete the onboarding process prior to taking any proctored exam. ",
|
||||||
"learning.proctoringPanel.generalInfoSubmitted": "Votre profil soumis est en cours de révision.",
|
"learning.proctoringPanel.generalInfoSubmitted": "Your submitted profile is in review.",
|
||||||
"learning.proctoringPanel.generalTime": "L'examen du profil d'intégration peut prendre plus de 2 jours ouvrables.",
|
"learning.proctoringPanel.generalTime": "Onboarding profile review can take 2+ business days.",
|
||||||
"learning.proctoringPanel.onboardingButton": "Complétez l'intégration",
|
"learning.proctoringPanel.onboardingButton": "Complete Onboarding",
|
||||||
"learning.proctoringPanel.onboardingPracticeButton": "Voir l'examen d'intégration",
|
"learning.proctoringPanel.onboardingPracticeButton": "View Onboarding Exam",
|
||||||
"learning.proctoringPanel.onboardingButtonNotOpen": "Ouverture de l'intégration : {releaseDate}",
|
"learning.proctoringPanel.onboardingButtonNotOpen": "Onboarding Opens: {releaseDate}",
|
||||||
"learning.proctoringPanel.reviewRequirementsButton": "Examiner les instructions et la configuration système requise",
|
"learning.proctoringPanel.reviewRequirementsButton": "Review instructions and system requirements",
|
||||||
"learning.proctoringPanel.onboardingButtonPastDue": "Intégration en retard",
|
"learning.proctoringPanel.onboardingButtonPastDue": "Onboarding Past Due",
|
||||||
"learning.outline.sequence-due": "{description} échéance {assignmentDue}",
|
"learning.outline.sequence-due": "{description} due {assignmentDue}",
|
||||||
"progress.certificateStatus.unverifiedBody": "Afin de générer une attestation, vous devez effectuer une vérification d'identité. {idVerificationSupportLink}.",
|
"progress.certificateStatus.unverifiedBody": "In order to generate a certificate, you must complete ID verification. {idVerificationSupportLink}.",
|
||||||
"progress.certificateStatus.downloadableBody": "Présentez vos réalisations sur LinkedIn ou votre curriculum vitae aujourd'hui. Vous pouvez télécharger votre certificat maintenant et y accéder à tout moment depuis votre tableau de bord et votre profil.",
|
"progress.certificateStatus.downloadableBody": "Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.",
|
||||||
"courseCelebration.certificateBody.notAvailable.endDate": "Ce cours se termine le {endDate} et les notes finales et les attestations sont programmées pour être\n disponibles après le {certAvailableDate}.",
|
"courseCelebration.certificateBody.notAvailable.endDate": "This course ended on {endDate} and final grades and certificates are scheduled to be\n available after {certAvailableDate}.",
|
||||||
"progress.certificateStatus.notPassingHeader": "État de l'attestation",
|
"progress.certificateStatus.notPassingHeader": "Certificate status",
|
||||||
"progress.certificateStatus.notPassingBody": "Pour être admissible à une attestation, vous devez avoir la note de passage.",
|
"progress.certificateStatus.notPassingBody": "In order to qualify for a certificate, you must have a passing grade.",
|
||||||
"progress.certificateStatus.inProgressHeader": "Plus de contenu sera bientôt disponible!",
|
"progress.certificateStatus.inProgressHeader": "More content is coming soon!",
|
||||||
"progress.certificateStatus.inProgressBody": "Il semble qu'il y ait plus de contenu dans ce cours qui sera publié dans le futur. Attendez les mises à jour par courriel ou revenez sur votre cours pour savoir quand ce contenu sera disponible.",
|
"progress.certificateStatus.inProgressBody": "It looks like there is more content in this course that will be released in the future. Look out for email updates or check back on your course for when this content will be available.",
|
||||||
"progress.certificateStatus.requestableHeader": "État de l'attestation",
|
"progress.certificateStatus.requestableHeader": "Certificate status",
|
||||||
"progress.certificateStatus.requestableBody": "Félicitations, vous vous qualifiez pour une attestation! Pour accéder à votre attestation, demandez-la ci-dessous.",
|
"progress.certificateStatus.requestableBody": "Congratulations, you qualified for a certificate! In order to access your certificate, request it below.",
|
||||||
"progress.certificateStatus.requestableButton": "Demander une attestation",
|
"progress.certificateStatus.requestableButton": "Request certificate",
|
||||||
"progress.certificateStatus.unverifiedHeader": "État de l'attestation",
|
"progress.certificateStatus.unverifiedHeader": "Certificate status",
|
||||||
"progress.certificateStatus.unverifiedButton": "Vérifiez votre identité",
|
"progress.certificateStatus.unverifiedButton": "Verify ID",
|
||||||
"progress.certificateStatus.courseCelebration.verificationPending": "La vérification de votre identité est en attente et votre attestation sera disponible une fois approuvé.",
|
"progress.certificateStatus.courseCelebration.verificationPending": "Your ID verification is pending and your certificate will be available once approved.",
|
||||||
"progress.certificateStatus.downloadableHeader": "Votre attestation est disponible!",
|
"progress.certificateStatus.downloadableHeader": "Your certificate is available!",
|
||||||
"progress.certificateStatus.downloadableButton": "Téléchargez mon attestation",
|
"progress.certificateStatus.downloadableButton": "Download my certificate",
|
||||||
"progress.certificateStatus.viewableButton": "Voir mon attestation",
|
"progress.certificateStatus.viewableButton": "View my certificate",
|
||||||
"progress.certificateStatus.notAvailableHeader": "État de l'attestation",
|
"progress.certificateStatus.notAvailableHeader": "Certificate status",
|
||||||
"progress.certificateStatus.upgradeHeader": "Obtenir un certificat",
|
"progress.certificateStatus.upgradeHeader": "Earn a certificate",
|
||||||
"progress.certificateStatus.upgradeBody": "Vous êtes dans une piste d'audit et n'êtes pas admissible à une attestation. Afin d'obtenir vers une attestation, mettez à niveau votre cours dès aujourd'hui.",
|
"progress.certificateStatus.upgradeBody": "You are in an audit track and do not qualify for a certificate. In order to work towards a certificate, upgrade your course today.",
|
||||||
"progress.certificateStatus.upgradeButton": "Mettre à jour dès maintenant",
|
"progress.certificateStatus.upgradeButton": "Upgrade now",
|
||||||
"progress.certificateStatus.unverifiedHomeHeader": "Vérifiez votre identité pour obtenir une attestation !",
|
"progress.certificateStatus.unverifiedHomeHeader": "Verify your identity to earn a certificate!",
|
||||||
"progress.certificateStatus.unverifiedHomeButton": "Vérifiez mon identité",
|
"progress.certificateStatus.unverifiedHomeButton": "Verify my ID",
|
||||||
"progress.certificateStatus.unverifiedHomeBody": "Afin de générer une attestation pour ce cours, vous devez compléter le processus de vérification d'identité.",
|
"progress.certificateStatus.unverifiedHomeBody": "In order to generate a certificate for this course, you must complete the ID verification process.",
|
||||||
"progress.completion.donut.label": "achevée",
|
"progress.completion.donut.label": "completed",
|
||||||
"progress.completion.body": "Cela représente la part du contenu du cours que vous avez terminé. Notez que certains contenus peuvent ne pas encore être publiés.",
|
"progress.completion.body": "This represents how much of the course content you have completed. Note that some content may not yet be released.",
|
||||||
"progress.completion.tooltip.locked": "Contenu que vous avez terminé.",
|
"progress.completion.tooltip.locked": "Content that you have completed.",
|
||||||
"progress.completion.header": "Achèvement du cours",
|
"progress.completion.header": "Course completion",
|
||||||
"progress.completion.tooltip": "Contenu auquel vous avez accès et que vous n'avez pas terminé.",
|
"progress.completion.tooltip": "Content that you have access to and have not completed.",
|
||||||
"progress.completion.tooltip.complete": "Contenu verrouillé et disponible uniquement pour ceux qui effectuent une mise à niveau.",
|
"progress.completion.tooltip.complete": "Content that is locked and available only to those who upgrade.",
|
||||||
"progress.completion.donut.percentComplete": "Vous avez terminé {percent}% du contenu de ce cours.",
|
"progress.completion.donut.percentComplete": "You have completed {percent}% of content in this course.",
|
||||||
"progress.completion.donut.percentIncomplete": "Vous n'avez pas terminé {percent}% du contenu de ce cours auquel vous avez accès.",
|
"progress.completion.donut.percentIncomplete": "You have not completed {percent}% of content in this course that you have access to.",
|
||||||
"progress.completion.donut.percentLocked": "{percent}% du contenu de ce cours est verrouillé et disponible uniquement pour ceux qui effectuent une mise à niveau.",
|
"progress.completion.donut.percentLocked": "{percent}% of content in this course is locked and available only for those who upgrade.",
|
||||||
"progress.ungradedAlert": "Pour connaître la progression des aspects non notés du cours, consultez votre {outlineLink}.",
|
"progress.ungradedAlert": "For progress on ungraded aspects of the course, view your {outlineLink}.",
|
||||||
"progress.footnotes.droppableAssignments": "Le plus bas {numDroppable, plural, one{# {assignmentType} score} autre{# {assignmentType} scores}} supprimé.",
|
"progress.footnotes.droppableAssignments": "The lowest {numDroppable, plural, one{# {assignmentType} score is} other{# {assignmentType} scores are}} dropped.",
|
||||||
"progress.assignmentType": "Type d'évaluation",
|
"progress.assignmentType": "Assignment type",
|
||||||
"progress.footnotes.backToContent": "Retour au contenu",
|
"progress.footnotes.backToContent": "Back to content",
|
||||||
"progress.courseGrade.body": "Cela représente votre note pondérée par rapport à la note nécessaire pour réussir ce cours.",
|
"progress.courseGrade.body": "This represents your weighted grade against the grade needed to pass this course.",
|
||||||
"progress.courseGrade.gradeBar.altText": "Votre note actuelle est {currentGrade}%. Une note pondérée de {passingGrade}% est nécessaire afin de réussir ce cours.",
|
"progress.courseGrade.gradeBar.altText": "Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.",
|
||||||
"progress.courseGrade.footer.generic.passing": "Vous réussissez actuellement ce cours",
|
"progress.courseGrade.footer.generic.passing": "You’re currently passing this course",
|
||||||
"progress.courseGrade.footer.nonPassing": "Une note pondérée de {passingGrade}% est nécessaire pour réussir ce cours.",
|
"progress.courseGrade.footer.nonPassing": "A weighted grade of {passingGrade}% is required to pass in this course",
|
||||||
"progress.courseGrade.footer.passing": "Vous réussissez actuellement ce cours avec une note de {letterGrade} ({minGrade}-{maxGrade}%)",
|
"progress.courseGrade.footer.passing": "You’re currently passing this course with a grade of {letterGrade} ({minGrade}-{maxGrade}%)",
|
||||||
"progress.courseGrade.preview.headerLocked": "fonction verrouillée",
|
"progress.courseGrade.preview.headerLocked": "locked feature",
|
||||||
"progress.courseGrade.preview.headerLimited": "fonctionnalité limitée",
|
"progress.courseGrade.preview.headerLimited": "limited feature",
|
||||||
"progress.courseGrade.preview.header.ariaHidden": "Aperçu d'un ",
|
"progress.courseGrade.preview.header.ariaHidden": "Preview of a ",
|
||||||
"progress.courseGrade.preview.body.unlockCertificate": "Déverrouillez pour afficher les notes et obtenir une attestation.",
|
"progress.courseGrade.preview.body.unlockCertificate": "Unlock to view grades and work towards a certificate.",
|
||||||
"progress.courseGrade.partialpreview.body.unlockCertificate": "Déverrouillez pour travailler en vue d'une attestation.",
|
"progress.courseGrade.partialpreview.body.unlockCertificate": "Unlock to work towards a certificate.",
|
||||||
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "La date limite de mise à niveau dans ce cours est écoulée.",
|
"progress.courseGrade.preview.body.upgradeDeadlinePassed": "The deadline to upgrade in this course has passed.",
|
||||||
"progress.courseGrade.preview.button.upgrade": "Mettre à jour dès maintenant",
|
"progress.courseGrade.preview.button.upgrade": "Upgrade now",
|
||||||
"progress.courseGrade.gradeRange.tooltip": "Plage de notes pour ce cours :",
|
"progress.courseGrade.gradeRange.tooltip": "Grade ranges for this course:",
|
||||||
"progress.courseOutline": "Plan du Cours",
|
"progress.courseOutline": "Course Outline",
|
||||||
"progress.courseGrade.label.currentGrade": "Votre note actuelle",
|
"progress.courseGrade.label.currentGrade": "Your current grade",
|
||||||
"progress.detailedGrades": "Notes détaillées",
|
"progress.detailedGrades": "Detailed grades",
|
||||||
"progress.detailedGrades.emptyTable": "Vous n'avez actuellement aucun score de problème noté.",
|
"progress.detailedGrades.emptyTable": "You currently have no graded problem scores.",
|
||||||
"progress.footnotes.title": "Notes de bas de page du résumé des notes",
|
"progress.footnotes.title": "Grade summary footnotes",
|
||||||
"progress.gradeSummary.grade": "Note",
|
"progress.gradeSummary.grade": "Grade",
|
||||||
"progress.courseGrade.grades": "Notes",
|
"progress.courseGrade.grades": "Grades",
|
||||||
"progress.courseGrade.gradeRange.Tooltip": "Info-bulle de plage de notes",
|
"progress.courseGrade.gradeRange.Tooltip": "Grade range tooltip",
|
||||||
"progress.gradeSummary": "Relevé de notes",
|
"progress.gradeSummary": "Grade summary",
|
||||||
"progress.gradeSummary.limitedAccessExplanation": "Vous avez un accès limité aux devoirs notés dans le cadre du parcours audit de ce cours.",
|
"progress.gradeSummary.limitedAccessExplanation": "You have limited access to graded assignments as part of the audit track in this course.",
|
||||||
"progress.gradeSummary.tooltip.alt": "Info-bulle des résumés de notes",
|
"progress.gradeSummary.tooltip.alt": "Grade summary tooltip",
|
||||||
"progress.gradeSummary.tooltip.body": "La pondération de ce travail est déterminé par votre instructeur. En multipliant votre note par la pondération du travail, une note pondérée est calculée. Cette note pondérée servira à déterminer si vous réussissez le cours.",
|
"progress.gradeSummary.tooltip.body": "Your course assignment's weight is determined by your instructor. By multiplying your grade by the weight for that assignment type, your weighted grade is calculated. Your weighted grade is what's used to determine if you pass the course.",
|
||||||
"progress.courseGrade.label.passingGrade": "Note de passage",
|
"progress.courseGrade.label.passingGrade": "Passing grade",
|
||||||
"progress.detailedGrades.problemScore.label": "Score aux exercices : ",
|
"progress.detailedGrades.problemScore.label": "Problem Scores:",
|
||||||
"progress.detailedGrades.problemScore.toggleButton": "Basculer les scores des problèmes individuels pour {subsectionTitle}",
|
"progress.detailedGrades.problemScore.toggleButton": "Toggle individual problem scores for {subsectionTitle}",
|
||||||
"progress.score": "Note",
|
"progress.score": "Score",
|
||||||
"progress.weight": "Pondération",
|
"progress.weight": "Weight",
|
||||||
"progress.weightedGrade": "Note pondérée",
|
"progress.weightedGrade": "Weighted grade",
|
||||||
"progress.weightedGradeSummary": "Votre résumé de note pondéré actuel",
|
"progress.weightedGradeSummary": "Your current weighted grade summary",
|
||||||
"progress.noAcessToAssignmentType": "Vous n'avez pas accès aux devoirs de type {assignmentType}",
|
"progress.noAcessToAssignmentType": "You do not have access to assignments of type {assignmentType}",
|
||||||
"progress.noAcessToSubsection": "Vous n'avez pas accès à la sous-section {displayName}",
|
"progress.noAcessToSubsection": "You do not have access to subsection {displayName}",
|
||||||
"progress.header": "Votre progression",
|
"progress.header": "Your progress",
|
||||||
"progress.header.targetUser": "Progression pour {username}",
|
"progress.header.targetUser": "Course progress for {username}",
|
||||||
"progress.link.studio": "Voir la notation dans Studio",
|
"progress.link.studio": "View grading in Studio",
|
||||||
"progress.relatedLinks.datesCard.description": "Une vue du calendrier des dates d'échéance de vos cours et des devoirs à venir.",
|
"progress.relatedLinks.datesCard.description": "A schedule view of your course due dates and upcoming assignments.",
|
||||||
"progress.relatedLinks.datesCard.link": "Dates",
|
"progress.relatedLinks.datesCard.link": "Dates",
|
||||||
"progress.relatedLinks.outlineCard.description": "Une vue d'ensemble du contenu de votre cours.",
|
"progress.relatedLinks.outlineCard.description": "A birds-eye view of your course content.",
|
||||||
"progress.relatedLinks.outlineCard.link": "Plan du Cours",
|
"progress.relatedLinks.outlineCard.link": "Course Outline",
|
||||||
"progress.relatedLinks": "Liens connexes",
|
"progress.relatedLinks": "Related links",
|
||||||
"datesBanner.suggestedSchedule": "Nous avons établi un calendrier suggéré pour vous aider à rester sur la bonne voie. Mais ne vous inquiétez pas—il est flexible et vous permet d'apprendre à votre rythme.",
|
"datesBanner.suggestedSchedule": "We’ve built a suggested schedule to help you stay on track. But don’t worry—it’s flexible so you can learn at your own pace.",
|
||||||
"datesBanner.upgradeToCompleteGradedBanner.header": "Upgrade to unlock",
|
"datesBanner.upgradeToCompleteGradedBanner.header": "Upgrade to unlock",
|
||||||
"datesBanner.upgradeToCompleteGradedBanner.body": "Vous auditez ce cours, ce qui signifie que vous ne pouvez pas participer aux devoirs notés. Pour terminer les devoirs notés dans le cadre de ce cours, vous pouvez mettre à niveau dès aujourd'hui.",
|
"datesBanner.upgradeToCompleteGradedBanner.body": "You are auditing this course, which means that you are unable to participate in graded assignments. To complete graded assignments as part of this course, you can upgrade today.",
|
||||||
"datesBanner.upgradeToCompleteGradedBanner.button": "Mettre à jour dès maintenant",
|
"datesBanner.upgradeToCompleteGradedBanner.button": "Upgrade now",
|
||||||
"datesBanner.upgradeToResetBanner.body": "To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.",
|
"datesBanner.upgradeToResetBanner.body": "To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.",
|
||||||
"datesBanner.upgradeToResetBanner.button": "Réviser votre type d'inscription pour déplacer les dates limites",
|
"datesBanner.upgradeToResetBanner.button": "Upgrade to shift due dates",
|
||||||
"datesBanner.resetDatesBanner.header": "Il semble que vous ayez manqué des dates limites importantes en fonction de notre calendrier suggéré.",
|
"datesBanner.resetDatesBanner.header": "It looks like you missed some important deadlines based on our suggested schedule.",
|
||||||
"datesBanner.resetDatesBanner.body": "To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.",
|
"datesBanner.resetDatesBanner.body": "To keep yourself on track, you can update this schedule and shift the past due assignments into the future. Don’t worry—you won’t lose any of the progress you’ve made when you shift your due dates.",
|
||||||
"datesBanner.resetDatesBanner.button": "Déplacer les dates limites",
|
"datesBanner.resetDatesBanner.button": "Shift due dates",
|
||||||
"unit.bookmark.button.add.bookmark": "Ajouter cette page aux favoris",
|
"unit.bookmark.button.add.bookmark": "Bookmark this page",
|
||||||
"unit.bookmark.button.remove.bookmark": "Ajouté aux favoris",
|
"unit.bookmark.button.remove.bookmark": "Bookmarked",
|
||||||
"learning.celebration.completed": "Vous avez complété la première section de votre cours.",
|
"learning.celebration.completed": "You just completed the first section of your course.",
|
||||||
"learning.celebration.congrats": "Félicitations !",
|
"learning.celebration.congrats": "Congratulations!",
|
||||||
"learning.celebration.earned": "Vous l'avez mérité!",
|
"learning.celebration.earned": "You earned it!",
|
||||||
"learning.celebration.emailSubject": "Je complèterai bientôt {title} en ligne avec {platform}!",
|
"learning.celebration.emailSubject": "I'm on my way to completing {title} online with {platform}!",
|
||||||
"learning.celebration.forward": "Continuer",
|
"learning.celebration.forward": "Keep going",
|
||||||
"learning.celebration.share": "Prenez un moment pour célébrer et partager votre progrès.",
|
"learning.celebration.share": "Take a moment to celebrate and share your progress.",
|
||||||
"learning.celebration.social": "Je suis sur le point de terminer {title} en ligne avec {platform}. Que passez-vous votre temps à apprendre?",
|
"learning.celebration.social": "I’m on my way to completing {title} online with {platform}. What are you spending your time learning?",
|
||||||
"calculator.instructions.button.label": "Instructions de la calculatrice",
|
"calculator.instructions.button.label": "Calculator Instructions",
|
||||||
"calculator.instructions": "Pour plus d'informations, consultez le {expressions_link}.",
|
"calculator.instructions": "For detailed information, see the {expressions_link}.",
|
||||||
"calculator.instructions.support.title": "Centre d'aide",
|
"calculator.instructions.support.title": "Help Center",
|
||||||
"calculator.instructions.useful.tips": "Conseils utiles:",
|
"calculator.instructions.useful.tips": "Useful tips:",
|
||||||
"calculator.hint1": "Utilisez les parenthèses () pour clarifier vos expressions. Vous pouvez utiliser des parenthèses au sein d'autres parenthèses.",
|
"calculator.hint1": "Use parentheses () to make expressions clear. You can use parentheses inside other parentheses.",
|
||||||
"calculator.hint2": "Ne pas utiliser d'espaces dans ces expressions.",
|
"calculator.hint2": "Do not use spaces in expressions.",
|
||||||
"calculator.hint3": "Pour les constantes, indiquer explicitement la multiplication (par exemple: 5*c).",
|
"calculator.hint3": "For constants, indicate multiplication explicitly (example: 5*c).",
|
||||||
"calculator.hint4": "Pour les suffixes, indiquer le nombre et le suffixe sans espace (exemple : 5c).",
|
"calculator.hint4": "For affixes, type the number and affix without a space (example: 5c).",
|
||||||
"calculator.hint5": "Pour les fonctions, taper le nom de la fonction, puis l'expression entre parenthèses.",
|
"calculator.hint5": "For functions, type the name of the function, then the expression in parentheses.",
|
||||||
"calculator.instruction.table.to.use.heading": "À utiliser",
|
"calculator.instruction.table.to.use.heading": "To Use",
|
||||||
"calculator.instruction.table.type.heading": "Type",
|
"calculator.instruction.table.type.heading": "Type",
|
||||||
"calculator.instruction.table.examples.heading": "Exemples",
|
"calculator.instruction.table.examples.heading": "Examples",
|
||||||
"calculator.instruction.table.to.use.numbers": "Nombres",
|
"calculator.instruction.table.to.use.numbers": "Numbers",
|
||||||
"calculator.instruction.table.to.use.numbers.type1": "Nombres entiers",
|
"calculator.instruction.table.to.use.numbers.type1": "Integers",
|
||||||
"calculator.instruction.table.to.use.numbers.type2": "Fractions",
|
"calculator.instruction.table.to.use.numbers.type2": "Fractions",
|
||||||
"calculator.instruction.table.to.use.numbers.type3": "Nombres décimaux",
|
"calculator.instruction.table.to.use.numbers.type3": "Decimals",
|
||||||
"calculator.instruction.table.to.use.operators": "Opérateurs",
|
"calculator.instruction.table.to.use.operators": "Operators",
|
||||||
"calculator.instruction.table.to.use.operators.type1": "(additionner, soustraire, multiplier, diviser)",
|
"calculator.instruction.table.to.use.operators.type1": "(add, subtract, multiply, divide)",
|
||||||
"calculator.instruction.table.to.use.operators.type2": "(élever à une puissance)",
|
"calculator.instruction.table.to.use.operators.type2": "(raise to a power)",
|
||||||
"calculator.instruction.table.to.use.operators.type3": "(résistances parallèles)",
|
"calculator.instruction.table.to.use.operators.type3": "(parallel resistors)",
|
||||||
"calculator.instruction.table.to.use.constants": "Constantes",
|
"calculator.instruction.table.to.use.constants": "Constants",
|
||||||
"calculator.instruction.table.to.use.affixes": "Affixes",
|
"calculator.instruction.table.to.use.affixes": "Affixes",
|
||||||
"calculator.instruction.table.to.use.affixes.type": "Pourcentage (%) et suffixes métriques ({affixes})",
|
"calculator.instruction.table.to.use.affixes.type": "Percent sign (%) and metric affixes ({affixes})",
|
||||||
"calculator.instruction.table.to.use.basic.functions": "Fonctions de base",
|
"calculator.instruction.table.to.use.basic.functions": "Basic functions",
|
||||||
"calculator.instruction.table.to.use.trig.functions": "Fonctions trigonométriques",
|
"calculator.instruction.table.to.use.trig.functions": "Trigonometric functions",
|
||||||
"calculator.instruction.table.to.use.scientific.notation": "Notation scientifique",
|
"calculator.instruction.table.to.use.scientific.notation": "Scientific notation",
|
||||||
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} et l'exposant",
|
"calculator.instruction.table.to.use.scientific.notation.type1": "{exponentSyntax} and the exponent",
|
||||||
"calculator.instruction.table.to.use.scientific.notation.type2": "notation {notationSyntax}",
|
"calculator.instruction.table.to.use.scientific.notation.type2": "{notationSyntax} notation",
|
||||||
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} et l'exposant",
|
"calculator.instruction.table.to.use.scientific.notation.type3": "{notationSyntax} and the exponent",
|
||||||
"calculator.button.label": "Calculatrice",
|
"calculator.button.label": "Calculator",
|
||||||
"calculator.input.field.label": "Sasie Calculatrice",
|
"calculator.input.field.label": "Calculator Input",
|
||||||
"calculator.submit.button.label": "Calculer",
|
"calculator.submit.button.label": "Calculate",
|
||||||
"calculator.result.field.label": "Résultat de la calculatrice",
|
"calculator.result.field.label": "Calculator Result",
|
||||||
"calculator.result.field.placeholder": "Résultat",
|
"calculator.result.field.placeholder": "Result",
|
||||||
"notes.button.show": "Afficher les notes",
|
"notes.button.show": "Show Notes",
|
||||||
"notes.button.hide": "Masquer les notes",
|
"notes.button.hide": "Hide Notes",
|
||||||
"courseExit.catalogSearchSuggestion": "Vous souhaitez en apprendre plus? {searchOurCatalogLink} pour trouver plus de cours et de programmes à explorer.",
|
"courseExit.catalogSearchSuggestion": "Looking to learn more? {searchOurCatalogLink} to find more courses and programs to explore.",
|
||||||
"courseCelebration.certificateBody.available": "\n Affichez vos accomplissements sur LinkedIn ou votre CV dès aujourd'hui.\n Vous pouvez télécharger votre attestation maintenant et y accéder à tout moment depuis vos\n {dashboardLink} et {profileLink}.",
|
"courseCelebration.certificateBody.available": "\n Showcase your accomplishment on LinkedIn or your resumé today.\n You can download your certificate now and access it any time from your\n {dashboardLink} and {profileLink}.",
|
||||||
"courseCelebration.certificateBody.unverified": "Afin de générer une attestation, vous devez effectuer une vérification d'identité.\n {idVerificationSupportLink} maintenant.",
|
"courseCelebration.certificateBody.unverified": "In order to generate a certificate, you must complete ID verification.\n {idVerificationSupportLink} now.",
|
||||||
"courseCelebration.certificateBody.upgradable": "Il n’est pas trop tard pour effectuer une mise à niveau. Pour {price}, vous débloquerez l'accès à tous les\n devoirs dans ce cours. À la fin, vous recevrez une attestation qui est une source\n d'informations précieuses pour améliorer vos perspectives d'emploi et faire progresser votre carrière, ou mettre en valeur votre\n attestation dans des demandes d'admission.",
|
"courseCelebration.certificateBody.upgradable": "It’s not too late to upgrade. For {price} you will unlock access to all graded\n assignments in this course. Upon completion, you will receive a verified certificate which is a\n valuable credential to improve your job prospects and advance your career, or highlight your\n certificate in school applications.",
|
||||||
"courseCelebration.upgradeDiscountCodePrompt": "Utilisez le code {code} lors du paiement pour {percent}% de réduction!",
|
"courseCelebration.upgradeDiscountCodePrompt": "Use code {code} at checkout for {percent}% off!",
|
||||||
"courseCelebration.recommendations.heading": "Continuez à développer vos compétences avec ces cours!",
|
"courseCelebration.recommendations.heading": "Keep building your skills with these courses!",
|
||||||
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}and } other { }}",
|
"courseCelebration.recommendations.formatting.list_join": "{style, select, punctuation {, } conjunction { {sp}and } other { }}",
|
||||||
"courseCelebration.recommendations.browse_catalog": "Explorez plus de cours",
|
"courseCelebration.recommendations.browse_catalog": "Explore more courses",
|
||||||
"courseCelebration.recommendations.loading_recommendations": "Chargement des recommandations",
|
"courseCelebration.recommendations.loading_recommendations": "Loading recommendations",
|
||||||
"courseCelebration.recommendations.card.schools.label": "Écoles et Partenaires",
|
"courseCelebration.recommendations.card.schools.label": "Schools and Partners",
|
||||||
"courseCelebration.recommendations.label": "Cours",
|
"courseCelebration.recommendations.label": "Course",
|
||||||
"courseCelebration.dashboardInfo": "Vous pouvez accéder à ce cours et à ses supports sur votre {dashboardLink}.",
|
"courseCelebration.dashboardInfo": "You can access this course and its materials on your {dashboardLink}.",
|
||||||
"courseExit.programs.applyForCredit": "Demander un crédit",
|
"courseExit.programs.applyForCredit": "Apply for credit",
|
||||||
"courseCelebration.certificateHeader.downloadable": "Votre attestation est disponible!",
|
"courseCelebration.certificateHeader.downloadable": "Your certificate is available!",
|
||||||
"courseCelebration.certificateHeader.notAvailable": "Votre note et votre attestation seront bientôt prêtes !",
|
"courseCelebration.certificateHeader.notAvailable": "Your grade and certificate will be ready soon!",
|
||||||
"courseCelebration.certificateBody.notAvailable.accessCertificate": "Si vous avez obtenu une note de passage, votre attestation sera automatiquement générée.",
|
"courseCelebration.certificateBody.notAvailable.accessCertificate": "If you have earned a passing grade, your certificate will be automatically issued.",
|
||||||
"courseCelebration.certificateHeader.unverified": "Vous devez avoir complété votre vérification pour recevoir votre attestation.",
|
"courseCelebration.certificateHeader.unverified": "You must complete verification to receive your certificate.",
|
||||||
"courseCelebration.certificateHeader.requestable": "Félicitations, vous avez terminé le processus pour passer un certificat !",
|
"courseCelebration.certificateHeader.requestable": "Congratulations, you qualified for a certificate!",
|
||||||
"courseCelebration.certificateHeader.upgradable": "Mettre à niveau pour obtenir une attestation",
|
"courseCelebration.certificateHeader.upgradable": "Upgrade to pursue a verified certificate",
|
||||||
"courseCelebration.certificateImage": "Exemple d'attestation",
|
"courseCelebration.certificateImage": "Sample certificate",
|
||||||
"courseCelebration.completedCourseHeader": "Vous avez complété ce cours.",
|
"courseCelebration.completedCourseHeader": "You have completed your course.",
|
||||||
"courseCelebration.congratulationsHeader": "Félicitations !",
|
"courseCelebration.congratulationsHeader": "Congratulations!",
|
||||||
"courseCelebration.congratulationsImage": "Quatre personnes levant leurs mains pour célébrer",
|
"courseCelebration.congratulationsImage": "Four people raising their hands in celebration",
|
||||||
"courseExit.courseInProgressDescription": "Il semble qu'il y ait plus de contenu dans ce cours qui sera publié dans le futur. Attendez les mises à jour par courriel ou revenez sur votre cours pour savoir quand ce contenu sera disponible.",
|
"courseExit.courseInProgressDescription": "It looks like there is more content in this course that will be released in the future. Look out for email updates or check back on your course for when this content will be available.",
|
||||||
"courseExit.courseInProgressHeader": "Plus de contenu sera bientôt disponible!",
|
"courseExit.courseInProgressHeader": "More content is coming soon!",
|
||||||
"courseExit.dashboardLink": "Tableau de bord",
|
"courseExit.dashboardLink": "Dashboard",
|
||||||
"courseCelebration.downloadButton": "Téléchargez mon attestation",
|
"courseCelebration.downloadButton": "Download my certificate",
|
||||||
"courseExit.endOfCourseDescription": "Malheureusement, vous n'êtes actuellement pas éligible pour une attestatation. Vous devez recevoir une note de passage pour être admissible à une attestation.",
|
"courseExit.endOfCourseDescription": "Unfortunately, you are not currently eligible for a certificate. You need to receive a passing grade to be eligible for a certificate.",
|
||||||
"courseExit.endOfCourseHeader": "Vous avez atteint la fin du cours!",
|
"courseExit.endOfCourseHeader": "You’ve reached the end of the course!",
|
||||||
"courseExit.endOfCourseTitle": "Fin du cours",
|
"courseExit.endOfCourseTitle": "End of Course",
|
||||||
"courseExit.idVerificationSupportLink": "En savoir plus sur la vérification d'identité",
|
"courseExit.idVerificationSupportLink": "Learn more about ID verification",
|
||||||
"courseCelebration.linkedinAddToProfileButton": "Ajouter au profil LinkedIn",
|
"courseCelebration.linkedinAddToProfileButton": "Add to LinkedIn profile",
|
||||||
"courseExit.programs.microBachelors.learnMore": "Apprenez-en davantage sur la façon dont votre accréditation MicroBachelors peut être utilisée pour demander un crédit.",
|
"courseExit.programs.microBachelors.learnMore": "Learn more about how your MicroBachelors credential can be applied for credit.",
|
||||||
"courseExit.programs.microMasters.learnMore": "En savoir plus sur le processus d’application des attestations MicroMasters aux diplômes de maîtrise.",
|
"courseExit.programs.microMasters.learnMore": "Learn more about the process of applying MicroMasters certificates to Master’s degrees.",
|
||||||
"courseExit.programs.microMasters.mastersMessage": "Si vous souhaitez utiliser votre attestation MicroMasters en vue d'un programme de maîtrise, vous pouvez commencer dès aujourd'hui!",
|
"courseExit.programs.microMasters.mastersMessage": "If you’re interested in using your MicroMasters certificate towards a Master’s program, you can get started today!",
|
||||||
"learn.sequence.navigation.complete.button": "Compléter le cours",
|
"learn.sequence.navigation.complete.button": "Complete the course",
|
||||||
"courseExit.nextButton.endOfCourse": "Suivant (fin du cours)",
|
"courseExit.nextButton.endOfCourse": "Next (end of course)",
|
||||||
"courseExit.profileLink": "Profil",
|
"courseExit.profileLink": "Profile",
|
||||||
"courseExit.programs.lastCourse": "Vous avez terminé le dernier cours de {title}!",
|
"courseExit.programs.lastCourse": "You have completed the last course in {title}!",
|
||||||
"courseCelebration.requestCertificateBodyText": "Pour accéder à votre certificat, demandez-le ci-dessous.",
|
"courseCelebration.requestCertificateBodyText": "In order to access your certificate, request it below.",
|
||||||
"courseCelebration.requestCertificateButton": "Demander une attestation",
|
"courseCelebration.requestCertificateButton": "Request certificate",
|
||||||
"courseExit.searchOurCatalogLink": "Rechercher dans notre catalogue",
|
"courseExit.searchOurCatalogLink": "Search our catalog",
|
||||||
"courseCelebration.shareMessage": "Partagez votre succès sur les réseaux sociaux ou par courriel.",
|
"courseCelebration.shareMessage": "Share your success on social media or email.",
|
||||||
"courseExit.social.shareCompletionMessage": "Je viens de terminer {title} avec {platform}!",
|
"courseExit.social.shareCompletionMessage": "I just completed {title} with {platform}!",
|
||||||
"courseExit.upgradeButton": "Mettre à jour dès maintenant",
|
"courseExit.upgradeButton": "Upgrade now",
|
||||||
"courseExit.upgradeLink": "mettre à jour maintenant",
|
"courseExit.upgradeLink": "upgrade now",
|
||||||
"courseCelebration.verificationPending": "La vérification de votre identité est en attente et votre attestation sera disponible une fois approuvé.",
|
"courseCelebration.verificationPending": "Your ID verification is pending and your certificate will be available once approved.",
|
||||||
"courseExit.verifiedCertificateSupportLink": "En savoir plus sur les attestations",
|
"courseExit.verifiedCertificateSupportLink": "Learn more about verified certificates",
|
||||||
"courseCelebration.verifyIdentityButton": "Vérifiez votre identité maintenant",
|
"courseCelebration.verifyIdentityButton": "Verify ID now",
|
||||||
"courseCelebration.viewCertificateButton": "Voir mon attestation",
|
"courseCelebration.viewCertificateButton": "View my certificate",
|
||||||
"courseExit.viewCourseScheduleButton": "Voir le calendrier de cours",
|
"courseExit.viewCourseScheduleButton": "View course schedule",
|
||||||
"courseExit.viewCoursesButton": "Voir mes cours",
|
"courseExit.viewCoursesButton": "View my courses",
|
||||||
"courseExit.viewGradesButton": "Voir les notes",
|
"courseExit.viewGradesButton": "View grades",
|
||||||
"courseExit.programCompletion.dashboardMessage": "Pour afficher l'état de votre attestation, consultez la section Programmes de votre {programLink}.",
|
"courseExit.programCompletion.dashboardMessage": "To view your certificate status, check the Programs section of your {programLink}.",
|
||||||
"courseExit.upgradeFootnote": "L'accès à ce cours et à ses supports est disponible sur votre tableau de bord jusqu'au {expirationDate}. Pour étendre l'accès, {upgradeLink}.",
|
"courseExit.upgradeFootnote": "Access to this course and its materials are available on your dashboard until {expirationDate}. To extend access, {upgradeLink}.",
|
||||||
"learn.course.license.allRightsReserved.text": "Tous droits réservés",
|
"learn.course.license.allRightsReserved.text": "All Rights Reserved",
|
||||||
"learn.course.license.creativeCommons.terms.preamble": "Contenu sous license Creative Commons, avec les conditions suivantes:",
|
"learn.course.license.creativeCommons.terms.preamble": "Creative Commons licensed content, with terms as follows:",
|
||||||
"learn.course.license.creativeCommons.terms.by": "Crédit",
|
"learn.course.license.creativeCommons.terms.by": "Attribution",
|
||||||
"learn.course.license.creativeCommons.terms.nc": "Non commercial",
|
"learn.course.license.creativeCommons.terms.nc": "Noncommercial",
|
||||||
"learn.course.license.creativeCommons.terms.nd": "Pas de travaux dérivés",
|
"learn.course.license.creativeCommons.terms.nd": "No Derivatives",
|
||||||
"learn.course.license.creativeCommons.terms.sa": "Partage à l'identique",
|
"learn.course.license.creativeCommons.terms.sa": "Share Alike",
|
||||||
"learn.course.license.creativeCommons.terms.zero": "Aucun termes",
|
"learn.course.license.creativeCommons.terms.zero": "No terms",
|
||||||
"learn.course.license.creativeCommons.text": "Quelques droits réservés",
|
"learn.course.license.creativeCommons.text": "Some Rights Reserved",
|
||||||
"learn.breadcrumb.navigation.course.home": "Cours",
|
"learn.breadcrumb.navigation.course.home": "Course",
|
||||||
"notification.tray.container": "Barre de notification",
|
"notification.tray.container": "Notification tray",
|
||||||
"notification.open.button": "Afficher la barre de notification",
|
"notification.open.button": "Show notification tray",
|
||||||
"notification.close.button": "Fermer la barre de notification",
|
"notification.close.button": "Close notification tray",
|
||||||
"responsive.close.notification": "Retour au cours",
|
"responsive.close.notification": "Back to course",
|
||||||
"notification.tray.title": "Notifications",
|
"notification.tray.title": "Notifications",
|
||||||
"notification.tray.no.message": "Vous n'avez aucune nouvelle notification pour le moment.",
|
"notification.tray.no.message": "You have no new notifications at this time.",
|
||||||
"learn.contentLock.content.locked": "Contenu vérouillé",
|
"learn.contentLock.content.locked": "Content Locked",
|
||||||
"learn.contentLock.complete.prerequisite": "Vous devez compléter le prérequis: '{prereqSectionName}' pour accéder à ce contenu.",
|
"learn.contentLock.complete.prerequisite": "You must complete the prerequisite: '{prereqSectionName}' to access this content.",
|
||||||
"learn.contentLock.goToSection": "Aller à la section des prérequis",
|
"learn.contentLock.goToSection": "Go To Prerequisite Section",
|
||||||
"learn.hiddenAfterDue.gradeAvailable": "Si vous avez complété ce travail, votre note est disponible sur {progressPage}.",
|
"learn.hiddenAfterDue.gradeAvailable": "If you have completed this assignment, your grade is available on the {progressPage}.",
|
||||||
"learn.hiddenAfterDue.header": "La date d'échéance de ce devoir est passée.",
|
"learn.hiddenAfterDue.header": "The due date for this assignment has passed.",
|
||||||
"learn.hiddenAfterDue.description": "Ce devoir n'est plus disponible, car la date limite est passée.",
|
"learn.hiddenAfterDue.description": "Because the due date has passed, this assignment is no longer available.",
|
||||||
"learn.hiddenAfterDue.progressPage": "page de progression",
|
"learn.hiddenAfterDue.progressPage": "progress page",
|
||||||
"learn.honorCode.content": "L'honnêteté et l'intégrité académique sont importantes pour {siteName} et les institutions offrant des cours et des programmes sur le site {siteName}. En cliquant sur « J'accepte » ci-dessous, je confirme que j'ai lu, compris et respecterai le {link} pour le site {siteName}.",
|
"learn.honorCode.content": "Honesty and academic integrity are important to {siteName} and the institutions providing courses and programs on the {siteName} site. By clicking “I agree” below, I confirm that I have read, understand, and will abide by the {link} for the {siteName} Site.",
|
||||||
"learn.honorCode.name": "Code d'honneur",
|
"learn.honorCode.name": "Honor Code",
|
||||||
"learn.honorCode.cancel": "Annuler",
|
"learn.honorCode.cancel": "Cancel",
|
||||||
"learn.honorCode.agree": "Je suis d'accord",
|
"learn.honorCode.agree": "I agree",
|
||||||
"gatedContent.paragraph.bulletOne": "Obtenez un {verifiedCertLink} d'achèvement pour le mettre en valeur sur votre CV",
|
"gatedContent.paragraph.bulletOne": "Earn a {verifiedCertLink} of completion to showcase on your resumé",
|
||||||
"gatedContent.paragraph.bulletTwo": "Déverrouillez l'accès à toutes les activités du cours, y compris {gradedAssignments}",
|
"gatedContent.paragraph.bulletTwo": "Unlock access to all course activities, including {gradedAssignments}",
|
||||||
"gatedContent.paragraph.bulletThree": "{fullAccess} au contenu et aux supports du cours, même après la fin du cours",
|
"gatedContent.paragraph.bulletThree": "{fullAccess} to course content and materials, even after the course ends",
|
||||||
"gatedContent.paragraph.bulletFour": "Soutenez notre {nonProfitMission} sur edX",
|
"gatedContent.paragraph.bulletFour": "Support our {nonProfitMission} at edX",
|
||||||
"learn.lockPaywall.title": "Les devoirs notés sont verrouillés",
|
"learn.lockPaywall.title": "Graded assignments are locked",
|
||||||
"learn.lockPaywall.content": "Mettez à niveau pour accéder à des fonctionnalités verrouillées comme celle-ci et tirer le meilleur parti de votre cours.",
|
"learn.lockPaywall.content": "Upgrade to gain access to locked features like this one and get the most out of your course.",
|
||||||
"learn.lockPaywall.example.alt": "Exemple d'attestation",
|
"learn.lockPaywall.example.alt": "Example Certificate",
|
||||||
"learn.lockPaywall.list.intro": "Lorsque vous effectuez une mise à niveau, vous :",
|
"learn.lockPaywall.list.intro": "When you upgrade, you:",
|
||||||
"learn.lockPaywall.list.bullet1.linktext": "attestation vérifiée",
|
"learn.lockPaywall.list.bullet1.linktext": "verified certificate",
|
||||||
"learn.lockPaywall.list.bullet2.boldtext": "évaluations notées",
|
"learn.lockPaywall.list.bullet2.boldtext": "graded assignments",
|
||||||
"learn.lockPaywall.list.bullet3.boldtext": "Accès complet",
|
"learn.lockPaywall.list.bullet3.boldtext": "Full access",
|
||||||
"learn.lockPaywall.list.bullet4.boldtext": "mission à but non lucratif",
|
"learn.lockPaywall.list.bullet4.boldtext": "non-profit mission",
|
||||||
"learn.loading.content.lock": "Chargement du contenu bloqué de messagerie...",
|
"learn.loading.content.lock": "Loading locked content messaging...",
|
||||||
"learn.loading.honor.codk": "Chargement de la messagerie du code d'honneur...",
|
"learn.loading.honor.codk": "Loading honor code messaging...",
|
||||||
"learn.loading.learning.sequence": "Chargement de la séquence d'apprentissage...",
|
"learn.loading.learning.sequence": "Loading learning sequence...",
|
||||||
"learn.course.load.failure": "Il y a eu une erreur lors du chargement de ce cours.",
|
"learn.course.load.failure": "There was an error loading this course.",
|
||||||
"learn.sequence.no.content": "Il n'y a pas de contenu ici.",
|
"learn.sequence.no.content": "There is no content here.",
|
||||||
"learn.sequence.navigation.next.button": "Suivant",
|
"learn.sequence.navigation.next.button": "Next",
|
||||||
"learn.sequence.navigation.next.up.button": "Prochaine étape : {title}",
|
"learn.sequence.navigation.next.up.button": "Next Up: {title}",
|
||||||
"learn.sequence.navigation.previous.button": "Précédent",
|
"learn.sequence.navigation.previous.button": "Previous",
|
||||||
"learn.course.sequence.navigation.mobile.menu": "{current} de {total}",
|
"learn.course.sequence.navigation.mobile.menu": "{current} of {total}",
|
||||||
"learn.redirect.interstitial.message": "Redirection...",
|
"learn.redirect.interstitial.message": "Redirecting...",
|
||||||
"learn.loading.error": "Erreur : {error}",
|
"learn.loading.error": "Error: {error}",
|
||||||
"learning.celebration.emailBody": "Qu'apprenez-vous durant votre temps libre?",
|
"learning.celebration.emailBody": "What are you spending your time learning?",
|
||||||
"learning.social.shareEmail": "Partagez vos progrès par courriel.",
|
"learning.social.shareEmail": "Share your progress via email.",
|
||||||
"learning.social.shareService": "Partagez vos progrès sur {service}.",
|
"learning.social.shareService": "Share your progress on {service}.",
|
||||||
"general.altText.close": "Fermer",
|
"general.altText.close": "Close",
|
||||||
"learning.logistration.register": "inscription",
|
"learning.logistration.register": "register",
|
||||||
"general.register.sentenceCase": "S'inscrire",
|
"general.register.sentenceCase": "Register",
|
||||||
"learning.logistration.login": "connexion",
|
"learning.logistration.login": "sign in",
|
||||||
"general.signIn.sentenceCase": "Connectez-vous",
|
"general.signIn.sentenceCase": "Sign in",
|
||||||
"learn.course.tabs.navigation.overflow.menu": "Plus...",
|
"learn.course.tabs.navigation.overflow.menu": "More...",
|
||||||
"learning.offer.screenReaderPrices": "Prix d'origine: {originalPrice}, prix réduit : {discountedPrice}",
|
"learning.offer.screenReaderPrices": "Original price: {originalPrice}, discount price: {discountedPrice}",
|
||||||
"learning.upgradeButton.screenReaderInlinePrices": "Prix d'origine : {originalPrice}",
|
"learning.upgradeButton.screenReaderInlinePrices": "Original price: {originalPrice}",
|
||||||
"learning.upgradeButton.buttonText": "Mise à niveau pour {pricing}",
|
"learning.upgradeButton.buttonText": "Upgrade for {pricing}",
|
||||||
"learning.upgradeNowButton.buttonText": "Mettez à niveau maintenant pour {pricing}",
|
"learning.upgradeNowButton.buttonText": "Upgrade now for {pricing}",
|
||||||
"learning.generic.upgradeNotification.verifiedCertLink": "attestation vérifiée",
|
"learning.generic.upgradeNotification.verifiedCertLink": "verified certificate",
|
||||||
"learning.generic.upgradeNotification.verifiedCertMessage": "Obtenez un {verifiedCertLink} d'achèvement pour le mettre en valeur sur votre CV",
|
"learning.generic.upgradeNotification.verifiedCertMessage": "Earn a {verifiedCertLink} of completion to showcase on your resumé",
|
||||||
"learning.generic.upgradeNotification.noFBE.nonProfitMission": "Soutenez notre {nonProfitMission} sur edX",
|
"learning.generic.upgradeNotification.noFBE.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||||
"learning.generic.upgradeNotification.gradedAssignments": "évaluations notées",
|
"learning.generic.upgradeNotification.gradedAssignments": "graded assignments",
|
||||||
"learning.generic.upgradeNotification.verifiedCertLink.fullAccess": "Accès complet",
|
"learning.generic.upgradeNotification.verifiedCertLink.fullAccess": "Full access",
|
||||||
"learning.generic.upgradeNotification.FBE.nonProfitMission": "mission à but non lucratif",
|
"learning.generic.upgradeNotification.FBE.nonProfitMission": "non-profit mission",
|
||||||
"learning.generic.upgradeNotification.unlockGraded": "Déverrouillez l'accès à toutes les activités du cours, y compris {gradedAssignments}",
|
"learning.generic.upgradeNotification.unlockGraded": "Unlock your access to all course activities, including {gradedAssignments}",
|
||||||
"learning.generic.upgradeNotification.fullAccess": "{fullAccess} au contenu et aux supports du cours, même après la fin du cours",
|
"learning.generic.upgradeNotification.fullAccess": "{fullAccess} to course content and materials, even after the course ends",
|
||||||
"learning.generic.upgradeNotification.nonProfitMission": "Soutenez notre {nonProfitMission} sur edX",
|
"learning.generic.upgradeNotification.nonProfitMission": "Support our {nonProfitMission} at edX",
|
||||||
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "incluant tous les progrès",
|
"learning.generic.upgradeNotification.expirationAccessLoss.progress": "including any progress",
|
||||||
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "bénéfices d'une mise à niveau",
|
"learning.generic.upgradeNotification.expirationVerifiedCert.benefits": "benefits of upgrading",
|
||||||
"learning.generic.upgradeNotification.expirationAccessLoss": "Vous perdez tout accès à ce cours, {includingAnyProgress}, le {date}.",
|
"learning.generic.upgradeNotification.expirationAccessLoss": "You will lose all access to this course, {includingAnyProgress}, on {date}.",
|
||||||
"learning.generic.upgradeNotification.expirationVerifiedCert": "Mettre à niveau votre cours vous permettra de tenter d'obtenir une attestation vérifié et d'avoir accès à beaucoup d'autres fonctionnalités. Apprenez en plus à propos des {benefitsOfUpgrading}.",
|
"learning.generic.upgradeNotification.expirationVerifiedCert": "Upgrading your course enables you to pursue a verified certificate and unlocks numerous features. Learn more about the {benefitsOfUpgrading}.",
|
||||||
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} restant",
|
"learning.generic.upgradeNotification.expirationDays": "{dayCount, number} {dayCount, plural, \n one {day}\n other {days}} left",
|
||||||
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} restant",
|
"learning.generic.upgradeNotification.expirationHours": "{hourCount, number} {hourCount, plural,\n one {hour}\n other {hours}} left",
|
||||||
"learning.generic.upgradeNotification.expirationMinutes": "Il reste moins de 1 heure",
|
"learning.generic.upgradeNotification.expirationMinutes": "Less than 1 hour left",
|
||||||
"learning.generic.upgradeNotification.expiration": "L'accès au cours expirera le {date}",
|
"learning.generic.upgradeNotification.expiration": "Course access will expire {date}",
|
||||||
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% de rabais pour les nouveaux apprenants",
|
"learning.generic.upgradeNotification.firstTimeLearnerDiscount": "{percentage}% First-Time Learner Discount",
|
||||||
"learning.generic.upgradeNotification.accessExpiration": "Mettre à niveau votre cours aujourd'hui",
|
"learning.generic.upgradeNotification.accessExpiration": "Upgrade your course today",
|
||||||
"learning.generic.upgradeNotification.accessExpirationUrgent": "L'accès au cours expire",
|
"learning.generic.upgradeNotification.accessExpirationUrgent": "Course Access Expiration",
|
||||||
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Obtenir un certificat vérifié",
|
"learning.generic.upgradeNotification.pursueAverifiedCertificate": "Pursue a verified certificate",
|
||||||
"learning.generic.upgradeNotification.code": "Utilisez le code {code} lors du paiement",
|
"learning.generic.upgradeNotification.code": "Use code {code} at checkout",
|
||||||
"masquerade-widget.userName.error.generic": "Une erreur est survenue; veuillez réessayer.",
|
"masquerade-widget.userName.error.generic": "An error has occurred; please try again.",
|
||||||
"masquerade-widget.userName.input.placeholder": "Nom d'utilisateur ou courriel",
|
"masquerade-widget.userName.input.placeholder": "Username or email",
|
||||||
"masquerade-widget.userName.input.label": "Se faire passer pour cet utilisateur",
|
"masquerade-widget.userName.input.label": "Masquerade as this user",
|
||||||
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
|
"learning.effortEstimation.combinedEstimate": "{minutes} + {activities}",
|
||||||
"learning.effortEstimation.activities": "{activityCount, plural, one {# activité} other {# activités}}",
|
"learning.effortEstimation.activities": "{activityCount, plural, one {# activity} other {# activities}}",
|
||||||
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} other {# min}}",
|
"learning.effortEstimation.minutesAbbreviated": "{minuteCount, plural, one {# min} other {# min}}",
|
||||||
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minute} other {# minutes}}",
|
"learning.effortEstimation.minutesFull": "{minuteCount, plural, one {# minute} other {# minutes}}",
|
||||||
"learning.streakCelebration.congratulations": "Félicitations !",
|
"learning.streakCelebration.congratulations": "Congratulations!",
|
||||||
"learning.streakCelebration.body": "Continuez comme ça, vous êtes sur une lancée!",
|
"learning.streakCelebration.body": "Keep it up, you’re on a roll!",
|
||||||
"learning.streakCelebration.button": "Continuez ainsi",
|
"learning.streakCelebration.button": "Keep it up",
|
||||||
"learning.streakCelebration.buttonSrOnly": "Fermer le modal et continuer",
|
"learning.streakCelebration.buttonSrOnly": "Close modal and continue",
|
||||||
"learning.streakCelebration.buttonAA759": "Continuer le cours",
|
"learning.streakCelebration.buttonAA759": "Continue with course",
|
||||||
"learning.streakCelebration.header": "série de jours",
|
"learning.streakCelebration.header": "day streak",
|
||||||
"learning.streakCelebration.factoidABoldedSection": "ont 20 fois plus de chances de réussir leur cours",
|
"learning.streakCelebration.factoidABoldedSection": "are 20x more likely to pass their course",
|
||||||
"learning.streakCelebration.factoidBBoldedSection": "termine 5 fois plus de contenu de cours en moyenne",
|
"learning.streakCelebration.factoidBBoldedSection": "complete 5x as much course content on average",
|
||||||
"learning.streakCelebration.streakDiscountMessage": "Vous avez droit à 15% de rabais si vous mettez à jour ce cours pour un temps limité seulement.",
|
"learning.streakCelebration.streakDiscountMessage": "You’ve unlocked a 15% off discount when you upgrade this course for a limited time only.",
|
||||||
"learning.streakcelebration.factoida": "Les utilisateurs qui apprennent {streak_length} jours d'affilée {bolded_section} que ceux qui n'apprennent pas.",
|
"learning.streakcelebration.factoida": "Users who learn {streak_length} days in a row {bolded_section} than those who don’t.",
|
||||||
"learning.streakcelebration.factoidb": "Les utilisateurs qui apprennent {streak_length} jours d'affilée {bolded_section} par rapport à ceux qui ne le font pas.",
|
"learning.streakcelebration.factoidb": "Users who learn {streak_length} days in a row {bolded_section} vs. those who don’t.",
|
||||||
"learning.streakCelebration.streakCelebrationCouponEndDateMessage": "Termine {date}.",
|
"learning.streakCelebration.streakCelebrationCouponEndDateMessage": "Ends {date}.",
|
||||||
"learning.loading.failure": "Il y a eu une erreur lors du chargement de ce cours.",
|
"learning.loading.failure": "There was an error loading this course.",
|
||||||
"learning.loading": "Chargement de vos cours..."
|
"learning.loading": "Loading course page…"
|
||||||
}
|
}
|
||||||
@@ -28,57 +28,54 @@ import { TabContainer } from './tab-page';
|
|||||||
import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data';
|
import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data';
|
||||||
import { fetchCourse } from './courseware/data';
|
import { fetchCourse } from './courseware/data';
|
||||||
import initializeStore from './store';
|
import initializeStore from './store';
|
||||||
import NoticesProvider from './generic/notices';
|
|
||||||
|
|
||||||
subscribe(APP_READY, () => {
|
subscribe(APP_READY, () => {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<AppProvider store={initializeStore()}>
|
<AppProvider store={initializeStore()}>
|
||||||
<NoticesProvider>
|
<UserMessagesProvider>
|
||||||
<UserMessagesProvider>
|
<Switch>
|
||||||
<Switch>
|
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
|
||||||
<PageRoute exact path="/goal-unsubscribe/:token" component={GoalUnsubscribe} />
|
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
|
||||||
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
|
<PageRoute path="/course/:courseId/home">
|
||||||
<PageRoute path="/course/:courseId/home">
|
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
|
||||||
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">
|
<OutlineTab />
|
||||||
<OutlineTab />
|
</TabContainer>
|
||||||
|
</PageRoute>
|
||||||
|
<PageRoute path="/course/:courseId/dates">
|
||||||
|
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
||||||
|
<DatesTab />
|
||||||
|
</TabContainer>
|
||||||
|
</PageRoute>
|
||||||
|
<PageRoute
|
||||||
|
path={[
|
||||||
|
'/course/:courseId/progress/:targetUserId/',
|
||||||
|
'/course/:courseId/progress',
|
||||||
|
]}
|
||||||
|
render={({ match }) => (
|
||||||
|
<TabContainer
|
||||||
|
tab="progress"
|
||||||
|
fetch={(courseId) => fetchProgressTab(courseId, match.params.targetUserId)}
|
||||||
|
slice="courseHome"
|
||||||
|
>
|
||||||
|
<ProgressTab />
|
||||||
</TabContainer>
|
</TabContainer>
|
||||||
</PageRoute>
|
)}
|
||||||
<PageRoute path="/course/:courseId/dates">
|
/>
|
||||||
<TabContainer tab="dates" fetch={fetchDatesTab} slice="courseHome">
|
<PageRoute path="/course/:courseId/course-end">
|
||||||
<DatesTab />
|
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
|
||||||
</TabContainer>
|
<CourseExit />
|
||||||
</PageRoute>
|
</TabContainer>
|
||||||
<PageRoute
|
</PageRoute>
|
||||||
path={[
|
<PageRoute
|
||||||
'/course/:courseId/progress/:targetUserId/',
|
path={[
|
||||||
'/course/:courseId/progress',
|
'/course/:courseId/:sequenceId/:unitId',
|
||||||
]}
|
'/course/:courseId/:sequenceId',
|
||||||
render={({ match }) => (
|
'/course/:courseId',
|
||||||
<TabContainer
|
]}
|
||||||
tab="progress"
|
component={CoursewareContainer}
|
||||||
fetch={(courseId) => fetchProgressTab(courseId, match.params.targetUserId)}
|
/>
|
||||||
slice="courseHome"
|
</Switch>
|
||||||
>
|
</UserMessagesProvider>
|
||||||
<ProgressTab />
|
|
||||||
</TabContainer>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<PageRoute path="/course/:courseId/course-end">
|
|
||||||
<TabContainer tab="courseware" fetch={fetchCourse} slice="courseware">
|
|
||||||
<CourseExit />
|
|
||||||
</TabContainer>
|
|
||||||
</PageRoute>
|
|
||||||
<PageRoute
|
|
||||||
path={[
|
|
||||||
'/course/:courseId/:sequenceId/:unitId',
|
|
||||||
'/course/:courseId/:sequenceId',
|
|
||||||
'/course/:courseId',
|
|
||||||
]}
|
|
||||||
component={CoursewareContainer}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</UserMessagesProvider>
|
|
||||||
</NoticesProvider>
|
|
||||||
</AppProvider>,
|
</AppProvider>,
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
@@ -96,7 +93,6 @@ initialize({
|
|||||||
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
|
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
|
||||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
|
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
|
||||||
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
|
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
|
||||||
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
|
|
||||||
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
|
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
|
||||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||||
SOCIAL_UTM_MILESTONE_CAMPAIGN: process.env.SOCIAL_UTM_MILESTONE_CAMPAIGN || null,
|
SOCIAL_UTM_MILESTONE_CAMPAIGN: process.env.SOCIAL_UTM_MILESTONE_CAMPAIGN || null,
|
||||||
@@ -105,10 +101,8 @@ initialize({
|
|||||||
SUPPORT_URL_CALCULATOR_MATH: process.env.SUPPORT_URL_CALCULATOR_MATH || null,
|
SUPPORT_URL_CALCULATOR_MATH: process.env.SUPPORT_URL_CALCULATOR_MATH || null,
|
||||||
SUPPORT_URL_ID_VERIFICATION: process.env.SUPPORT_URL_ID_VERIFICATION || null,
|
SUPPORT_URL_ID_VERIFICATION: process.env.SUPPORT_URL_ID_VERIFICATION || null,
|
||||||
SUPPORT_URL_VERIFIED_CERTIFICATE: process.env.SUPPORT_URL_VERIFIED_CERTIFICATE || null,
|
SUPPORT_URL_VERIFIED_CERTIFICATE: process.env.SUPPORT_URL_VERIFIED_CERTIFICATE || null,
|
||||||
TERMS_OF_SERVICE_URL: process.env.TERMS_OF_SERVICE_URL || null,
|
|
||||||
TWITTER_HASHTAG: process.env.TWITTER_HASHTAG || null,
|
TWITTER_HASHTAG: process.env.TWITTER_HASHTAG || null,
|
||||||
TWITTER_URL: process.env.TWITTER_URL || null,
|
TWITTER_URL: process.env.TWITTER_URL || null,
|
||||||
LEGACY_THEME_NAME: process.env.LEGACY_THEME_NAME || null,
|
|
||||||
}, 'LearnerAppConfig');
|
}, 'LearnerAppConfig');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.course-header {
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.course-title-lockup {
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding-bottom: 0.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
.btn {
|
||||||
|
height: 3rem;
|
||||||
|
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.course-tabs-navigation {
|
.course-tabs-navigation {
|
||||||
border-bottom: solid 1px #eaeaea;
|
border-bottom: solid 1px #eaeaea;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Helmet } from 'react-helmet';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useToggle } from '@edx/paragon';
|
import { useToggle } from '@edx/paragon';
|
||||||
|
|
||||||
import { CourseTabsNavigation } from '../course-tabs';
|
import { CourseTabsNavigation } from '../course-header';
|
||||||
import { useModel } from '../generic/model-store';
|
import { useModel } from '../generic/model-store';
|
||||||
import { AlertList } from '../generic/user-messages';
|
import { AlertList } from '../generic/user-messages';
|
||||||
import StreakModal from '../shared/streak-celebration';
|
import StreakModal from '../shared/streak-celebration';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Factory } from 'rosie';
|
|||||||
import { initializeTestStore, render, screen } from '../setupTest';
|
import { initializeTestStore, render, screen } from '../setupTest';
|
||||||
import LoadedTabPage from './LoadedTabPage';
|
import LoadedTabPage from './LoadedTabPage';
|
||||||
|
|
||||||
jest.mock('../course-tabs/CourseTabsNavigation', () => () => <div data-testid="CourseTabsNavigation" />);
|
jest.mock('../course-header/CourseTabsNavigation', () => () => <div data-testid="CourseTabsNavigation" />);
|
||||||
jest.mock('../instructor-toolbar/InstructorToolbar', () => () => <div data-testid="InstructorToolbar" />);
|
jest.mock('../instructor-toolbar/InstructorToolbar', () => () => <div data-testid="InstructorToolbar" />);
|
||||||
jest.mock('../shared/streak-celebration/StreakCelebrationModal', () => () => <div data-testid="StreakModal" />);
|
jest.mock('../shared/streak-celebration/StreakCelebrationModal', () => () => <div data-testid="StreakModal" />);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Redirect } from 'react-router';
|
|||||||
|
|
||||||
import Footer from '@edx/frontend-component-footer';
|
import Footer from '@edx/frontend-component-footer';
|
||||||
import { Toast } from '@edx/paragon';
|
import { Toast } from '@edx/paragon';
|
||||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
import { Header } from '../course-header';
|
||||||
import PageLoading from '../generic/PageLoading';
|
|
||||||
import { getAccessDeniedRedirectUrl } from '../shared/access';
|
import { getAccessDeniedRedirectUrl } from '../shared/access';
|
||||||
|
import PageLoading from '../generic/PageLoading';
|
||||||
import { useModel } from '../generic/model-store';
|
import { useModel } from '../generic/model-store';
|
||||||
|
|
||||||
import genericMessages from '../generic/messages';
|
import genericMessages from '../generic/messages';
|
||||||
|
|||||||
Reference in New Issue
Block a user