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 + `
${link.title} Dashboard
`, + ); + } + }); +} + +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(); + }); + + %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