feat: add explore course link on the header (#94)

This commit is contained in:
leangseu-edx
2022-12-14 10:55:02 -05:00
committed by GitHub
parent 3b32a5cd16
commit a3106928e1
20 changed files with 215 additions and 29 deletions

View File

@@ -9,7 +9,7 @@ import { AvatarButton, Dropdown } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import urls from 'data/services/lms/urls';
import { useIsCollapsed } from './hooks';
import { useIsCollapsed, findCoursesNavDropdownClicked } from './hooks';
import messages from './messages';
export const AuthenticatedUserDropdown = ({ username }) => {
@@ -17,6 +17,7 @@ export const AuthenticatedUserDropdown = ({ username }) => {
const { authenticatedUser } = React.useContext(AppContext);
const { profileImage } = authenticatedUser;
const dashboard = appHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = appHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed();
return (
@@ -48,9 +49,14 @@ export const AuthenticatedUserDropdown = ({ username }) => {
{formatMessage(messages.profile)}
</Dropdown.Item>
{isCollapsed && (
<Dropdown.Item href={urls.programsUrl}>
{formatMessage(messages.viewPrograms)}
</Dropdown.Item>
<>
<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().LMS_BASE_URL}/account/settings`}>
{formatMessage(messages.account)}

View File

@@ -14,10 +14,14 @@ jest.mock('@edx/frontend-platform/react', () => ({
jest.mock('data/redux', () => ({
hooks: {
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}')`),
}));
describe('AuthenticatedUserDropdown', () => {

View File

@@ -50,6 +50,12 @@ exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = `
>
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://localhost:18000/account/settings"
>

View File

@@ -28,8 +28,16 @@ exports[`LearnerDashboardHeader snapshots with collapsed 1`] = `
/>
</div>
<div
className="my-auto ml-1"
className="my-auto ml-1 d-flex"
>
<IconButton
as="a"
href="test-course-search-url"
iconAs="Icon"
invertColors={true}
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
variant="primary"
/>
<UserMenu />
</div>
</div>
@@ -66,6 +74,14 @@ exports[`LearnerDashboardHeader snapshots without collapsed 1`] = `
<div
className="flex-grow-1"
/>
<Button
as="a"
href="test-course-search-url"
onClick={[MockFunction findCoursesNavClicked('test-course-search-url')]}
variant="inverse-tertiary"
>
Explore courses
</Button>
<UserMenu />
</div>
</header>

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { useWindowSize, breakpoints } from '@edx/paragon';
import track from 'tracking';
import { linkNames } from 'tracking/constants';
export const useIsCollapsed = () => {
const { width } = useWindowSize();
@@ -7,4 +9,16 @@ export const useIsCollapsed = () => {
return isCollapsed;
};
export default { useIsCollapsed };
export const findCoursesNavClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.learnerHomeNavExplore,
});
export const findCoursesNavDropdownClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.learnerHomeNavDropdownExplore,
});
export default {
useIsCollapsed,
findCoursesNavClicked,
findCoursesNavDropdownClicked,
};

View File

@@ -1,5 +1,15 @@
import { useWindowSize, breakpoints } from '@edx/paragon';
import { useIsCollapsed } from './hooks';
import track from 'tracking';
import { linkNames } from 'tracking/constants';
import { useIsCollapsed, findCoursesNavClicked, findCoursesNavDropdownClicked } from './hooks';
jest.mock('tracking', () => ({
findCourses: {
findCoursesClicked: jest.fn(),
},
}));
const url = 'http://example.com';
describe('LearnerDashboardHeader hooks', () => {
describe('useIsCollapsed', () => {
@@ -12,4 +22,22 @@ describe('LearnerDashboardHeader hooks', () => {
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,
});
});
});
});

View File

@@ -2,18 +2,21 @@ import React, { useContext } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { Program } from '@edx/paragon/icons';
import { Button, Image } from '@edx/paragon';
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 { hooks as appHooks } from 'data/redux';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import GreetingBanner from './GreetingBanner';
import ConfirmEmailBanner from './ConfirmEmailBanner';
import { useIsCollapsed } from './hooks';
import { useIsCollapsed, findCoursesNavClicked } from './hooks';
import messages from './messages';
import './index.scss';
@@ -25,6 +28,9 @@ export const UserMenu = () => {
export const LearnerDashboardHeader = () => {
const { formatMessage } = useIntl();
const isCollapsed = useIsCollapsed();
const { courseSearchUrl } = appHooks.usePlatformSettingsData();
const exploreCoursesClick = findCoursesNavClicked(courseSearchUrl);
return (
<>
@@ -43,9 +49,33 @@ export const LearnerDashboardHeader = () => {
<div className="flex-grow-1">
{isCollapsed && <GreetingBanner size="small" />}
</div>
{isCollapsed
? (<div className="my-auto ml-1"><UserMenu /></div>)
: (<UserMenu />)}
{isCollapsed ? (
<div className="my-auto ml-1 d-flex">
<IconButton
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" />}

View File

@@ -14,6 +14,14 @@ jest.mock('@edx/frontend-platform/react', () => ({
}));
jest.mock('./hooks', () => ({
useIsCollapsed: jest.fn(),
findCoursesNavClicked: (href) => jest.fn().mockName(`findCoursesNavClicked('${href}')`),
}));
jest.mock('data/redux', () => ({
hooks: {
usePlatformSettingsData: jest.fn(() => ({
courseSearchUrl: 'test-course-search-url',
})),
},
}));
jest.mock('containers/MasqueradeBar', () => 'MasqueradeBar');

View File

@@ -57,6 +57,11 @@ const messages = defineMessages({
defaultMessage: 'Switch to Programs',
description: 'Header link for switching to program page.',
},
exploreCourses: {
id: 'leanerDashboard.exploreCourses',
defaultMessage: 'Explore courses',
description: 'Header link for switching to course page.',
},
});
export default messages;

View File

@@ -4,6 +4,7 @@ export const categories = StrictDict({
dashboard: 'dashboard',
upgrade: 'upgrade',
userEngagement: 'user-engagement',
searchButton: 'search_button',
});
export const events = StrictDict({
@@ -46,6 +47,12 @@ export const eventNames = StrictDict({
enterpriseDashboardModalOpened: `${learnerPortal}.opened`,
enterpriseDashboardModalCTAClicked: `${learnerPortal}.dashboard_cta.clicked`,
enterpriseDashboardModalClosed: `${learnerPortal}.closed`,
findCoursesClicked: 'edx.bi.dashboard.find_courses_button.clicked',
});
export const linkNames = StrictDict({
learnerHomeNavExplore: 'learner_home_nav_explore',
learnerHomeNavDropdownExplore: 'learner_home_nav_dropdown_explore',
});
export const appName = 'learner-home';

View File

@@ -3,6 +3,7 @@ import engagement from './trackers/engagement';
import enterpriseDashboard from './trackers/enterpriseDashboard';
import entitlements from './trackers/entitlements';
import socialShare from './trackers/socialShare';
import findCourses from './trackers/findCourses';
export default {
course,
@@ -10,4 +11,5 @@ export default {
enterpriseDashboard,
entitlements,
socialShare,
findCourses,
};

View File

@@ -0,0 +1,16 @@
import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
import { categories, eventNames } from '../constants';
export const findCoursesClicked = (href, args = {}) => createLinkTracker(
createEventTracker(eventNames.findCoursesClicked, {
pageName: 'learner_home',
linkType: 'button',
linkCategory: categories.searchButton,
...args,
}),
href,
);
export default {
findCoursesClicked,
};

View File

@@ -0,0 +1,44 @@
import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
import { findCoursesClicked } from './findCourses';
import { categories, eventNames } from '../constants';
jest.mock('data/services/segment/utils', () => ({
createEventTracker: jest.fn((args) => ({ createEventTracker: args })),
createLinkTracker: jest.fn((args) => ({ createLinkTracker: args })),
}));
describe('find courses trackers', () => {
const url = 'http://example.com';
const defaultProps = {
pageName: 'learner_home',
linkType: 'button',
linkCategory: categories.searchButton,
};
beforeEach(() => {
createEventTracker.mockClear();
createLinkTracker.mockClear();
});
test('no args', () => {
findCoursesClicked(url);
expect(createEventTracker).toHaveBeenCalledWith(eventNames.findCoursesClicked, defaultProps);
expect(createLinkTracker).toHaveBeenCalledWith(
createEventTracker(eventNames.findCoursesClicked, defaultProps), url,
);
});
test('with args', () => {
const args = { linkName: 'foo' };
findCoursesClicked(url, args);
expect(createEventTracker).toHaveBeenCalledWith(eventNames.findCoursesClicked, {
...defaultProps,
...args,
});
expect(createLinkTracker).toHaveBeenCalledWith(
createEventTracker(eventNames.findCoursesClicked, {
...defaultProps,
...args,
}),
url,
);
});
});

View File

@@ -19,6 +19,7 @@ exports[`LookingForChallengeWidget snapshots default 1`] = `
<Hyperlink
className="d-flex align-items-center"
destination="course-search-url"
onClick={[MockFunction track.findCoursesWidgetClicked('course-search-url')]}
variant="brand"
>
<format-message-function

View File

@@ -30,7 +30,7 @@ export const LookingForChallengeWidget = () => {
<Hyperlink
variant="brand"
destination={courseSearchUrl}
onClick={track.findCoursesClicked(courseSearchUrl)}
onClick={track.findCoursesWidgetClicked(courseSearchUrl)}
className="d-flex align-items-center"
>
{formatMessage(messages.findCoursesButton, { arrow: arrowIcon })}

View File

@@ -11,7 +11,7 @@ jest.mock('data/redux', () => ({
}));
jest.mock('../RecommendationsPanel/track', () => ({
findCoursesClicked: jest.fn().mockName('track.findCoursesClicked'),
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
}));
describe('LookingForChallengeWidget', () => {

View File

@@ -39,7 +39,7 @@ export const LoadedView = ({
iconBefore={Search}
as="a"
href={courseSearchUrl}
onClick={track.findCoursesClicked(courseSearchUrl)}
onClick={track.findCoursesWidgetClicked(courseSearchUrl)}
>
{formatMessage(messages.exploreCoursesButton)}
</Button>

View File

@@ -14,7 +14,7 @@ jest.mock('data/redux', () => ({
},
}));
jest.mock('./track', () => ({
findCoursesClicked: () => 'find-courses-clicked',
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
}));
describe('RecommendationsPanel LoadedView', () => {

View File

@@ -65,7 +65,7 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio
<Button
as="a"
href="course-search-url"
onClick="find-courses-clicked"
onClick={[MockFunction track.findCoursesWidgetClicked('course-search-url')]}
variant="tertiary"
>
Explore courses
@@ -139,7 +139,7 @@ exports[`RecommendationsPanel LoadedView snapshot without personalize recommenda
<Button
as="a"
href="course-search-url"
onClick="find-courses-clicked"
onClick={[MockFunction track.findCoursesWidgetClicked('course-search-url')]}
variant="tertiary"
>
Explore courses

View File

@@ -1,19 +1,18 @@
import { StrictDict } from 'utils';
import { createLinkTracker, createEventTracker } from 'data/services/segment/utils';
import track from 'tracking';
export const eventNames = StrictDict({
findCoursesClicked: 'edx.bi.dashboard.find_courses_button.clicked',
recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
});
export const findCoursesClicked = (href) => createLinkTracker(
createEventTracker(eventNames.findCoursesClicked, {
pageName: 'learner_home',
linkType: 'button',
linkCategory: 'search_button',
}),
href,
);
export const linkNames = StrictDict({
findCoursesWidget: 'learner_home_widget_explore',
});
export const findCoursesWidgetClicked = (href) => track.findCourses.findCoursesClicked(href, {
linkName: linkNames.findCoursesWidget,
});
export const recommendedCourseClicked = (courseKey, isPersonalized, href) => createLinkTracker(
createEventTracker(eventNames.recommendedCourseClicked, {
@@ -24,6 +23,6 @@ export const recommendedCourseClicked = (courseKey, isPersonalized, href) => cre
);
export default {
findCoursesClicked,
findCoursesWidgetClicked,
recommendedCourseClicked,
};