diff --git a/lms/static/js/apiClient/.eslintrc.js b/lms/static/js/apiClient/.eslintrc.js
new file mode 100644
index 0000000000..838b853a82
--- /dev/null
+++ b/lms/static/js/apiClient/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+ extends: 'eslint-config-edx',
+ root: true,
+ settings: {
+ 'import/resolver': {
+ webpack: {
+ config: 'webpack.dev.config.js',
+ },
+ },
+ },
+};
diff --git a/lms/static/js/apiClient/index.js b/lms/static/js/apiClient/index.js
new file mode 100644
index 0000000000..444dbdfe83
--- /dev/null
+++ b/lms/static/js/apiClient/index.js
@@ -0,0 +1,16 @@
+import { getAuthenticatedAPIClient } from '@edx/frontend-auth';
+import { NewRelicLoggingService } from '@edx/frontend-logging';
+
+const apiClient = getAuthenticatedAPIClient({
+ appBaseUrl: process.env.LMS_ROOT_URL,
+ authBaseUrl: process.env.LMS_ROOT_URL,
+ loginUrl: `${process.env.LMS_ROOT_URL}/login`,
+ logoutUrl: `${process.env.LMS_ROOT_URL}/logout`,
+ csrfTokenApiPath: '/csrf/api/v1/token',
+ refreshAccessTokenEndpoint: `${process.env.LMS_ROOT_URL}/login_refresh`,
+ accessTokenCookieName: process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD,
+ userInfoCookieName: process.env.EDXMKTG_USER_INFO_COOKIE_NAME,
+ loggingService: NewRelicLoggingService,
+});
+
+export default apiClient;
diff --git a/lms/static/js/custom_user_menu_links/.eslintrc.js b/lms/static/js/custom_user_menu_links/.eslintrc.js
new file mode 100644
index 0000000000..838b853a82
--- /dev/null
+++ b/lms/static/js/custom_user_menu_links/.eslintrc.js
@@ -0,0 +1,11 @@
+module.exports = {
+ extends: 'eslint-config-edx',
+ root: true,
+ settings: {
+ 'import/resolver': {
+ webpack: {
+ config: 'webpack.dev.config.js',
+ },
+ },
+ },
+};
diff --git a/lms/static/js/custom_user_menu_links/CustomUserMenuLinks.js b/lms/static/js/custom_user_menu_links/CustomUserMenuLinks.js
new file mode 100644
index 0000000000..a2afad55f1
--- /dev/null
+++ b/lms/static/js/custom_user_menu_links/CustomUserMenuLinks.js
@@ -0,0 +1,20 @@
+import { getLearnerPortalLinks } from '@edx/frontend-enterprise';
+
+import apiClient from '../apiClient';
+
+function CustomUserMenuLinks() {
+ // Inject enterprise learner portal links
+ getLearnerPortalLinks(apiClient).then((learnerPortalLinks) => {
+ const $dashboardLink = $('#user-menu .dashboard');
+ const classNames = 'mobile-nav-item dropdown-item dropdown-nav-item';
+ for (let i = 0; i < learnerPortalLinks.length; i += 1) {
+ const link = learnerPortalLinks[i];
+
+ $dashboardLink.after( // xss-lint: disable=javascript-jquery-insertion
+ `
`,
+ );
+ }
+ });
+}
+
+export { CustomUserMenuLinks }; // eslint-disable-line import/prefer-default-export
diff --git a/lms/static/js/learner_dashboard/EnterpriseLearnerPortalBanner.jsx b/lms/static/js/learner_dashboard/EnterpriseLearnerPortalBanner.jsx
new file mode 100644
index 0000000000..daed1759d7
--- /dev/null
+++ b/lms/static/js/learner_dashboard/EnterpriseLearnerPortalBanner.jsx
@@ -0,0 +1,78 @@
+import React, { Component } from 'react';
+import { getLearnerPortalLinks } from '@edx/frontend-enterprise';
+import { StatusAlert } from '@edx/paragon';
+
+import apiClient from '../apiClient';
+
+const LOCAL_STORAGE_KEY = 'has-viewed-enterprise-learner-portal-banner';
+
+function getAlertHtml(learnerPortalLinks) {
+ let html = '';
+ for (let i = 0; i < learnerPortalLinks.length; i += 1) {
+ const link = learnerPortalLinks[i];
+ html += `
+ ${link.title} has a dedicated page where you can see all of your sponsored courses.
+ Go to
your learner portal.
+
`;
+ }
+ return html;
+}
+
+function setViewedBanner() {
+ window.localStorage.setItem(LOCAL_STORAGE_KEY, true);
+}
+
+function hasViewedBanner() {
+ window.localStorage.getItem(LOCAL_STORAGE_KEY);
+}
+
+class EnterpriseLearnerPortalBanner extends Component {
+ constructor(props) {
+ super(props);
+
+ this.onClose = this.onClose.bind(this);
+
+ this.state = {
+ open: false,
+ alertHtml: '',
+ };
+ }
+
+ componentDidMount() {
+ if (!hasViewedBanner()) {
+ getLearnerPortalLinks(apiClient).then((learnerPortalLinks) => {
+ this.setState({
+ open: true,
+ alertHtml: getAlertHtml(learnerPortalLinks),
+ });
+ });
+ }
+ }
+
+ onClose() {
+ this.setState({ open: false });
+ setViewedBanner();
+ }
+
+ render() {
+ const { alertHtml, open } = this.state;
+
+ if (open) {
+ return (
+
+ )}
+ onClose={this.onClose}
+ />
+
+ );
+ }
+
+ return null;
+ }
+}
+
+export { EnterpriseLearnerPortalBanner }; // eslint-disable-line import/prefer-default-export
diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss
index 53143f9266..ed3e8ad2d1 100644
--- a/lms/static/sass/_build-lms-v1.scss
+++ b/lms/static/sass/_build-lms-v1.scss
@@ -73,6 +73,7 @@
@import 'features/_unsupported-browser-alert';
@import 'features/content-type-gating';
@import 'features/course-duration-limits';
+@import 'features/enterprise-learner-portal-banner';
// search
@import 'search/search';
diff --git a/lms/static/sass/_build-lms-v2.scss b/lms/static/sass/_build-lms-v2.scss
index 124f3a8544..1dc6e8ed4b 100644
--- a/lms/static/sass/_build-lms-v2.scss
+++ b/lms/static/sass/_build-lms-v2.scss
@@ -33,6 +33,7 @@
@import 'features/course-sock';
@import 'features/course-upgrade-message';
@import 'features/content-type-gating';
+@import 'features/enterprise-learner-portal-banner';
// Responsive Design
diff --git a/lms/static/sass/bootstrap/lms-main.scss b/lms/static/sass/bootstrap/lms-main.scss
index e78b42db2c..b1a066465b 100644
--- a/lms/static/sass/bootstrap/lms-main.scss
+++ b/lms/static/sass/bootstrap/lms-main.scss
@@ -25,6 +25,7 @@ $static-path: '../..';
@import 'features/course-sock';
@import 'features/course-upgrade-message';
@import 'features/course-duration-limits';
+@import 'features/enterprise-learner-portal-banner';
// Individual Pages
diff --git a/lms/static/sass/features/_enterprise-learner-portal-banner.scss b/lms/static/sass/features/_enterprise-learner-portal-banner.scss
new file mode 100644
index 0000000000..d6543505d5
--- /dev/null
+++ b/lms/static/sass/features/_enterprise-learner-portal-banner.scss
@@ -0,0 +1,89 @@
+$enterprise-learner-portal-banner-background-color: #d9edf7 !default;
+$enterprise-learner-portal-banner-text-color: #4e4e4e !default;
+$enterprise-learner-portal-banner-cta-base: #0075b4 !default;
+$enterprise-learner-portal-banner-cta-hover: #075683 !default;
+
+.edx-enterprise-learner-portal-banner-wrapper {
+ background: $enterprise-learner-portal-banner-background-color;
+ box-sizing: border-box;
+
+ /** Base Styles - start **/
+ text-align: left;
+ line-height: 1.5;
+ font: {
+ family: 'Open Sans', "Helvetica Neue", Helvetica, Arial, sans-serif;
+ size: 1rem;
+ weight: 400;
+ }
+
+ .alert {
+ position: relative;
+ padding: 0.75rem 1.25rem;
+ }
+
+ .alert-dismissible {
+ .close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 0.75rem 1.25rem;
+ background: transparent;
+ border: 0;
+ text-shadow: 0 1px 0 #fff;
+ opacity: 0.5;
+ float: right;
+ line-height: 1;
+ font: {
+ size: 1.5rem;
+ weight: 700;
+ }
+ }
+
+ .btn {
+ display: inline-block;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ box-shadow: none;
+ }
+ }
+ /** Base Styles - end **/
+
+
+ .edx-enterprise-learner-portal-banner {
+ box-sizing: border-box;
+ display: flex;
+ justify-content: space-between;
+ max-width: 1200px;
+ min-width: 0;
+ margin: 0 auto;
+ background: inherit;
+ border: none;
+
+ .policy-link {
+ color: $enterprise-learner-portal-banner-cta-base;
+ text-decoration: underline;
+
+ &:focus,
+ &:hover {
+ color: $enterprise-learner-portal-banner-cta-hover;
+ border: none;
+ }
+ }
+
+ .alert-dialog {
+ margin-right: 30px;
+ color: $enterprise-learner-portal-banner-text-color;
+ }
+
+ .btn.close {
+ color: $enterprise-learner-portal-banner-cta-base;
+
+ &:focus,
+ &:hover {
+ color: $enterprise-learner-portal-banner-cta-hover;
+ cursor: pointer;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/lms/templates/header/user_dropdown.html b/lms/templates/header/user_dropdown.html
index 29288047c8..0f18b836bc 100644
--- a/lms/templates/header/user_dropdown.html
+++ b/lms/templates/header/user_dropdown.html
@@ -1,6 +1,6 @@
## mako
<%page expression_filter="h"/>
-<%namespace name='static' file='static_content.html'/>
+<%namespace name='static' file='../static_content.html'/>
<%!
from django.conf import settings
@@ -22,6 +22,12 @@ resume_block = retrieve_last_sitewide_block_completed(self.real_user)
displayname = get_enterprise_learner_generic_name(request) or username
%>
+<%static:webpack entry="CustomUserMenuLinks">
+ $(document).ready(function() {
+ CustomUserMenuLinks();
+ });
+%static:webpack>
+
%endif
+ ${static.renderReact(
+ component="EnterpriseLearnerPortalBanner",
+ id="enterprise-learner-portal-banner",
+ props={}
+ )}
diff --git a/webpack.common.config.js b/webpack.common.config.js
index 890ed2eb81..5e8602b06a 100644
--- a/webpack.common.config.js
+++ b/webpack.common.config.js
@@ -91,6 +91,8 @@ module.exports = Merge.smart({
StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx',
StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js',
ProblemBrowser: './lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx',
+ CustomUserMenuLinks: './lms/static/js/custom_user_menu_links/CustomUserMenuLinks.js',
+ EnterpriseLearnerPortalBanner: './lms/static/js/learner_dashboard/EnterpriseLearnerPortalBanner.jsx',
// Learner Dashboard
EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js',
diff --git a/webpack.dev.config.js b/webpack.dev.config.js
index 913d773cac..c560486dd3 100644
--- a/webpack.dev.config.js
+++ b/webpack.dev.config.js
@@ -20,7 +20,10 @@ module.exports = _.values(Merge.smart(commonConfig, {
debug: true
}),
new webpack.DefinePlugin({
- 'process.env.NODE_ENV': JSON.stringify('development')
+ 'process.env.NODE_ENV': JSON.stringify('development'),
+ 'process.env.LMS_ROOT_URL': JSON.stringify('https://localhost:18000'),
+ 'process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD': JSON.stringify('edx-jwt-cookie-header-payload'),
+ 'process.env.EDXMKTG_USER_INFO_COOKIE_NAME': JSON.stringify('edx-user-info')
})
],
module: {
diff --git a/webpack.prod.config.js b/webpack.prod.config.js
index 360ab56d4d..23c87f8ecd 100644
--- a/webpack.prod.config.js
+++ b/webpack.prod.config.js
@@ -17,7 +17,10 @@ var optimizedConfig = Merge.smart(commonConfig, {
devtool: false,
plugins: [
new webpack.DefinePlugin({
- 'process.env.NODE_ENV': JSON.stringify('production')
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ 'process.env.LMS_ROOT_URL': JSON.stringify(process.env.LMS_ROOT_URL),
+ 'process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD': JSON.stringify(process.env.JWT_AUTH_COOKIE_HEADER_PAYLOAD),
+ 'process.env.EDXMKTG_USER_INFO_COOKIE_NAME': JSON.stringify(process.env.EDXMKTG_USER_INFO_COOKIE_NAME)
}),
new webpack.LoaderOptionsPlugin({ // This may not be needed; legacy option for loaders written for webpack 1
minimize: true