feat: add career link to user dropdown (#152)

Co-authored-by: leangseu-edx <83240113+leangseu-edx@users.noreply.github.com>
This commit is contained in:
Kris Hatcher
2023-06-05 11:50:40 -04:00
committed by GitHub
parent 4643e0b130
commit e43a49b431
42 changed files with 200 additions and 1121 deletions

1
.env
View File

@@ -39,3 +39,4 @@ HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
ENABLE_NOTICES=''
CAREER_LINK_URL=''

View File

@@ -46,3 +46,4 @@ HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
ENABLE_NOTICES=''
CAREER_LINK_URL=''

View File

@@ -45,3 +45,4 @@ HOTJAR_DEBUG=''
ACCOUNT_SETTINGS_URL='http://account-settings-url.test'
ACCOUNT_PROFILE_URL='http://account-profile-url.test'
ENABLE_NOTICES=''
CAREER_LINK_URL=''

View File

@@ -23,7 +23,7 @@ import ZendeskFab from 'components/ZendeskFab';
import track from 'tracking';
import fakeData from 'data/services/lms/fakeData/courses';
import LearnerDashboardHeaderVariant from './containers/LearnerDashboardHeaderVariant';
import LearnerDashboardHeader from './containers/LearnerDashboardHeader';
import messages from './messages';
@@ -77,7 +77,7 @@ export const App = () => {
<title>{formatMessage(messages.pageTitle)}</title>
</Helmet>
<div>
<LearnerDashboardHeaderVariant />
<LearnerDashboardHeader />
<main>
{hasNetworkFailure
? (

View File

@@ -12,14 +12,14 @@ import { Alert } from '@edx/paragon';
import { RequestKeys } from 'data/constants/requests';
import { reduxHooks } from 'hooks';
import Dashboard from 'containers/Dashboard';
import LearnerDashboardHeaderVariant from 'containers/LearnerDashboardHeaderVariant';
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
import { App } from './App';
import messages from './messages';
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('containers/Dashboard', () => 'Dashboard');
jest.mock('containers/LearnerDashboardHeaderVariant', () => 'LearnerDashboardHeaderVariant');
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('data/redux', () => ({
selectors: 'redux.selectors',
@@ -54,7 +54,7 @@ describe('App router component', () => {
expect(el.find(Helmet).find('title').text()).toEqual(useIntl().formatMessage(messages.pageTitle));
});
it('displays learner dashboard header', () => {
expect(el.find(LearnerDashboardHeaderVariant).length).toEqual(1);
expect(el.find(LearnerDashboardHeader).length).toEqual(1);
});
it('wraps the page in a browser router', () => {
expect(el.find(Router)).toMatchObject(el);

View File

@@ -11,7 +11,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeaderVariant />
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"
@@ -40,7 +40,7 @@ exports[`App router component component no network failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeaderVariant />
<LearnerDashboardHeader />
<main>
<Dashboard />
</main>
@@ -63,7 +63,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
</title>
</HelmetWrapper>
<div>
<LearnerDashboardHeaderVariant />
<LearnerDashboardHeader />
<main>
<Alert
variant="danger"

View File

@@ -15,6 +15,7 @@ const configuration = {
ZENDESK_KEY: process.env.ZENDESK_KEY,
SUPPORT_URL: process.env.SUPPORT_URL || null,
ENABLE_NOTICES: process.env.ENABLE_NOTICES || null,
CAREER_LINK_URL: process.env.CAREER_LINK_URL || null,
};
const features = {};

View File

@@ -1,85 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { AvatarButton, Dropdown } from '@edx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import { useIsCollapsed, findCoursesNavDropdownClicked } from './hooks';
import messages from './messages';
export const AuthenticatedUserDropdown = ({ username }) => {
const { formatMessage } = useIntl();
const { authenticatedUser } = React.useContext(AppContext);
const dashboard = reduxHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
const { profileImage } = authenticatedUser;
return (
<Dropdown variant={isCollapsed ? 'light' : 'dark'} className="user-dropdown ml-1">
<Dropdown.Toggle
as={AvatarButton}
src={profileImage}
id="user"
variant="primary"
>
<span data-hj-suppress className="d-none d-md-inline">
{username}
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<Dropdown.Header>SWITCH DASHBOARD</Dropdown.Header>
<Dropdown.Item as="a" href="/edx-dashboard" className="active">Personal</Dropdown.Item>
{!!dashboard && (
<Dropdown.Item
as="a"
href={dashboard.url}
key={dashboard.label}
>
{dashboard.label} {formatMessage(messages.dashboard)}
</Dropdown.Item>
)}
<Dropdown.Divider />
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${username}`}>
{formatMessage(messages.profile)}
</Dropdown.Item>
{isCollapsed && (
<>
<Dropdown.Item href={urls.programsUrl}>
{formatMessage(messages.viewPrograms)}
</Dropdown.Item>
<Dropdown.Item href={courseSearchUrl} onClick={findCoursesNavDropdownClicked(courseSearchUrl)}>
{formatMessage(messages.exploreCourses)}
</Dropdown.Item>
</>
)}
<Dropdown.Item href={getConfig().ACCOUNT_SETTINGS_URL}>
{formatMessage(messages.account)}
</Dropdown.Item>
{getConfig().ORDER_HISTORY_URL && (
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
{formatMessage(messages.orderHistory)}
</Dropdown.Item>
)}
<Dropdown.Item href={getConfig().SUPPORT_URL}>
{formatMessage(messages.help)}
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item href={getConfig().LOGOUT_URL}>
{formatMessage(messages.signOut)}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
};
AuthenticatedUserDropdown.propTypes = {
username: PropTypes.string.isRequired,
};
export default AuthenticatedUserDropdown;

View File

@@ -1,63 +0,0 @@
import { shallow } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import { reduxHooks } from 'hooks';
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
import { useIsCollapsed } from './hooks';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
profileImage: 'profileImage',
},
},
}));
jest.mock('hooks', () => ({
reduxHooks: {
useEnterpriseDashboardData: jest.fn(),
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'test-course-search-url',
})),
},
}));
jest.mock('containers/LearnerDashboardHeader/hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
}));
const config = {
ACCOUNT_PROFILE_URL: 'http://account-profile-url.test',
ACCOUNT_SETTINGS_URL: 'http://account-settings-url.test',
LOGOUT_URL: 'http://logout-url.test',
ORDER_HISTORY_URL: 'http://order-history-url.test',
SUPPORT_URL: 'http://localhost:18000/support',
};
getConfig.mockReturnValue(config);
describe('AuthenticatedUserDropdown', () => {
const props = {
username: 'username',
};
const defaultDashboardData = {
label: 'label',
url: 'url',
};
describe('snapshots', () => {
test('with enterprise dashboard', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<AuthenticatedUserDropdown {...props} />);
expect(wrapper).toMatchSnapshot();
});
test('without enterprise dashboard and expanded', () => {
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<AuthenticatedUserDropdown {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -5,7 +5,7 @@ import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Button } from '@edx/paragon';
import { Button, Badge } from '@edx/paragon';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
@@ -45,11 +45,19 @@ export const CollapseMenuBody = ({ isOpen }) => {
</Button>
{authenticatedUser && (
<>
{dashboard && (
{!!dashboard && (
<Button as="a" href={dashboard.url} variant="inverse-primary">
{formatMessage(messages.dashboard)}
</Button>
)}
{!dashboard && getConfig().CAREER_LINK_URL && (
<Button href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Button>
)}
<Button
as="a"
href={`${getConfig().LMS_BASE_URL}/u/${

View File

@@ -4,7 +4,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Menu, Close } from '@edx/paragon/icons';
import { IconButton, Icon } from '@edx/paragon';
import { useLearnerDashboardHeaderVariantData, useIsCollapsed } from '../hooks';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
import CollapseMenuBody from './CollapseMenuBody';
import BrandLogo from '../BrandLogo';
@@ -14,7 +14,7 @@ import messages from '../messages';
export const CollapsedHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderVariantData();
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
return (
isCollapsed && (

View File

@@ -2,14 +2,14 @@ import { shallow } from 'enzyme';
import CollapsedHeader from '.';
import { useLearnerDashboardHeaderVariantData, useIsCollapsed } from '../hooks';
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(() => true),
useLearnerDashboardHeaderVariantData: jest.fn(() => ({
useLearnerDashboardHeaderData: jest.fn(() => ({
isOpen: false,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
})),
@@ -28,7 +28,7 @@ describe('CollapsedHeader', () => {
});
it('renders with isOpen true', () => {
useLearnerDashboardHeaderVariantData.mockReturnValueOnce({
useLearnerDashboardHeaderData.mockReturnValueOnce({
isOpen: true,
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
});

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { AvatarButton, Dropdown } from '@edx/paragon';
import { AvatarButton, Dropdown, Badge } from '@edx/paragon';
import { reduxHooks } from 'hooks';
@@ -39,6 +39,14 @@ export const AuthenticatedUserDropdown = () => {
</Dropdown.Item>
)}
<Dropdown.Divider />
{!dashboard && getConfig().CAREER_LINK_URL && (
<Dropdown.Item href={`${getConfig().CAREER_LINK_URL}`}>
{formatMessage(messages.career)}
<Badge className="px-2 mx-2" variant="warning">
{formatMessage(messages.newAlert)}
</Badge>
</Dropdown.Item>
)}
<Dropdown.Item href={`${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser.username}`}>
{formatMessage(messages.profile)}
</Dropdown.Item>

View File

@@ -36,6 +36,7 @@ const config = {
LOGOUT_URL: 'http://logout-url.test',
ORDER_HISTORY_URL: 'http://order-history-url.test',
SUPPORT_URL: 'http://localhost:18000/support',
CAREER_LINK_URL: 'http://localhost:18000/career',
};
getConfig.mockReturnValue(config);

View File

@@ -98,6 +98,17 @@ exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and ex
Personal
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://localhost:18000/career"
>
Career
<Badge
className="px-2 mx-2"
variant="warning"
>
New
</Badge>
</Dropdown.Item>
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>

View File

@@ -1,63 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Image } from '@edx/paragon';
import messages from './messages';
export const GreetingBanner = ({ size }) => {
const { formatMessage } = useIntl();
let greetMessage;
const hour = new Date().getHours();
if (hour > 16) {
greetMessage = messages.goodEvening;
} else if (hour > 11) {
greetMessage = messages.goodAfternoon;
} else {
greetMessage = messages.goodMorning;
}
const isSmall = size === 'small';
return (
<div
className={classNames(
'd-flex align-items-center justify-content-center',
{ 'pb-5': !isSmall, 'p-3.5': isSmall },
)}
>
<a href={`${getConfig().LMS_BASE_URL}/dashboard`}>
<Image
style={{ width: isSmall ? '46px' : '148px' }}
className="d-block"
src={getConfig().LOGO_WHITE_URL}
alt={getConfig().SITE_NAME}
/>
</a>
<div className={`greetings-slash-container-${size} bg-brand-500`} />
{isSmall
? (
<h5 role="presentation" className="text-center text-accent-b">
{formatMessage(greetMessage)}
</h5>
) : (
<h1
role="presentation"
className="text-center text-accent-b display-1"
>
{formatMessage(greetMessage)}
</h1>
)}
</div>
);
};
GreetingBanner.propTypes = {
size: PropTypes.oneOf(['small', 'large']).isRequired,
};
export default GreetingBanner;

View File

@@ -1,29 +0,0 @@
import { shallow } from 'enzyme';
import { GreetingBanner } from './GreetingBanner';
describe('GreetingBanner', () => {
const morning = new Date('2021-01-01T11:59:59.999');
const afternoon = new Date('2021-01-01T16:59:59.999');
const evening = new Date('2021-01-01T18:00:00');
afterAll(() => jest.useRealTimers());
describe('snapshots', () => {
['small', 'large'].forEach((size) => {
test(`with size ${size} and morning`, () => {
jest.useFakeTimers('modern').setSystemTime(morning);
const wrapper = shallow(<GreetingBanner size={size} />);
expect(wrapper).toMatchSnapshot();
});
test(`with size ${size} and afternoon`, () => {
jest.useFakeTimers('modern').setSystemTime(afternoon);
const wrapper = shallow(<GreetingBanner size={size} />);
expect(wrapper).toMatchSnapshot();
});
test(`with size ${size} and evening`, () => {
jest.useFakeTimers('modern').setSystemTime(evening);
const wrapper = shallow(<GreetingBanner size={size} />);
expect(wrapper).toMatchSnapshot();
});
});
});
});

View File

@@ -1,143 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
<Dropdown
className="user-dropdown ml-1"
variant="light"
>
<Dropdown.Toggle
id="user"
src="profileImage"
variant="primary"
>
<span
className="d-none d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Item
as="a"
href="url"
key="label"
>
label
Dashboard
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://localhost:18000/dashboard/programs"
>
View Programs
</Dropdown.Item>
<Dropdown.Item
href="test-course-search-url"
onClick={[MockFunction findCoursesNavDropdownClicked('test-course-search-url')]}
>
Explore courses
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Item
href="http://localhost:18000/support"
>
Help
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
<Dropdown
className="user-dropdown ml-1"
variant="dark"
>
<Dropdown.Toggle
id="user"
src="profileImage"
variant="primary"
>
<span
className="d-none d-md-inline"
data-hj-suppress={true}
>
username
</span>
</Dropdown.Toggle>
<Dropdown.Menu
className="dropdown-menu-right"
>
<Dropdown.Header>
SWITCH DASHBOARD
</Dropdown.Header>
<Dropdown.Item
as="a"
className="active"
href="/edx-dashboard"
>
Personal
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://account-profile-url.test/u/username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="http://account-settings-url.test"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="http://order-history-url.test"
>
Order History
</Dropdown.Item>
<Dropdown.Item
href="http://localhost:18000/support"
>
Help
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
href="http://logout-url.test"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
`;

View File

@@ -1,181 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GreetingBanner snapshots with size large and afternoon 1`] = `
<div
className="d-flex align-items-center justify-content-center pb-5"
>
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
}
}
/>
</a>
<div
className="greetings-slash-container-large bg-brand-500"
/>
<h1
className="text-center text-accent-b display-1"
role="presentation"
>
Good Afternoon!
</h1>
</div>
`;
exports[`GreetingBanner snapshots with size large and evening 1`] = `
<div
className="d-flex align-items-center justify-content-center pb-5"
>
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
}
}
/>
</a>
<div
className="greetings-slash-container-large bg-brand-500"
/>
<h1
className="text-center text-accent-b display-1"
role="presentation"
>
Good Evening!
</h1>
</div>
`;
exports[`GreetingBanner snapshots with size large and morning 1`] = `
<div
className="d-flex align-items-center justify-content-center pb-5"
>
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "148px",
}
}
/>
</a>
<div
className="greetings-slash-container-large bg-brand-500"
/>
<h1
className="text-center text-accent-b display-1"
role="presentation"
>
Good Morning!
</h1>
</div>
`;
exports[`GreetingBanner snapshots with size small and afternoon 1`] = `
<div
className="d-flex align-items-center justify-content-center p-3.5"
>
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
}
}
/>
</a>
<div
className="greetings-slash-container-small bg-brand-500"
/>
<h5
className="text-center text-accent-b"
role="presentation"
>
Good Afternoon!
</h5>
</div>
`;
exports[`GreetingBanner snapshots with size small and evening 1`] = `
<div
className="d-flex align-items-center justify-content-center p-3.5"
>
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
}
}
/>
</a>
<div
className="greetings-slash-container-small bg-brand-500"
/>
<h5
className="text-center text-accent-b"
role="presentation"
>
Good Evening!
</h5>
</div>
`;
exports[`GreetingBanner snapshots with size small and morning 1`] = `
<div
className="d-flex align-items-center justify-content-center p-3.5"
>
<a
href="http://localhost:18000/dashboard"
>
<Image
alt="localhost"
className="d-block"
src="https://edx-cdn.org/v3/default/logo-white.svg"
style={
Object {
"width": "46px",
}
}
/>
</a>
<div
className="greetings-slash-container-small bg-brand-500"
/>
<h5
className="text-center text-accent-b"
role="presentation"
>
Good Morning!
</h5>
</div>
`;

View File

@@ -1,97 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LearnerDashboardHeader UserMenu snapshots with authenticated user 1`] = `
<AuthenticatedUserDropdown
username="test-username"
/>
`;
exports[`LearnerDashboardHeader UserMenu snapshots without authenticated user 1`] = `""`;
exports[`LearnerDashboardHeader snapshots with collapsed 1`] = `
exports[`LearnerDashboardHeader render 1`] = `
<Fragment>
<ConfirmEmailBanner />
<div
className="flex-column bg-primary"
>
<header
className="learner-dashboard-header"
>
<div
className="d-flex"
>
<div
className="flex-grow-1"
>
<GreetingBanner
size="small"
/>
</div>
<div
className="my-auto ml-1 d-flex"
>
<IconButton
alt="Course search"
as="a"
href="test-course-search-url"
iconAs="Icon"
invertColors={true}
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
src={[MockFunction icons.Search]}
variant="primary"
/>
<UserMenu />
</div>
</div>
</header>
</div>
<MasqueradeBar />
</Fragment>
`;
exports[`LearnerDashboardHeader snapshots without collapsed 1`] = `
<Fragment>
<ConfirmEmailBanner />
<div
className="flex-column bg-primary"
>
<Image
className="d-block w-100 mb-4"
src="icon/mock/path"
/>
<header
className="learner-dashboard-header"
>
<div
className="d-flex"
>
<Button
as="a"
href="http://localhost:18000/dashboard/programs"
iconBefore={[MockFunction icons.Program]}
variant="inverse-tertiary"
>
Switch to Programs
</Button>
<div
className="flex-grow-1"
/>
<Button
as="a"
href="test-course-search-url"
iconBefore={[MockFunction icons.Search]}
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
variant="inverse-tertiary"
>
Explore courses
</Button>
<UserMenu />
</div>
</header>
<GreetingBanner
size="large"
/>
</div>
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</Fragment>
`;

View File

@@ -1,11 +1,18 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@edx/paragon';
import track from 'tracking';
import { StrictDict } from 'utils';
import { linkNames } from 'tracking/constants';
import * as module from './hooks';
export const state = StrictDict({
isOpen: (val) => React.useState(val), // eslint-disable-line
});
export const useIsCollapsed = () => {
const { width } = useWindowSize();
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.maxWidth), [width]);
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.minWidth), [width]);
return isCollapsed;
};
@@ -17,8 +24,19 @@ export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCou
linkName: linkNames.learnerHomeNavDropdownExplore,
});
export const useLearnerDashboardHeaderData = () => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
return {
isOpen,
toggleIsOpen,
};
};
export default {
useIsCollapsed,
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
};

View File

@@ -1,7 +1,19 @@
import { useWindowSize, breakpoints } from '@edx/paragon';
import track from 'tracking';
import { linkNames } from 'tracking/constants';
import { useIsCollapsed, findCoursesNavClicked, findCoursesNavDropdownClicked } from './hooks';
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
const state = new MockUseState(hooks);
const {
useIsCollapsed,
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderData,
} = hooks;
jest.mock('tracking', () => ({
findCourses: {
@@ -12,13 +24,17 @@ jest.mock('tracking', () => ({
const url = 'http://example.com';
describe('LearnerDashboardHeader hooks', () => {
describe('state values', () => {
state.testGetter(state.keys.isOpen);
});
describe('useIsCollapsed', () => {
test('large screen is not collapsed', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.maxWidth + 1 });
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth + 1 });
expect(useIsCollapsed()).toEqual(false);
});
test('small screen is collapsed', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.maxWidth - 1 });
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth - 1 });
expect(useIsCollapsed()).toEqual(true);
});
});
@@ -40,4 +56,14 @@ describe('LearnerDashboardHeader hooks', () => {
});
});
});
describe('useLearnerDashboardHeaderData', () => {
test('default state', () => {
state.mock();
const out = useLearnerDashboardHeaderData();
state.expectInitializedWith(state.keys.isOpen, false);
out.toggleIsOpen();
expect(state.values.isOpen).toEqual(true);
});
});
});

View File

@@ -1,92 +1,22 @@
import React, { useContext } from 'react';
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Program, Search } from '@edx/paragon/icons';
import {
Button, Image, IconButton, Icon,
} from '@edx/paragon';
import topBanner from 'assets/top_stripe.svg';
import MasqueradeBar from 'containers/MasqueradeBar';
import urls from 'data/services/lms/urls';
import { reduxHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import GreetingBanner from './GreetingBanner';
import ConfirmEmailBanner from './ConfirmEmailBanner';
import { useIsCollapsed, findCoursesNavClicked } from './hooks';
import messages from './messages';
import CollapsedHeader from './CollapsedHeader';
import ExpandedHeader from './ExpandedHeader';
import './index.scss';
export const UserMenu = () => {
const { authenticatedUser } = useContext(AppContext);
return authenticatedUser ? (<AuthenticatedUserDropdown username={authenticatedUser.username} />) : null;
};
export const LearnerDashboardHeader = () => (
<>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</>
);
export const LearnerDashboardHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
return (
<>
<ConfirmEmailBanner />
<div className="flex-column bg-primary">
{!(isCollapsed) && (
<Image className="d-block w-100 mb-4" src={topBanner} />
)}
<header className="learner-dashboard-header">
<div className="d-flex">
{(!isCollapsed) && (
<Button as="a" href={urls.programsUrl} variant="inverse-tertiary" iconBefore={Program}>
{formatMessage(messages.switchToProgram)}
</Button>
)}
<div className="flex-grow-1">
{isCollapsed && <GreetingBanner size="small" />}
</div>
{isCollapsed ? (
<div className="my-auto ml-1 d-flex">
<IconButton
alt={formatMessage(messages.courseSearchAlt)}
as="a"
href={courseSearchUrl}
variant="primary"
invertColors
src={Search}
iconAs={Icon}
onClick={exploreCoursesClick}
/>
<UserMenu />
</div>
) : (
<>
<Button
as="a"
href={courseSearchUrl}
variant="inverse-tertiary"
iconBefore={Search}
onClick={exploreCoursesClick}
>
{formatMessage(messages.exploreCourses)}
</Button>
<UserMenu />
</>
)}
</div>
</header>
{!isCollapsed && <GreetingBanner size="large" />}
</div>
<MasqueradeBar />
</>
);
};
LearnerDashboardHeader.propTypes = {
};
LearnerDashboardHeader.propTypes = {};
export default LearnerDashboardHeader;

View File

@@ -1,12 +1,38 @@
.greetings-slash-container-small {
height: 4px;
width: 40px;
transform-origin: center;
transform: rotate(-70deg);
.dropdown-menu-collapse {
width: 100vw;
position: absolute;
left: 0;
}
.greetings-slash-container-large {
height: 8px;
width: 120px;
transform-origin: center;
transform: rotate(-70deg);
.learner-variant-header {
a {
// needed to make the link not resize the header
border-bottom: 2px solid transparent;
}
.course-link {
border-bottom: 2px solid !important;
}
.course-link:hover {
border-bottom: inherit !important;
}
}
.nav-small-menu {
> * {
justify-content: flex-start !important;
border-radius: 0 !important;
border-top: 1px solid #ddd !important;
&::after {
content: '\00BB';
padding-left: 10px;
}
}
}
.logo {
// copy from legacy dashboard
height: 40px;
}

View File

@@ -1,59 +1,18 @@
import { shallow } from 'enzyme';
import { AppContext } from '@edx/frontend-platform/react';
import LearnerDashboardHeader from '.';
import LearnerDashboardHeader, { UserMenu } from '.';
import { useIsCollapsed } from './hooks';
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: {
authenticatedUser: {
username: 'test-username',
},
},
}));
jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavClicked: (href) => jest.fn().mockName(`findCoursesNavClicked('${href}')`),
}));
jest.mock('hooks', () => ({
reduxHooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'test-course-search-url',
})),
},
}));
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
jest.mock('./ConfirmEmailBanner', () => 'ConfirmEmailBanner');
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('./GreetingBanner', () => 'GreetingBanner');
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
describe('LearnerDashboardHeader', () => {
describe('snapshots', () => {
test('with collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper).toMatchSnapshot();
});
test('without collapsed', () => {
useIsCollapsed.mockReturnValueOnce(false);
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper).toMatchSnapshot();
});
});
describe('UserMenu', () => {
describe('snapshots', () => {
test('with authenticated user', () => {
const wrapper = shallow(<UserMenu />);
expect(wrapper).toMatchSnapshot();
});
test('without authenticated user', () => {
AppContext.authenticatedUser = null;
const wrapper = shallow(<UserMenu />);
expect(wrapper).toMatchSnapshot();
});
});
test('render', () => {
const wrapper = shallow(<LearnerDashboardHeader />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ConfirmEmailBanner')).toHaveLength(1);
expect(wrapper.find('MasqueradeBar')).toHaveLength(1);
expect(wrapper.find('CollapsedHeader')).toHaveLength(1);
expect(wrapper.find('ExpandedHeader')).toHaveLength(1);
});
});

View File

@@ -2,70 +2,79 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
dashboard: {
id: 'leanerDashboard.menu.dashboard.label',
id: 'learnerVariantDashboard.menu.dashboard.label',
defaultMessage: 'Dashboard',
description: 'The text for the user menu Dashboard navigation link.',
},
help: {
id: 'leanerDashboard.help.label',
id: 'learnerVariantDashboard.help.label',
defaultMessage: 'Help',
description: 'The text for the link to the Help Center',
},
profile: {
id: 'leanerDashboard.menu.profile.label',
id: 'learnerVariantDashboard.menu.profile.label',
defaultMessage: 'Profile',
description: 'The text for the user menu Profile navigation link.',
},
viewPrograms: {
id: 'leanerDashboard.menu.viewPrograms.label',
id: 'learnerVariantDashboard.menu.viewPrograms.label',
defaultMessage: 'View Programs',
description: 'The text for the user menu View Programs navigation link.',
},
account: {
id: 'leanerDashboard.menu.account.label',
id: 'learnerVariantDashboard.menu.account.label',
defaultMessage: 'Account',
description: 'The text for the user menu Account navigation link.',
},
orderHistory: {
id: 'leanerDashboard.menu.orderHistory.label',
id: 'learnerVariantDashboard.menu.orderHistory.label',
defaultMessage: 'Order History',
description: 'The text for the user menu Order History navigation link.',
},
signOut: {
id: 'leanerDashboard.menu.signOut.label',
id: 'learnerVariantDashboard.menu.signOut.label',
defaultMessage: 'Sign Out',
description: 'The label for the user menu Sign Out action.',
},
goodMorning: {
id: 'greeting.morning',
defaultMessage: 'Good Morning!',
description: 'Good Morning',
course: {
id: 'learnerVariantDashboard.course',
defaultMessage: 'Courses',
description: 'Header link for switching to dashboard page.',
},
goodAfternoon: {
id: 'greeting.afternoon',
defaultMessage: 'Good Afternoon!',
description: 'Good Afternoon',
},
goodEvening: {
id: 'greeting.evening',
defaultMessage: 'Good Evening!',
description: 'Good Evening',
},
switchToProgram: {
id: 'leanerDashboard.switchToProgram',
defaultMessage: 'Switch to Programs',
program: {
id: 'learnerVariantDashboard.program',
defaultMessage: 'Programs',
description: 'Header link for switching to program page.',
},
exploreCourses: {
id: 'leanerDashboard.exploreCourses',
defaultMessage: 'Explore courses',
description: 'Header link for switching to course page.',
discoverNew: {
id: 'learnerVariantDashboard.discoverNew',
defaultMessage: 'Discover New',
description: 'Header link for switching to discover page.',
},
courseSearchAlt: {
id: 'leanerDashboard.courseSearchAlt',
defaultMessage: 'Course search',
description: 'Alt-text for course search icon button',
logoAltText: {
id: 'learnerVariantDashboard.logoAltText',
defaultMessage: 'edX, Inc. Dashboard',
description: 'Alt text for the edX logo.',
},
collapseMenuOpenAltText: {
id: 'learnerVariantDashboard.collapseMenuOpenAltText',
defaultMessage: 'Menu',
description: 'Alt text for the collapse menu icon when the menu is open.',
},
collapseMenuClosedAltText: {
id: 'learnerVariantDashboard.collapseMenuClosedAltText',
defaultMessage: 'Close',
description: 'Alt text for the collapse menu icon when the menu is closed.',
},
career: {
id: 'leanerDashboard.menu.career.label',
defaultMessage: 'Career',
description: 'The text for the user menu Career navigation link.',
},
newAlert: {
id: 'header.menu.new.label',
defaultMessage: 'New',
description: 'The text announcing that an item in the user menu is New',
},
});

View File

@@ -1,10 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LearnerDashboardHeaderVariant render 1`] = `
<Fragment>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</Fragment>
`;

View File

@@ -1,42 +0,0 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@edx/paragon';
import track from 'tracking';
import { StrictDict } from 'utils';
import { linkNames } from 'tracking/constants';
import * as module from './hooks';
export const state = StrictDict({
isOpen: (val) => React.useState(val), // eslint-disable-line
});
export const useIsCollapsed = () => {
const { width } = useWindowSize();
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.minWidth), [width]);
return isCollapsed;
};
export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.learnerHomeNavExplore,
});
export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.learnerHomeNavDropdownExplore,
});
export const useLearnerDashboardHeaderVariantData = () => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const toggleIsOpen = () => setIsOpen(!isOpen);
return {
isOpen,
toggleIsOpen,
};
};
export default {
useIsCollapsed,
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderVariantData,
};

View File

@@ -1,69 +0,0 @@
import { useWindowSize, breakpoints } from '@edx/paragon';
import track from 'tracking';
import { linkNames } from 'tracking/constants';
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
const state = new MockUseState(hooks);
const {
useIsCollapsed,
findCoursesNavClicked,
findCoursesNavDropdownClicked,
useLearnerDashboardHeaderVariantData,
} = hooks;
jest.mock('tracking', () => ({
findCourses: {
findCoursesClicked: jest.fn(),
},
}));
const url = 'http://example.com';
describe('LearnerDashboardHeaderVariant hooks', () => {
describe('state values', () => {
state.testGetter(state.keys.isOpen);
});
describe('useIsCollapsed', () => {
test('large screen is not collapsed', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth + 1 });
expect(useIsCollapsed()).toEqual(false);
});
test('small screen is collapsed', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.large.minWidth - 1 });
expect(useIsCollapsed()).toEqual(true);
});
});
describe('findCoursesNavClicked', () => {
test('calls tracking with nav link name', () => {
findCoursesNavClicked(url);
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
linkName: linkNames.learnerHomeNavExplore,
});
});
});
describe('findCoursesNavDropdownClicked', () => {
test('calls tracking with dropdown link name', () => {
findCoursesNavDropdownClicked(url);
expect(track.findCourses.findCoursesClicked).toHaveBeenCalledWith(url, {
linkName: linkNames.learnerHomeNavDropdownExplore,
});
});
});
describe('useLearnerDashboardHeaderVariantData', () => {
test('default state', () => {
state.mock();
const out = useLearnerDashboardHeaderVariantData();
state.expectInitializedWith(state.keys.isOpen, false);
out.toggleIsOpen();
expect(state.values.isOpen).toEqual(true);
});
});
});

View File

@@ -1,22 +0,0 @@
import React from 'react';
import MasqueradeBar from 'containers/MasqueradeBar';
import ConfirmEmailBanner from 'containers/LearnerDashboardHeader/ConfirmEmailBanner';
import CollapsedHeader from './CollapsedHeader';
import ExpandedHeader from './ExpandedHeader';
import './index.scss';
export const LearnerDashboardHeaderVariant = () => (
<>
<ConfirmEmailBanner />
<CollapsedHeader />
<ExpandedHeader />
<MasqueradeBar />
</>
);
LearnerDashboardHeaderVariant.propTypes = {};
export default LearnerDashboardHeaderVariant;

View File

@@ -1,38 +0,0 @@
.dropdown-menu-collapse {
width: 100vw;
position: absolute;
left: 0;
}
.learner-variant-header {
a {
// needed to make the link not resize the header
border-bottom: 2px solid transparent;
}
.course-link {
border-bottom: 2px solid !important;
}
.course-link:hover {
border-bottom: inherit !important;
}
}
.nav-small-menu {
> * {
justify-content: flex-start !important;
border-radius: 0 !important;
border-top: 1px solid #ddd !important;
&::after {
content: '\00BB';
padding-left: 10px;
}
}
}
.logo {
// copy from legacy dashboard
height: 40px;
}

View File

@@ -1,18 +0,0 @@
import { shallow } from 'enzyme';
import LearnerDashboardHeaderVariant from '.';
jest.mock('containers/LearnerDashboardHeader/ConfirmEmailBanner', () => 'ConfirmEmailBanner');
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');
jest.mock('./CollapsedHeader', () => 'CollapsedHeader');
jest.mock('./ExpandedHeader', () => 'ExpandedHeader');
describe('LearnerDashboardHeaderVariant', () => {
test('render', () => {
const wrapper = shallow(<LearnerDashboardHeaderVariant />);
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('ConfirmEmailBanner')).toHaveLength(1);
expect(wrapper.find('MasqueradeBar')).toHaveLength(1);
expect(wrapper.find('CollapsedHeader')).toHaveLength(1);
expect(wrapper.find('ExpandedHeader')).toHaveLength(1);
});
});

View File

@@ -1,71 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
dashboard: {
id: 'learnerVariantDashboard.menu.dashboard.label',
defaultMessage: 'Dashboard',
description: 'The text for the user menu Dashboard navigation link.',
},
help: {
id: 'learnerVariantDashboard.help.label',
defaultMessage: 'Help',
description: 'The text for the link to the Help Center',
},
profile: {
id: 'learnerVariantDashboard.menu.profile.label',
defaultMessage: 'Profile',
description: 'The text for the user menu Profile navigation link.',
},
viewPrograms: {
id: 'learnerVariantDashboard.menu.viewPrograms.label',
defaultMessage: 'View Programs',
description: 'The text for the user menu View Programs navigation link.',
},
account: {
id: 'learnerVariantDashboard.menu.account.label',
defaultMessage: 'Account',
description: 'The text for the user menu Account navigation link.',
},
orderHistory: {
id: 'learnerVariantDashboard.menu.orderHistory.label',
defaultMessage: 'Order History',
description: 'The text for the user menu Order History navigation link.',
},
signOut: {
id: 'learnerVariantDashboard.menu.signOut.label',
defaultMessage: 'Sign Out',
description: 'The label for the user menu Sign Out action.',
},
course: {
id: 'learnerVariantDashboard.course',
defaultMessage: 'Courses',
description: 'Header link for switching to dashboard page.',
},
program: {
id: 'learnerVariantDashboard.program',
defaultMessage: 'Programs',
description: 'Header link for switching to program page.',
},
discoverNew: {
id: 'learnerVariantDashboard.discoverNew',
defaultMessage: 'Discover New',
description: 'Header link for switching to discover page.',
},
logoAltText: {
id: 'learnerVariantDashboard.logoAltText',
defaultMessage: 'edX, Inc. Dashboard',
description: 'Alt text for the edX logo.',
},
collapseMenuOpenAltText: {
id: 'learnerVariantDashboard.collapseMenuOpenAltText',
defaultMessage: 'Menu',
description: 'Alt text for the collapse menu icon when the menu is open.',
},
collapseMenuClosedAltText: {
id: 'learnerVariantDashboard.collapseMenuClosedAltText',
defaultMessage: 'Close',
description: 'Alt text for the collapse menu icon when the menu is closed.',
},
});
export default messages;