Compare commits
11 Commits
master
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eac884bd59 | ||
|
|
2be382d01f | ||
|
|
c828a43d0e | ||
|
|
1eca5522cd | ||
|
|
a21abde463 | ||
|
|
37d9646629 | ||
|
|
ad72980ad7 | ||
|
|
71bcb6ba62 | ||
|
|
da867d0ef6 | ||
|
|
131096b4a5 | ||
|
|
76e83cc737 |
1
.env
1
.env
@@ -20,6 +20,7 @@ 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=''
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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
|
||||||
|
|||||||
6929
package-lock.json
generated
6929
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -33,10 +33,11 @@
|
|||||||
"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.0",
|
"@edx/frontend-enterprise-utils": "1.1.1",
|
||||||
"@edx/frontend-lib-special-exams": "1.13.3",
|
"@edx/frontend-lib-special-exams": "1.14.1",
|
||||||
"@edx/frontend-platform": "1.12.7",
|
"@edx/frontend-platform": "1.14.3",
|
||||||
"@edx/paragon": "16.14.9",
|
"@edx/paragon": "16.19.0",
|
||||||
|
"@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",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"util": "0.12.4"
|
"util": "0.12.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/frontend-build": "8.0.4",
|
"@edx/frontend-build": "9.0.5",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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,46 +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',
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
@@ -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 { Header } from '../../course-header';
|
import { LearningHeader as Header } from '@edx/frontend-component-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/css/bootstrap/lms-main.css">
|
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
<link rel="stylesheet" type="text/css" href="${getConfig().BASE_URL}/src/course-home/outline-tab/LmsHtmlFragment.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="${className}">${html}</body>
|
<body class="${className}">${html}</body>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ 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 { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import {
|
||||||
|
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';
|
||||||
@@ -23,6 +25,14 @@ 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
|
||||||
@@ -37,16 +47,16 @@ function CurrentGradeTooltip({ intl, tooltipClassName }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
<circle cx={`${Math.min(...[currentGrade, 100])}%`} cy="50%" r="8.5" fill="transparent" />
|
<circle cx={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`} cy="50%" r="8.5" fill="transparent" />
|
||||||
<rect className="grade-bar__divider" x={`${Math.min(...[currentGrade, 100])}%`} style={{ transform: 'translateY(2.61em)' }} />
|
<rect className="grade-bar__divider" x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : 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(...[currentGrade, 100])}%`}
|
x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`}
|
||||||
y="20px"
|
y="20px"
|
||||||
style={{ transform: `translateX(${currentGrade < 50 ? '' : '-'}3.4em)` }}
|
style={{ transform: `translateX(${currentGradeDirection}3.4em)` }}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.currentGradeLabel)}
|
{intl.formatMessage(messages.currentGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
import {
|
||||||
|
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
|
||||||
@@ -21,17 +31,17 @@ function PassingGradeTooltip({ intl, passingGrade, tooltipClassName }) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<g>
|
<g>
|
||||||
<circle cx={`${passingGrade}%`} cy="50%" r="8.5" fill="transparent" />
|
<circle cx={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`} cy="50%" r="8.5" fill="transparent" />
|
||||||
<circle className="grade-bar--passing" cx={`${passingGrade}%`} cy="50%" r="4.5" />
|
<circle className="grade-bar--passing" cx={`${isLocaleRtl ? 100 - passingGrade : 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={`${passingGrade}%`}
|
x={`${isLocaleRtl ? 100 - passingGrade : passingGrade}%`}
|
||||||
y="90px"
|
y="90px"
|
||||||
style={{ transform: `translateX(${passingGrade < 50 ? '' : '-'}3.4em)` }}
|
style={{ transform: `translateX(${passingGradeDirection}3.4em)` }}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.passingGradeLabel)}
|
{intl.formatMessage(messages.passingGradeLabel)}
|
||||||
</text>
|
</text>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { default as Header } from './Header';
|
/* eslint-disable import/prefer-default-export */
|
||||||
export { default as CourseTabsNavigation } from './CourseTabsNavigation';
|
export { default as CourseTabsNavigation } from './CourseTabsNavigation';
|
||||||
11
src/course-tabs/messages.js
Normal file
11
src/course-tabs/messages.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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('.course-header');
|
const courseHeader = container.querySelector('.learning-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);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 { SelectMenu } from '@edx/paragon';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
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';
|
||||||
@@ -30,9 +31,12 @@ function CourseBreadcrumb({
|
|||||||
>
|
>
|
||||||
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff
|
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !isStaff
|
||||||
? (
|
? (
|
||||||
<a className="text-primary-500" href={`/course/${courseId}/${defaultContent.id}`}>
|
<Link
|
||||||
|
className="text-primary-500"
|
||||||
|
to={`/course/${courseId}/${defaultContent.id}`}
|
||||||
|
>
|
||||||
{defaultContent.label}
|
{defaultContent.label}
|
||||||
</a>
|
</Link>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<SelectMenu isLink defaultMessage={defaultContent.label}>
|
<SelectMenu isLink defaultMessage={defaultContent.label}>
|
||||||
@@ -122,9 +126,9 @@ export default function CourseBreadcrumbs({
|
|||||||
<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 flex-nowrap align-items-center m-0">
|
||||||
<li className="list-unstyled d-flex m-0">
|
<li className="list-unstyled d-flex m-0">
|
||||||
<a
|
<Link
|
||||||
href={`/course/${courseId}/home`}
|
|
||||||
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
|
||||||
@@ -132,7 +136,7 @@ export default function CourseBreadcrumbs({
|
|||||||
description="The course home link in breadcrumbs nav"
|
description="The course home link in breadcrumbs nav"
|
||||||
defaultMessage="Course"
|
defaultMessage="Course"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{links.map(content => (
|
{links.map(content => (
|
||||||
<CourseBreadcrumb
|
<CourseBreadcrumb
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { screen, render } from '@testing-library/react';
|
import { screen, render } from '@testing-library/react';
|
||||||
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';
|
||||||
|
|
||||||
@@ -105,12 +106,14 @@ describe('CourseBreadcrumbs', () => {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
render(
|
render(
|
||||||
<CourseBreadcrumbs
|
<BrowserRouter>
|
||||||
courseId="course-v1:edX+DemoX+Demo_Course"
|
<CourseBreadcrumbs
|
||||||
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
courseId="course-v1:edX+DemoX+Demo_Course"
|
||||||
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
|
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
|
||||||
isStaff
|
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', async () => {
|
||||||
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
expect(screen.queryAllByRole('link')).toHaveLength(1);
|
||||||
|
|||||||
@@ -237,7 +237,13 @@ export function fetchSequence(sequenceId) {
|
|||||||
dispatch(fetchSequenceSuccess({ sequenceId }));
|
dispatch(fetchSequenceSuccess({ sequenceId }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error);
|
// Some errors are expected - for example, CoursewareContainer may request sequence metadata for a unit and rely
|
||||||
|
// 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 }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,16 +11,6 @@ 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',
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ initialize({
|
|||||||
TERMS_OF_SERVICE_URL: process.env.TERMS_OF_SERVICE_URL || 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,29 +36,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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-header';
|
import { CourseTabsNavigation } from '../course-tabs';
|
||||||
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-header/CourseTabsNavigation', () => () => <div data-testid="CourseTabsNavigation" />);
|
jest.mock('../course-tabs/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 { Header } from '../course-header';
|
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||||
import { getAccessDeniedRedirectUrl } from '../shared/access';
|
|
||||||
import PageLoading from '../generic/PageLoading';
|
import PageLoading from '../generic/PageLoading';
|
||||||
|
import { getAccessDeniedRedirectUrl } from '../shared/access';
|
||||||
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