From 5e7358dd4b446fa65390f6d0031d2be244d8e288 Mon Sep 17 00:00:00 2001 From: asadiqbal08 Date: Fri, 22 Oct 2021 17:25:07 +0500 Subject: [PATCH] feat: Introduced Learning Course Header in Header MFE. --- package.json | 10 ++- src/generic/messages.js | 16 ++++ src/index.jsx | 3 +- src/index.scss | 24 ++++++ src/learning-header/AnonymousUserMenu.jsx | 34 ++++++++ .../AuthenticatedUserDropdown.jsx | 55 +++++++++++++ src/learning-header/LearningHeader.jsx | 81 +++++++++++++++++++ src/learning-header/LearningHeader.test.jsx | 29 +++++++ src/learning-header/messages.js | 41 ++++++++++ src/setupTest.js | 58 +++++++++++++ 10 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 src/generic/messages.js create mode 100644 src/learning-header/AnonymousUserMenu.jsx create mode 100644 src/learning-header/AuthenticatedUserDropdown.jsx create mode 100644 src/learning-header/LearningHeader.jsx create mode 100644 src/learning-header/LearningHeader.test.jsx create mode 100644 src/learning-header/messages.js diff --git a/package.json b/package.json index afc865a12e..6c388376ca 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,18 @@ "react-test-renderer": "16.14.0", "reactifex": "1.1.1", "redux": "4.1.2", - "redux-saga": "1.1.3" + "redux-saga": "1.1.3", + "@testing-library/react": "10.3.0" }, "dependencies": { "babel-polyfill": "6.26.0", "react-responsive": "8.2.0", - "react-transition-group": "4.4.2" + "react-transition-group": "4.4.2", + "@fortawesome/fontawesome-svg-core": "1.2.34", + "@fortawesome/free-brands-svg-icons": "5.13.1", + "@fortawesome/free-regular-svg-icons": "5.13.1", + "@fortawesome/free-solid-svg-icons": "5.13.1", + "@fortawesome/react-fontawesome": "^0.1.14" }, "peerDependencies": { "@edx/frontend-platform": "^1.8.0", diff --git a/src/generic/messages.js b/src/generic/messages.js new file mode 100644 index 0000000000..41e9f860b4 --- /dev/null +++ b/src/generic/messages.js @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + registerSentenceCase: { + id: 'general.register.sentenceCase', + defaultMessage: 'Register', + description: 'Text in a button, prompting the user to register.', + }, + signInSentenceCase: { + id: 'general.signIn.sentenceCase', + defaultMessage: 'Sign in', + description: 'Text in a button, prompting the user to log in.', + }, +}); + +export default messages; diff --git a/src/index.jsx b/src/index.jsx index 22caa16b6a..ffabfeca1f 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,6 +1,7 @@ import Header from './Header'; +import LearningHeader from './learning-header/LearningHeader'; import messages from './i18n/index'; -export { messages }; +export { LearningHeader, messages }; export default Header; diff --git a/src/index.scss b/src/index.scss index 637163c7f2..8d5d162541 100644 --- a/src/index.scss +++ b/src/index.scss @@ -25,6 +25,30 @@ $white: #fff; } } +.learning-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; + } + } + } +} + .site-header-mobile, .site-header-desktop { position: relative; diff --git a/src/learning-header/AnonymousUserMenu.jsx b/src/learning-header/AnonymousUserMenu.jsx new file mode 100644 index 0000000000..75396086c2 --- /dev/null +++ b/src/learning-header/AnonymousUserMenu.jsx @@ -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 ( +
+ + +
+ ); +} + +AnonymousUserMenu.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(AnonymousUserMenu); diff --git a/src/learning-header/AuthenticatedUserDropdown.jsx b/src/learning-header/AuthenticatedUserDropdown.jsx new file mode 100644 index 0000000000..3e47b37a7d --- /dev/null +++ b/src/learning-header/AuthenticatedUserDropdown.jsx @@ -0,0 +1,55 @@ +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({ intl, username }) { + const dashboardMenuItem = ( + + {intl.formatMessage(messages.dashboard)} + + ); + + return ( + <> + {intl.formatMessage(messages.help)} + + + + + {username} + + + + {dashboardMenuItem} + + {intl.formatMessage(messages.profile)} + + + {intl.formatMessage(messages.account)} + + + {intl.formatMessage(messages.orderHistory)} + + + {intl.formatMessage(messages.signOut)} + + + + + ); +} + +AuthenticatedUserDropdown.propTypes = { + intl: intlShape.isRequired, + username: PropTypes.string.isRequired, +}; + +export default injectIntl(AuthenticatedUserDropdown); diff --git a/src/learning-header/LearningHeader.jsx b/src/learning-header/LearningHeader.jsx new file mode 100644 index 0000000000..fb5350eb01 --- /dev/null +++ b/src/learning-header/LearningHeader.jsx @@ -0,0 +1,81 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +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 ( + + {alt} + + ); +} + +LinkedLogo.propTypes = { + href: PropTypes.string.isRequired, + src: PropTypes.string.isRequired, + alt: PropTypes.string.isRequired, +}; + +function LearningHeader({ + courseOrg, courseNumber, courseTitle, intl, showUserDropdown, +}) { + const { authenticatedUser } = useContext(AppContext); + + const headerLogo = ( + + ); + + return ( +
+ {intl.formatMessage(messages.skipNavLink)} +
+ {headerLogo} +
+ {courseOrg} {courseNumber} + {courseTitle} +
+ {showUserDropdown && authenticatedUser && ( + + )} + {showUserDropdown && !authenticatedUser && ( + + )} +
+
+ ); +} + +LearningHeader.propTypes = { + courseOrg: PropTypes.string, + courseNumber: PropTypes.string, + courseTitle: PropTypes.string, + intl: intlShape.isRequired, + showUserDropdown: PropTypes.bool, +}; + +LearningHeader.defaultProps = { + courseOrg: null, + courseNumber: null, + courseTitle: null, + showUserDropdown: true, +}; + +export default injectIntl(LearningHeader); diff --git a/src/learning-header/LearningHeader.test.jsx b/src/learning-header/LearningHeader.test.jsx new file mode 100644 index 0000000000..8937e5d59d --- /dev/null +++ b/src/learning-header/LearningHeader.test.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { + authenticatedUser, initializeMockApp, render, screen, +} from '../setupTest'; +import { LearningHeader as 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(
); + expect(screen.getByRole('button')).toHaveTextContent(authenticatedUser.username); + }); + + it('displays course data', () => { + const courseData = { + courseOrg: 'course-org', + courseNumber: 'course-number', + courseTitle: 'course-title', + }; + render(
); + + expect(screen.getByText(`${courseData.courseOrg} ${courseData.courseNumber}`)).toBeInTheDocument(); + expect(screen.getByText(courseData.courseTitle)).toBeInTheDocument(); + }); +}); diff --git a/src/learning-header/messages.js b/src/learning-header/messages.js new file mode 100644 index 0000000000..03e85ea3a4 --- /dev/null +++ b/src/learning-header/messages.js @@ -0,0 +1,41 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + 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; diff --git a/src/setupTest.js b/src/setupTest.js index de956a28fb..7f8df43b9c 100644 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -3,6 +3,10 @@ import Enzyme from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import 'babel-polyfill'; +import { mergeConfig } from '@edx/frontend-platform'; +import { render as rtlRender } from '@testing-library/react'; +import { IntlProvider } from 'react-intl'; +import AppProvider from '@edx/frontend-platform/react/AppProvider'; Enzyme.configure({ adapter: new Adapter() }); @@ -27,3 +31,57 @@ process.env.LOGO_URL = 'https://edx-cdn.org/v3/default/logo.svg'; process.env.LOGO_TRADEMARK_URL = 'https://edx-cdn.org/v3/default/logo-trademark.svg'; process.env.LOGO_WHITE_URL = 'https://edx-cdn.org/v3/default/logo-white.svg'; process.env.FAVICON_URL = 'https://edx-cdn.org/v3/default/favicon.ico'; + +export const authenticatedUser = { + userId: 'abc123', + username: 'Mock User', + roles: [], + administrator: false, + }; + + export function initializeMockApp() { + mergeConfig({ + INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null, + STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null, + TWITTER_URL: process.env.TWITTER_URL || null, + authenticatedUser: { + userId: 'abc123', + username: 'Mock User', + roles: [], + administrator: false, + }, + }); +} + +function render( + ui, + { + store = null, + ...renderOptions + } = {}, + ) { + function Wrapper({ children }) { + return ( + // eslint-disable-next-line react/jsx-filename-extension + + + {children} + + + ); + } + + Wrapper.propTypes = { + children: PropTypes.node.isRequired, + }; + + return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); + } + + // Re-export everything. + export * from '@testing-library/react'; + + // Override `render` method. + export { + render, + };