feat: update related program location (#104)
This commit is contained in:
@@ -17,13 +17,13 @@ import {
|
||||
actions,
|
||||
} from 'data/redux';
|
||||
import { reduxHooks } from 'hooks';
|
||||
import LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import Dashboard from 'containers/Dashboard';
|
||||
import ZendeskFab from 'components/ZendeskFab';
|
||||
|
||||
import track from 'tracking';
|
||||
|
||||
import fakeData from 'data/services/lms/fakeData/courses';
|
||||
import LearnerDashboardHeaderVariant from './containers/LearnerDashboardHeaderVariant';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -77,7 +77,7 @@ export const App = () => {
|
||||
<title>{formatMessage(messages.pageTitle)}</title>
|
||||
</Helmet>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
{hasNetworkFailure
|
||||
? (
|
||||
|
||||
@@ -11,6 +11,12 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
|
||||
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alert.alert-info .alert-icon {
|
||||
color: black;
|
||||
}
|
||||
|
||||
@@ -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 LearnerDashboardHeader from 'containers/LearnerDashboardHeader';
|
||||
import LearnerDashboardHeaderVariant from 'containers/LearnerDashboardHeaderVariant';
|
||||
import { App } from './App';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/Dashboard', () => 'Dashboard');
|
||||
jest.mock('containers/LearnerDashboardHeader', () => 'LearnerDashboardHeader');
|
||||
jest.mock('containers/LearnerDashboardHeaderVariant', () => 'LearnerDashboardHeaderVariant');
|
||||
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(LearnerDashboardHeader).length).toEqual(1);
|
||||
expect(el.find(LearnerDashboardHeaderVariant).length).toEqual(1);
|
||||
});
|
||||
it('wraps the page in a browser router', () => {
|
||||
expect(el.find(Router)).toMatchObject(el);
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
@@ -40,7 +40,7 @@ exports[`App router component component no network failure snapshot 1`] = `
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Dashboard />
|
||||
</main>
|
||||
@@ -63,7 +63,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
|
||||
</title>
|
||||
</HelmetWrapper>
|
||||
<div>
|
||||
<LearnerDashboardHeader />
|
||||
<LearnerDashboardHeaderVariant />
|
||||
<main>
|
||||
<Alert
|
||||
variant="danger"
|
||||
|
||||
@@ -4,19 +4,23 @@ import PropTypes from 'prop-types';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
export const Banner = ({ children, variant, icon }) => (
|
||||
<Alert variant={variant} className="mb-0" icon={icon}>
|
||||
export const Banner = ({
|
||||
children, variant, icon, className,
|
||||
}) => (
|
||||
<Alert variant={variant} className={className} icon={icon}>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
Banner.defaultProps = {
|
||||
icon: Info,
|
||||
variant: 'info',
|
||||
className: 'mb-0',
|
||||
};
|
||||
Banner.propTypes = {
|
||||
variant: PropTypes.string,
|
||||
icon: PropTypes.func,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Banner;
|
||||
|
||||
@@ -19,5 +19,9 @@ describe('Banner', () => {
|
||||
|
||||
expect(wrapper.find(Alert).prop('variant')).toEqual('success');
|
||||
});
|
||||
test('renders with custom class', () => {
|
||||
const wrapper = shallow(<Banner {...props} className="custom-class" />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
16
src/components/EmailLink.test.jsx
Normal file
16
src/components/EmailLink.test.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import EmailLink from './EmailLink';
|
||||
|
||||
describe('EmailLink', () => {
|
||||
it('renders null when no address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
it('renders a MailtoLink when an address is provided', () => {
|
||||
const wrapper = shallow(<EmailLink address="test@email.com" />);
|
||||
expect(wrapper.find('MailtoLink').length).toEqual(1);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,16 @@ exports[`Banner snapshot renders default banner 1`] = `
|
||||
</Alert>
|
||||
`;
|
||||
|
||||
exports[`Banner snapshot renders with custom class 1`] = `
|
||||
<Alert
|
||||
className="custom-class"
|
||||
icon={[MockFunction icons.Info]}
|
||||
variant="info"
|
||||
>
|
||||
Hello, world!
|
||||
</Alert>
|
||||
`;
|
||||
|
||||
exports[`Banner snapshot renders with variants 1`] = `
|
||||
<Alert
|
||||
className="mb-0"
|
||||
|
||||
11
src/components/__snapshots__/EmailLink.test.jsx.snap
Normal file
11
src/components/__snapshots__/EmailLink.test.jsx.snap
Normal file
@@ -0,0 +1,11 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmailLink renders a MailtoLink when an address is provided 1`] = `
|
||||
<MailtoLink
|
||||
to="test@email.com"
|
||||
>
|
||||
test@email.com
|
||||
</MailtoLink>
|
||||
`;
|
||||
|
||||
exports[`EmailLink renders null when no address is provided 1`] = `""`;
|
||||
@@ -51,10 +51,25 @@
|
||||
|
||||
.course-card-banners {
|
||||
> .alert {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: map-get($spacers, 3) map-get($spacers, 4);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-left-radius: $alert-border-radius;
|
||||
border-bottom-right-radius: $alert-border-radius;
|
||||
}
|
||||
}
|
||||
|
||||
.related-programs-banner {
|
||||
.related-programs-list-container {
|
||||
list-style: none;
|
||||
display: inline;
|
||||
|
||||
> li {
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ exports[`CourseCard component snapshot: collapsed 1`] = `
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
>
|
||||
<RelatedProgramsBadge
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseCardActions
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
@@ -99,9 +96,6 @@ exports[`CourseCard component snapshot: not collapsed 1`] = `
|
||||
<Card.Footer
|
||||
orientation="horizontal"
|
||||
>
|
||||
<RelatedProgramsBadge
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseCardActions
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ProgramsList = ({ programs }) => (
|
||||
<ul className="related-programs-list-container">
|
||||
{programs.map((program) => (
|
||||
<li key={program.programUrl} className="my-2">
|
||||
<a href={program.programUrl}>{program.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
ProgramsList.propTypes = {
|
||||
programs: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
programUrl: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default ProgramsList;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ProgramsList } from './ProgramsList';
|
||||
|
||||
describe('ProgramsList', () => {
|
||||
const programs = [
|
||||
{
|
||||
programUrl: 'http://example.com',
|
||||
title: 'Example Program 1',
|
||||
},
|
||||
{
|
||||
programUrl: 'http://example.com',
|
||||
title: 'Example Program 2',
|
||||
},
|
||||
];
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(<ProgramsList programs={programs} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.find('li').length).toEqual(programs.length);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ProgramsList renders correctly 1`] = `
|
||||
<ul
|
||||
className="related-programs-list-container"
|
||||
>
|
||||
<li
|
||||
className="my-2"
|
||||
key="http://example.com"
|
||||
>
|
||||
<a
|
||||
href="http://example.com"
|
||||
>
|
||||
Example Program 1
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="my-2"
|
||||
key="http://example.com"
|
||||
>
|
||||
<a
|
||||
href="http://example.com"
|
||||
>
|
||||
Example Program 2
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RelatedProgramsBanner render with programs 1`] = `
|
||||
<Banner
|
||||
className="bg-white border-top border-bottom mb-0 related-programs-banner"
|
||||
icon={[MockFunction icons.Program]}
|
||||
variant="info"
|
||||
>
|
||||
Related Programs:
|
||||
<ProgramsList
|
||||
programs={
|
||||
Array [
|
||||
Object {
|
||||
"title": "Program 1",
|
||||
"url": "http://example.com/program1",
|
||||
},
|
||||
Object {
|
||||
"title": "Program 2",
|
||||
"url": "http://example.com/program2",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Banner>
|
||||
`;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Program } from '@edx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
import ProgramList from './ProgramsList';
|
||||
import messages from './messages';
|
||||
|
||||
export const RelatedProgramsBanner = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const programData = reduxHooks.useCardRelatedProgramsData(cardId);
|
||||
|
||||
return (
|
||||
programData?.length > 0 && (
|
||||
<Banner
|
||||
icon={Program}
|
||||
className="bg-white border-top border-bottom mb-0 related-programs-banner"
|
||||
>
|
||||
{formatMessage(messages.relatedPrograms)}
|
||||
<ProgramList programs={programData.list} />
|
||||
</Banner>
|
||||
)
|
||||
);
|
||||
};
|
||||
RelatedProgramsBanner.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RelatedProgramsBanner;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import RelatedProgramsBanner from '.';
|
||||
|
||||
jest.mock('./ProgramsList', () => 'ProgramsList');
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardRelatedProgramsData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
describe('RelatedProgramsBanner', () => {
|
||||
test('render empty', () => {
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
|
||||
length: 0,
|
||||
});
|
||||
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
|
||||
test('render with programs', () => {
|
||||
reduxHooks.useCardRelatedProgramsData.mockReturnValue({
|
||||
list: [
|
||||
{
|
||||
title: 'Program 1',
|
||||
url: 'http://example.com/program1',
|
||||
},
|
||||
{
|
||||
title: 'Program 2',
|
||||
url: 'http://example.com/program2',
|
||||
},
|
||||
],
|
||||
length: 2,
|
||||
});
|
||||
const el = shallow(<RelatedProgramsBanner cardId={cardId} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const messages = StrictDict({
|
||||
relatedPrograms: {
|
||||
id: 'learner-dash.courseCard.banners.relatedPrograms',
|
||||
description: 'title for related programs banner',
|
||||
defaultMessage: 'Related Programs: ',
|
||||
},
|
||||
expandBanner: {
|
||||
id: 'learner-dash.courseCard.banners.expandBanner',
|
||||
description: 'expand banner button text',
|
||||
defaultMessage: 'More',
|
||||
},
|
||||
expandBannerAlt: {
|
||||
id: 'learner-dash.courseCard.banners.expandBannerAlt',
|
||||
description: 'expand banner button alt text',
|
||||
defaultMessage: 'Expand banner icon',
|
||||
},
|
||||
collapseBanner: {
|
||||
id: 'learner-dash.courseCard.banners.collapseBanner',
|
||||
description: 'collapse banner button text',
|
||||
defaultMessage: 'Less',
|
||||
},
|
||||
collapseBannerAlt: {
|
||||
id: 'learner-dash.courseCard.banners.collapseBannerAlt',
|
||||
description: 'collapse banner button alt text',
|
||||
defaultMessage: 'Collapse banner icon',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,41 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCardBanners render with isEnrolled false 1`] = `
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<RelatedProgramsBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<EntitlementBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseCardBanners renders default CourseCardBanners 1`] = `
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<RelatedProgramsBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CourseBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<EntitlementBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CertificateBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CreditBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -7,11 +7,13 @@ import CourseBanner from './CourseBanner';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
import CreditBanner from './CreditBanner';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
import RelatedProgramsBanner from './RelatedProgramsBanner';
|
||||
|
||||
export const CourseCardBanners = ({ cardId }) => {
|
||||
const { isEnrolled } = reduxHooks.useCardEnrollmentData(cardId);
|
||||
return (
|
||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||
<RelatedProgramsBanner cardId={cardId} />
|
||||
<CourseBanner cardId={cardId} />
|
||||
<EntitlementBanner cardId={cardId} />
|
||||
{isEnrolled && <CertificateBanner cardId={cardId} />}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import CourseCardBanners from '.';
|
||||
|
||||
jest.mock('./CourseBanner', () => 'CourseBanner');
|
||||
jest.mock('./CertificateBanner', () => 'CertificateBanner');
|
||||
jest.mock('./CreditBanner', () => 'CreditBanner');
|
||||
jest.mock('./EntitlementBanner', () => 'EntitlementBanner');
|
||||
jest.mock('./RelatedProgramsBanner', () => 'RelatedProgramsBanner');
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useCardEnrollmentData: jest.fn(() => ({ isEnrolled: true })),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CourseCardBanners', () => {
|
||||
const props = {
|
||||
cardId: 'test-card-id',
|
||||
};
|
||||
test('renders default CourseCardBanners', () => {
|
||||
const wrapper = shallow(<CourseCardBanners {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('render with isEnrolled false', () => {
|
||||
reduxHooks.useCardEnrollmentData.mockReturnValueOnce({ isEnrolled: false });
|
||||
const wrapper = shallow(<CourseCardBanners {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,6 @@ import CourseCardMenu from './components/CourseCardMenu';
|
||||
import CourseCardActions from './components/CourseCardActions';
|
||||
import CourseCardDetails from './components/CourseCardDetails';
|
||||
import CourseCardTitle from './components/CourseCardTitle';
|
||||
import RelatedProgramsBadge from './components/RelatedProgramsBadge';
|
||||
|
||||
import './CourseCard.scss';
|
||||
|
||||
@@ -34,7 +33,6 @@ export const CourseCard = ({
|
||||
<CourseCardDetails cardId={cardId} />
|
||||
</Card.Section>
|
||||
<Card.Footer orientation={orientation}>
|
||||
<RelatedProgramsBadge cardId={cardId} />
|
||||
<CourseCardActions cardId={cardId} />
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
|
||||
@@ -14,7 +14,6 @@ jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu');
|
||||
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
|
||||
jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
|
||||
jest.mock('./components/CourseCardTitle', () => 'CourseCardTitle');
|
||||
jest.mock('./components/RelatedProgramsBadge', () => 'RelatedProgramsBadge');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ exports[`NoCoursesView snapshot 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
href="course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
variant="brand"
|
||||
>
|
||||
Explore courses
|
||||
|
||||
@@ -3,20 +3,14 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import EmptyCourse from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
hooks: {
|
||||
useRecommendedCoursesData: jest.fn(() => ({ courses: [], isControl: false })),
|
||||
useRequestIsPending: jest.fn(),
|
||||
usePlatformSettingsData: () => ({
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: 'course-search-url',
|
||||
}),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('containers/Dashboard/hooks', () => ({
|
||||
useIsDashboardCollapsed: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
describe('NoCoursesView', () => {
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<EmptyCourse />)).toMatchSnapshot();
|
||||
|
||||
@@ -37,6 +37,7 @@ exports[`LearnerDashboardHeader snapshots with collapsed 1`] = `
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
|
||||
src={[MockFunction icons.Search]}
|
||||
variant="primary"
|
||||
/>
|
||||
<UserMenu />
|
||||
@@ -78,6 +79,7 @@ exports[`LearnerDashboardHeader snapshots without collapsed 1`] = `
|
||||
<Button
|
||||
as="a"
|
||||
href="test-course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
|
||||
variant="inverse-tertiary"
|
||||
>
|
||||
|
||||
26
src/containers/LearnerDashboardHeaderVariant/BrandLogo.jsx
Normal file
26
src/containers/LearnerDashboardHeaderVariant/BrandLogo.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const BrandLogo = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
|
||||
return (
|
||||
<a href={dashboard?.url || '/'} className="mx-auto">
|
||||
<img
|
||||
className="logo py-3"
|
||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
||||
alt={formatMessage(messages.logoAltText)}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
BrandLogo.propTypes = {};
|
||||
|
||||
export default BrandLogo;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import BrandLogo from './BrandLogo';
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('BrandLogo', () => {
|
||||
test('dashboard defined', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce({
|
||||
url: 'url',
|
||||
});
|
||||
const wrapper = shallow(<BrandLogo />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('a').prop('href')).toEqual('url');
|
||||
});
|
||||
|
||||
test('dashboard undefined', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
|
||||
const wrapper = shallow(<BrandLogo />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('a').prop('href')).toEqual('/');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
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 { Button } from '@edx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import { findCoursesNavDropdownClicked } from '../hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
export const CollapseMenuBody = ({ isOpen }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavDropdownClicked(courseSearchUrl);
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<div className="d-flex flex-column shadow-sm nav-small-menu">
|
||||
<Button as="a" href="/" variant="inverse-primary">
|
||||
{formatMessage(messages.course)}
|
||||
</Button>
|
||||
<Button as="a" href={urls.programsUrl} variant="inverse-primary">
|
||||
{formatMessage(messages.program)}
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
variant="inverse-primary"
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
||||
{formatMessage(messages.help)}
|
||||
</Button>
|
||||
{authenticatedUser && (
|
||||
<>
|
||||
{dashboard && (
|
||||
<Button as="a" href={dashboard.url} variant="inverse-primary">
|
||||
{formatMessage(messages.dashboard)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as="a"
|
||||
href={`${getConfig().LMS_BASE_URL}/u/${
|
||||
authenticatedUser.username
|
||||
}`}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
{formatMessage(messages.profile)}
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={`${getConfig().LMS_BASE_URL}/account/settings`}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
{formatMessage(messages.account)}
|
||||
</Button>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
<Button
|
||||
as="a"
|
||||
variant="inverse-primary"
|
||||
href={getConfig().ORDER_HISTORY_URL}
|
||||
>
|
||||
{formatMessage(messages.orderHistory)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
as="a"
|
||||
href={getConfig().LOGOUT_URL}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
{formatMessage(messages.signOut)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
CollapseMenuBody.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default CollapseMenuBody;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import CollapseMenuBody from './CollapseMenuBody';
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
username: 'username',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: () => ({
|
||||
url: 'url',
|
||||
}),
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: 'courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
|
||||
}));
|
||||
|
||||
describe('CollapseMenuBody', () => {
|
||||
test('render', () => {
|
||||
const wrapper = shallow(<CollapseMenuBody isOpen />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render empty if not open', () => {
|
||||
const wrapper = shallow(<CollapseMenuBody isOpen={false} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
test('render unauthenticated', () => {
|
||||
const { authenticatedUser } = AppContext;
|
||||
AppContext.authenticatedUser = null;
|
||||
const wrapper = shallow(<CollapseMenuBody isOpen />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
AppContext.authenticatedUser = authenticatedUser;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollapseMenuBody render 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column shadow-sm nav-small-menu"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="url"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/u/username"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/account/settings"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Account
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/logout"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CollapseMenuBody render empty if not open 1`] = `""`;
|
||||
|
||||
exports[`CollapseMenuBody render unauthenticated 1`] = `
|
||||
<div
|
||||
className="d-flex flex-column shadow-sm nav-small-menu"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,48 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CollapsedHeader render nothing if not collapsed 1`] = `""`;
|
||||
|
||||
exports[`CollapsedHeader renders 1`] = `
|
||||
<Fragment>
|
||||
<header
|
||||
className="d-flex shadow-sm align-items-center learner-variant-header"
|
||||
>
|
||||
<IconButton
|
||||
alt="Close"
|
||||
className="p-4"
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction toggleIsOpen]}
|
||||
variant="primary"
|
||||
/>
|
||||
<mockConstructor />
|
||||
</header>
|
||||
<mockConstructor
|
||||
isOpen={false}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`CollapsedHeader renders with isOpen true 1`] = `
|
||||
<Fragment>
|
||||
<header
|
||||
className="d-flex shadow-sm align-items-center learner-variant-header"
|
||||
>
|
||||
<IconButton
|
||||
alt="Menu"
|
||||
className="p-4"
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction toggleIsOpen]}
|
||||
src={[MockFunction icons.Close]}
|
||||
variant="primary"
|
||||
/>
|
||||
<mockConstructor />
|
||||
</header>
|
||||
<mockConstructor
|
||||
isOpen={true}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
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 CollapseMenuBody from './CollapseMenuBody';
|
||||
import BrandLogo from '../BrandLogo';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
export const CollapsedHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderVariantData();
|
||||
|
||||
return (
|
||||
isCollapsed && (
|
||||
<>
|
||||
<header className="d-flex shadow-sm align-items-center learner-variant-header">
|
||||
<IconButton
|
||||
invertColors
|
||||
isActive
|
||||
src={isOpen ? Close : Menu}
|
||||
iconAs={Icon}
|
||||
alt={
|
||||
isOpen
|
||||
? formatMessage(messages.collapseMenuOpenAltText)
|
||||
: formatMessage(messages.collapseMenuClosedAltText)
|
||||
}
|
||||
onClick={toggleIsOpen}
|
||||
variant="primary"
|
||||
className="p-4"
|
||||
/>
|
||||
<BrandLogo />
|
||||
</header>
|
||||
<CollapseMenuBody isOpen={isOpen} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
CollapsedHeader.propTypes = {};
|
||||
|
||||
export default CollapsedHeader;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import CollapsedHeader from '.';
|
||||
|
||||
import { useLearnerDashboardHeaderVariantData, 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(() => ({
|
||||
isOpen: false,
|
||||
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('CollapsedHeader', () => {
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render nothing if not collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders with isOpen true', () => {
|
||||
useLearnerDashboardHeaderVariantData.mockReturnValueOnce({
|
||||
isOpen: true,
|
||||
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
|
||||
});
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
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 { reduxHooks } from 'hooks';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
export const AuthenticatedUserDropdown = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const dashboard = reduxHooks.useEnterpriseDashboardData();
|
||||
|
||||
return (
|
||||
authenticatedUser && (
|
||||
<Dropdown className="user-dropdown pr4">
|
||||
<Dropdown.Toggle
|
||||
as={AvatarButton}
|
||||
src={authenticatedUser.profileImage}
|
||||
id="user"
|
||||
variant="light"
|
||||
className="p-4"
|
||||
>
|
||||
<span data-hj-suppress className="d-md-inline">
|
||||
{authenticatedUser.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().LMS_BASE_URL}/u/${authenticatedUser.username}`}>
|
||||
{formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
{formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
AuthenticatedUserDropdown.propTypes = {};
|
||||
|
||||
export default AuthenticatedUserDropdown;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { AuthenticatedUserDropdown } from './AuthenticatedUserDropdown';
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
profileImage: 'profileImage',
|
||||
username: 'username',
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: jest.fn(),
|
||||
usePlatformSettingsData: jest.fn(() => ({
|
||||
courseSearchUrl: 'test-course-search-url',
|
||||
})),
|
||||
},
|
||||
}));
|
||||
jest.mock('../hooks', () => ({
|
||||
useIsCollapsed: jest.fn(),
|
||||
findCoursesNavDropdownClicked: (href) => jest.fn().mockName(`findCoursesNavDropdownClicked('${href}')`),
|
||||
}));
|
||||
|
||||
describe('AuthenticatedUserDropdown', () => {
|
||||
const defaultDashboardData = {
|
||||
label: 'label',
|
||||
url: 'url',
|
||||
};
|
||||
|
||||
describe('snapshots', () => {
|
||||
test('no auth render empty', () => {
|
||||
const { authenticatedUser } = AppContext;
|
||||
AppContext.authenticatedUser = null;
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
AppContext.authenticatedUser = authenticatedUser;
|
||||
});
|
||||
test('with enterprise dashboard', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(defaultDashboardData);
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('without enterprise dashboard and expanded', () => {
|
||||
reduxHooks.useEnterpriseDashboardData.mockReturnValueOnce(null);
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<AuthenticatedUserDropdown />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots no auth render empty 1`] = `""`;
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
|
||||
<Dropdown
|
||||
className="user-dropdown pr4"
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
className="p-4"
|
||||
id="user"
|
||||
src="profileImage"
|
||||
variant="light"
|
||||
>
|
||||
<span
|
||||
className="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://localhost:18000/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/account/settings"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/logout"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
`;
|
||||
|
||||
exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = `
|
||||
<Dropdown
|
||||
className="user-dropdown pr4"
|
||||
>
|
||||
<Dropdown.Toggle
|
||||
className="p-4"
|
||||
id="user"
|
||||
src="profileImage"
|
||||
variant="light"
|
||||
>
|
||||
<span
|
||||
className="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://localhost:18000/u/username"
|
||||
>
|
||||
Profile
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/account/settings"
|
||||
>
|
||||
Account
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
href="http://localhost:18000/logout"
|
||||
>
|
||||
Sign Out
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ExpandedHeader render 1`] = `
|
||||
<header
|
||||
className="d-flex shadow-sm align-items-center learner-variant-header pl-4"
|
||||
>
|
||||
<div
|
||||
className="flex-grow-1 d-flex align-items-center"
|
||||
>
|
||||
<BrandLogo />
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4 course-link"
|
||||
href="/"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Courses
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="programsUrl"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavClicked("courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
<Button
|
||||
as="a"
|
||||
className="p-4"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
<AuthenticatedUserDropdown />
|
||||
</header>
|
||||
`;
|
||||
|
||||
exports[`ExpandedHeader render empty if collapsed 1`] = `""`;
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { reduxHooks } from 'hooks';
|
||||
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
|
||||
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
|
||||
import messages from '../messages';
|
||||
import BrandLogo from '../BrandLogo';
|
||||
|
||||
export const ExpandedHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
|
||||
|
||||
return (
|
||||
!isCollapsed && (
|
||||
<header className="d-flex shadow-sm align-items-center learner-variant-header pl-4">
|
||||
<div className="flex-grow-1 d-flex align-items-center">
|
||||
<BrandLogo />
|
||||
|
||||
<Button
|
||||
as="a"
|
||||
href="/"
|
||||
variant="inverse-primary"
|
||||
className="p-4 course-link"
|
||||
>
|
||||
{formatMessage(messages.course)}
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={urls.programsUrl}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
>
|
||||
{formatMessage(messages.program)}
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href={courseSearchUrl}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
onClick={exploreCoursesClick}
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<span className="flex-grow-1" />
|
||||
<Button
|
||||
as="a"
|
||||
href={getConfig().SUPPORT_URL}
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
>
|
||||
{formatMessage(messages.help)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AuthenticatedUserDropdown />
|
||||
</header>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
ExpandedHeader.propTypes = {};
|
||||
|
||||
export default ExpandedHeader;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ExpandedHeader from '.';
|
||||
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
programsUrl: 'programsUrl',
|
||||
}));
|
||||
|
||||
jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: 'courseSearchUrl',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useIsCollapsed: jest.fn(),
|
||||
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
|
||||
}));
|
||||
|
||||
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
|
||||
jest.mock('../BrandLogo', () => 'BrandLogo');
|
||||
|
||||
describe('ExpandedHeader', () => {
|
||||
test('render', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
const wrapper = shallow(<ExpandedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render empty if collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
const wrapper = shallow(<ExpandedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BrandLogo dashboard defined 1`] = `
|
||||
<a
|
||||
className="mx-auto"
|
||||
href="url"
|
||||
>
|
||||
<img
|
||||
alt="edX, Inc. Dashboard"
|
||||
className="logo py-3"
|
||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
||||
/>
|
||||
</a>
|
||||
`;
|
||||
|
||||
exports[`BrandLogo dashboard undefined 1`] = `
|
||||
<a
|
||||
className="mx-auto"
|
||||
href="/"
|
||||
>
|
||||
<img
|
||||
alt="edX, Inc. Dashboard"
|
||||
className="logo py-3"
|
||||
src="https://edx-cdn.org/v3/prod/logo.svg"
|
||||
/>
|
||||
</a>
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LearnerDashboardHeaderVariant render 1`] = `
|
||||
<Fragment>
|
||||
<ConfirmEmailBanner />
|
||||
<CollapsedHeader />
|
||||
<ExpandedHeader />
|
||||
<MasqueradeBar />
|
||||
</Fragment>
|
||||
`;
|
||||
42
src/containers/LearnerDashboardHeaderVariant/hooks.js
Normal file
42
src/containers/LearnerDashboardHeaderVariant/hooks.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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,
|
||||
};
|
||||
69
src/containers/LearnerDashboardHeaderVariant/hooks.test.js
Normal file
69
src/containers/LearnerDashboardHeaderVariant/hooks.test.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
src/containers/LearnerDashboardHeaderVariant/index.jsx
Normal file
22
src/containers/LearnerDashboardHeaderVariant/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
38
src/containers/LearnerDashboardHeaderVariant/index.scss
Normal file
38
src/containers/LearnerDashboardHeaderVariant/index.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.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;
|
||||
}
|
||||
18
src/containers/LearnerDashboardHeaderVariant/index.test.jsx
Normal file
18
src/containers/LearnerDashboardHeaderVariant/index.test.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
71
src/containers/LearnerDashboardHeaderVariant/messages.js
Normal file
71
src/containers/LearnerDashboardHeaderVariant/messages.js
Normal file
@@ -0,0 +1,71 @@
|
||||
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;
|
||||
@@ -194,6 +194,7 @@ jest.mock('@edx/paragon/icons', () => ({
|
||||
ArrowBack: jest.fn().mockName('icons.ArrowBack'),
|
||||
ArrowDropDown: jest.fn().mockName('icons.ArrowDropDown'),
|
||||
ArrowDropUp: jest.fn().mockName('icons.ArrowDropUp'),
|
||||
Book: jest.fn().mockName('icons.Book'),
|
||||
Cancel: jest.fn().mockName('icons.Cancel'),
|
||||
Close: jest.fn().mockName('icons.Close'),
|
||||
CheckCircle: jest.fn().mockName('icons.CheckCircle'),
|
||||
@@ -208,6 +209,7 @@ jest.mock('@edx/paragon/icons', () => ({
|
||||
Tune: jest.fn().mockName('icons.Tune'),
|
||||
PersonSearch: jest.fn().mockName('icons.PersonSearch'),
|
||||
Program: jest.fn().mockName('icons.Program'),
|
||||
Search: jest.fn().mockName('icons.Search'),
|
||||
}));
|
||||
|
||||
jest.mock('data/constants/app', () => ({
|
||||
|
||||
@@ -48,6 +48,10 @@ export const LoadedView = ({
|
||||
);
|
||||
};
|
||||
|
||||
LoadedView.defaultProps = {
|
||||
isControl: true,
|
||||
};
|
||||
|
||||
LoadedView.propTypes = {
|
||||
courses: PropTypes.arrayOf(PropTypes.shape({
|
||||
courseKey: PropTypes.string,
|
||||
@@ -55,7 +59,7 @@ LoadedView.propTypes = {
|
||||
logoImageUrl: PropTypes.string,
|
||||
marketingUrl: PropTypes.string,
|
||||
})).isRequired,
|
||||
isControl: PropTypes.oneOf([true, false, null]).isRequired,
|
||||
isControl: PropTypes.oneOf([true, false, null]),
|
||||
};
|
||||
|
||||
export default LoadedView;
|
||||
|
||||
@@ -65,6 +65,7 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio
|
||||
<Button
|
||||
as="a"
|
||||
href="course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('course-search-url')]}
|
||||
variant="tertiary"
|
||||
>
|
||||
@@ -139,6 +140,7 @@ exports[`RecommendationsPanel LoadedView snapshot without personalize recommenda
|
||||
<Button
|
||||
as="a"
|
||||
href="course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('course-search-url')]}
|
||||
variant="tertiary"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user