Compare commits

...

9 Commits

Author SHA1 Message Date
Blue
a20ec2c3e6 Ahtesham/VAN-1615/modal fixes (#194) 2023-08-22 18:19:19 +05:00
ahtesham-quraish
362d2406d3 fix: modal outside click fix
Description:
Modal window should not closed when click outside the modal
VAN-1615
2023-08-22 13:41:49 +05:00
Syed Sajjad Hussain Shah
736abb4247 feat: add experiment and eventing to recommendations painted door exp (#191) 2023-08-18 18:31:21 +05:00
Syed Sajjad Hussain Shah
05bdf4ee02 feat: add experiment and eventing to recommendations painted door exp
VAN-1604
2023-08-18 18:00:23 +05:00
Blue
30fadadb26 Ahtesham/van 1601/stack buttons (#190)
Stacked the button for modal window for mobile screen
2023-08-17 12:52:42 +05:00
ahtesham-quraish
825cca2d8d fix: buttons are stacked in modal window
Description:
We have stacked the buttons those come in modal window
VAN-1601
2023-08-17 12:41:28 +05:00
Blue
3bad4897ba feat: add recommendations modal (#189) 2023-08-16 21:23:44 +05:00
Blue
743c233f6d Merge branch 'master' into ahtesham/VAN-1601/design 2023-08-16 21:13:25 +05:00
ahtesham-quraish
6cecc38e85 feat: add recommendations modal
Description:
Add recommendations buttons and modal for painted door experiment
VAN-1601
2023-08-16 12:52:54 +05:00
30 changed files with 846 additions and 230 deletions

View File

@@ -5,6 +5,15 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" /> <link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
<% if (process.env.OPTIMIZELY_URL) { %>
<script
src="<%= process.env.OPTIMIZELY_URL %>"
></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -29,6 +29,7 @@ import LearnerDashboardHeader from './containers/LearnerDashboardHeader';
import messages from './messages'; import messages from './messages';
import './App.scss'; import './App.scss';
import { PaintedDoorExperimentProvider } from './RecsPaintedDoorExpContext';
export const App = () => { export const App = () => {
const { authenticatedUser } = React.useContext(AppContext); const { authenticatedUser } = React.useContext(AppContext);
@@ -78,6 +79,7 @@ export const App = () => {
<title>{formatMessage(messages.pageTitle)}</title> <title>{formatMessage(messages.pageTitle)}</title>
</Helmet> </Helmet>
<div> <div>
<PaintedDoorExperimentProvider>
<LearnerDashboardHeader /> <LearnerDashboardHeader />
<main> <main>
{hasNetworkFailure {hasNetworkFailure
@@ -93,6 +95,7 @@ export const App = () => {
</main> </main>
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} /> <Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
<ZendeskFab /> <ZendeskFab />
</PaintedDoorExperimentProvider>
</div> </div>
</Router> </Router>
); );

View File

@@ -25,6 +25,9 @@ jest.mock('components/ZendeskFab', () => 'ZendeskFab');
jest.mock('ExperimentContext', () => ({ jest.mock('ExperimentContext', () => ({
ExperimentProvider: 'ExperimentProvider', ExperimentProvider: 'ExperimentProvider',
})); }));
jest.mock('RecsPaintedDoorExpContext', () => ({
PaintedDoorExperimentProvider: 'PaintedDoorExperimentProvider',
}));
jest.mock('data/redux', () => ({ jest.mock('data/redux', () => ({
selectors: 'redux.selectors', selectors: 'redux.selectors',
actions: 'redux.actions', actions: 'redux.actions',

View File

@@ -0,0 +1,120 @@
import React from 'react';
import PropTypes from 'prop-types';
import { StrictDict } from 'utils';
import * as module from './RecsPaintedDoorExpContext';
import {
useEmailConfirmationData,
useHasAvailableDashboards,
useRequestIsPending,
} from './data/redux/hooks';
import { RequestKeys } from './data/constants/requests';
import { trackPaintedDoorVariationGroup } from './widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
export const state = StrictDict({
enterpriseUser: (val) => React.useState(val), // eslint-disable-line
experimentData: (val) => React.useState(val), // eslint-disable-line
});
const PAINTED_DOOR_RECOMMENDATIONS_EXP_ID = 25116810832;
const PAINTED_DOOR_RECOMMENDATIONS_PAGE = 'url_targeting_for_van1604_recommendations_painted_door_exp';
const PAINTED_DOOR_RECS_EXP_NAVBAR_BTN_VARIATION = 'btn_navbar';
const PAINTED_DOOR_RECS_EXP_WIDGET_BTN_VARIATION = 'btn_widget';
export function getPaintedDoorRecommendationsExperimentVariation() {
try {
if (window.optimizely && window.optimizely.get('data').experiments[PAINTED_DOOR_RECOMMENDATIONS_EXP_ID]) {
const selectedVariant = window.optimizely.get('state').getVariationMap()[PAINTED_DOOR_RECOMMENDATIONS_EXP_ID];
return selectedVariant?.name;
}
} catch (e) { /* empty */ }
return '';
}
export function activatePaintedDoorRecommendationsExperiment() {
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'page',
pageName: PAINTED_DOOR_RECOMMENDATIONS_PAGE,
});
}
const useIsEnterpriseUser = () => {
const [enterpriseUser, setEnterpriseUser] = module.state.enterpriseUser({
isEnterpriseUser: false,
isLoading: true,
});
const initIsPending = useRequestIsPending(RequestKeys.initialize);
const hasAvailableDashboards = useHasAvailableDashboards();
const confirmationData = useEmailConfirmationData();
React.useEffect(() => {
if (!initIsPending && Object.keys(confirmationData).length && hasAvailableDashboards) {
setEnterpriseUser(prev => ({
...prev,
isEnterpriseUser: true,
isLoading: false,
}));
} else if (!initIsPending && Object.keys(confirmationData).length && !hasAvailableDashboards) {
setEnterpriseUser(prev => ({
...prev,
isEnterpriseUser: false,
isLoading: false,
}));
}
}, [initIsPending]); // eslint-disable-line react-hooks/exhaustive-deps
return enterpriseUser;
};
export const PaintedDoorExperimentContext = React.createContext();
export const PaintedDoorExperimentProvider = ({ children }) => {
const [experimentData, setExperimentData] = module.state.experimentData({
experimentVariation: '',
isPaintedDoorNavbarBtnVariation: false,
isPaintedDoorWidgetBtnVariation: false,
experimentLoading: true,
});
const enterpriseUser = useIsEnterpriseUser();
const contextValue = React.useMemo(
() => ({
...experimentData,
}),
[experimentData],
);
React.useEffect(() => {
let timer = null;
if (!enterpriseUser.isLoading && !enterpriseUser.isEnterpriseUser) {
activatePaintedDoorRecommendationsExperiment();
timer = setTimeout(() => {
const variation = getPaintedDoorRecommendationsExperimentVariation();
setExperimentData(prev => ({
...prev,
experimentVariation: variation,
isPaintedDoorNavbarBtnVariation: variation === PAINTED_DOOR_RECS_EXP_NAVBAR_BTN_VARIATION,
isPaintedDoorWidgetBtnVariation: variation === PAINTED_DOOR_RECS_EXP_WIDGET_BTN_VARIATION,
experimentLoading: false,
}));
trackPaintedDoorVariationGroup(variation);
}, 500);
}
return () => clearTimeout(timer);
}, [enterpriseUser]); // eslint-disable-line react-hooks/exhaustive-deps
return (
<PaintedDoorExperimentContext.Provider value={contextValue}>
{children}
</PaintedDoorExperimentContext.Provider>
);
};
export const usePaintedDoorExperimentContext = () => React.useContext(PaintedDoorExperimentContext);
PaintedDoorExperimentProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export default usePaintedDoorExperimentContext;

View File

@@ -11,6 +11,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
</title> </title>
</HelmetWrapper> </HelmetWrapper>
<div> <div>
<PaintedDoorExperimentProvider>
<LearnerDashboardHeader /> <LearnerDashboardHeader />
<main> <main>
<Alert <Alert
@@ -25,6 +26,7 @@ exports[`App router component component initialize failure snapshot 1`] = `
logo="fakeLogo.png" logo="fakeLogo.png"
/> />
<ZendeskFab /> <ZendeskFab />
</PaintedDoorExperimentProvider>
</div> </div>
</BrowserRouter> </BrowserRouter>
`; `;
@@ -40,6 +42,7 @@ exports[`App router component component no network failure snapshot 1`] = `
</title> </title>
</HelmetWrapper> </HelmetWrapper>
<div> <div>
<PaintedDoorExperimentProvider>
<LearnerDashboardHeader /> <LearnerDashboardHeader />
<main> <main>
<ExperimentProvider> <ExperimentProvider>
@@ -50,6 +53,7 @@ exports[`App router component component no network failure snapshot 1`] = `
logo="fakeLogo.png" logo="fakeLogo.png"
/> />
<ZendeskFab /> <ZendeskFab />
</PaintedDoorExperimentProvider>
</div> </div>
</BrowserRouter> </BrowserRouter>
`; `;
@@ -65,6 +69,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
</title> </title>
</HelmetWrapper> </HelmetWrapper>
<div> <div>
<PaintedDoorExperimentProvider>
<LearnerDashboardHeader /> <LearnerDashboardHeader />
<main> <main>
<Alert <Alert
@@ -79,6 +84,7 @@ exports[`App router component component refresh failure snapshot 1`] = `
logo="fakeLogo.png" logo="fakeLogo.png"
/> />
<ZendeskFab /> <ZendeskFab />
</PaintedDoorExperimentProvider>
</div> </div>
</BrowserRouter> </BrowserRouter>
`; `;

View File

@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalView snapshot should renders default ModalView 1`] = `
<div
className="containers"
>
<ModalDialog
hasCloseButton={false}
isBlocking={true}
isFullscreenScroll={true}
isOpen={true}
onClose={[MockFunction]}
title=""
>
<ModalDialog.Header>
<Component>
<h3
className="mt-2"
>
Thank you for your interest!
</h3>
</Component>
</ModalDialog.Header>
<ModalDialog.Body>
<div>
<p
className="mt-2"
>
Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.
</p>
<p>
Would you like to be alerted when it becomes available?
</p>
</div>
</ModalDialog.Body>
<Component>
<ActionRow>
<Component
onClick={[Function]}
variant="tertiary"
>
Skip for now
</Component>
<Component
onClick={[Function]}
variant="primary"
>
Count me in!
</Component>
</ActionRow>
</Component>
</ModalDialog>
</div>
`;

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { StrictDict } from 'utils';
import * as module from './hooks';
export const state = StrictDict({
isRecommendationsModalOpen: (val) => React.useState(val), // eslint-disable-line
});
export const useRecommendationsModal = () => {
const [isRecommendationsModalOpen, setIsRecommendationsModalOpen] = module.state.isRecommendationsModalOpen(false);
const toggleRecommendationsModal = () => setIsRecommendationsModalOpen(!isRecommendationsModalOpen);
return {
isRecommendationsModalOpen,
toggleRecommendationsModal,
};
};

View File

@@ -0,0 +1,21 @@
import { MockUseState } from 'testUtils';
import * as hooks from './hooks';
const state = new MockUseState(hooks);
const {
useRecommendationsModal,
} = hooks;
describe('LearnerDashboardHeader hooks', () => {
describe('useRecommendationsModal', () => {
test('default state', () => {
state.mock();
const out = useRecommendationsModal();
state.expectInitializedWith(state.keys.isRecommendationsModalOpen, false);
out.toggleRecommendationsModal();
expect(state.values.isRecommendationsModalOpen).toEqual(true);
});
});
});

View File

@@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ModalDialog, ActionRow } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import './index.scss';
import {
trackPaintedDoorRecommendationHomeInterestBtnClicked,
trackPaintedDoorRecommendationHomeSkipBtnClicked,
} from '../../widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
export const ModalView = ({
isOpen,
onClose,
variation,
}) => {
const { formatMessage } = useIntl();
const handleSkipBtnClick = () => trackPaintedDoorRecommendationHomeSkipBtnClicked(variation);
const handleInterestBtnClick = () => trackPaintedDoorRecommendationHomeInterestBtnClicked(variation);
return (
<div className="containers">
<ModalDialog
title=""
isOpen={isOpen}
onClose={onClose}
hasCloseButton={false}
isFullscreenScroll
isBlocking
>
<ModalDialog.Header>
<ModalDialog.Title>
<h3 className="mt-2">{formatMessage(messages.recommendationsModalHeading)}</h3>
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<div>
<p className="mt-2">{formatMessage(messages.recommendationsFeatureText)}</p>
<p>{formatMessage(messages.recommendationsAlertText)}</p>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary" onClick={handleSkipBtnClick}>
{formatMessage(messages.modalSkipButton)}
</ModalDialog.CloseButton>
<ModalDialog.CloseButton variant="primary" onClick={handleInterestBtnClick}>
{formatMessage(messages.modalCountMeButton)}
</ModalDialog.CloseButton>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
</div>
);
};
ModalView.defaultProps = {
isOpen: false,
};
ModalView.propTypes = {
onClose: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
variation: PropTypes.string.isRequired,
};
export default ModalView;

View File

@@ -0,0 +1,13 @@
@import "@edx/paragon/scss/core/core";
@media (max-width: 464px) {
.pgn__action-row{
flex-direction: column;
gap: 5px;
button {
width: 100%;
}
}
}

View File

@@ -0,0 +1,16 @@
import { shallow } from 'enzyme';
import ModalView from '.';
describe('ModalView', () => {
const props = {
isOpen: true,
onClose: jest.fn(),
};
describe('snapshot', () => {
test('should renders default ModalView', () => {
const wrapper = shallow(<ModalView {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,31 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
recommendationsFeatureText: {
id: 'RecommendationsPanel.recommendationsFeatureText',
defaultMessage: 'Personalized recommendations feature is not yet available. We are working hard on bringing it to your learner home in the near future.',
description: 'recommendations feature text',
},
recommendationsAlertText: {
id: 'RecommendationsPanel.recommendationsAlertText',
defaultMessage: 'Would you like to be alerted when it becomes available?',
description: 'recommendations alerted text',
},
recommendationsModalHeading: {
id: 'RecommendationsPanel.recommendationsModalHeading',
defaultMessage: 'Thank you for your interest!',
description: 'Heading of modal',
},
modalSkipButton: {
id: 'RecommendationsPanel.modalSkipButton',
defaultMessage: 'Skip for now',
description: 'button for Skip for now',
},
modalCountMeButton: {
id: 'RecommendationsPanel.modalCountMeButton',
defaultMessage: 'Count me in!',
description: 'button for Count me in!',
},
});
export default messages;

View File

@@ -12,7 +12,13 @@ import { reduxHooks } from 'hooks';
import { findCoursesNavDropdownClicked } from '../hooks'; import { findCoursesNavDropdownClicked } from '../hooks';
import ModalView from '../../../components/ModalView';
import messages from '../messages'; import messages from '../messages';
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
import usePaintedDoorExperimentContext from '../../../RecsPaintedDoorExpContext';
import {
trackPaintedDoorRecommendationHomeBtnClicked,
} from '../../../widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
export const CollapseMenuBody = ({ isOpen }) => { export const CollapseMenuBody = ({ isOpen }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -20,8 +26,19 @@ export const CollapseMenuBody = ({ isOpen }) => {
const dashboard = reduxHooks.useEnterpriseDashboardData(); const dashboard = reduxHooks.useEnterpriseDashboardData();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData(); const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
const exploreCoursesClick = findCoursesNavDropdownClicked(urls.baseAppUrl(courseSearchUrl)); const exploreCoursesClick = findCoursesNavDropdownClicked(urls.baseAppUrl(courseSearchUrl));
const {
experimentVariation,
isPaintedDoorNavbarBtnVariation,
experimentLoading,
} = usePaintedDoorExperimentContext();
const handleSeeAllRecommendationsClick = () => {
toggleRecommendationsModal();
trackPaintedDoorRecommendationHomeBtnClicked(experimentVariation);
};
return ( return (
isOpen && ( isOpen && (
@@ -40,6 +57,14 @@ export const CollapseMenuBody = ({ isOpen }) => {
> >
{formatMessage(messages.discoverNew)} {formatMessage(messages.discoverNew)}
</Button> </Button>
{(!experimentLoading && isPaintedDoorNavbarBtnVariation) && (
<Button
variant="inverse-primary"
onClick={handleSeeAllRecommendationsClick}
>
{formatMessage(messages.recommendedForYou)}
</Button>
)}
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary"> <Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
{formatMessage(messages.help)} {formatMessage(messages.help)}
</Button> </Button>
@@ -92,6 +117,11 @@ export const CollapseMenuBody = ({ isOpen }) => {
</Button> </Button>
</> </>
)} )}
<ModalView
isOpen={isRecommendationsModalOpen}
onClose={toggleRecommendationsModal}
variation={experimentVariation}
/>
</div> </div>
) )
); );

View File

@@ -3,6 +3,13 @@ import { AppContext } from '@edx/frontend-platform/react';
import CollapseMenuBody from './CollapseMenuBody'; import CollapseMenuBody from './CollapseMenuBody';
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(() => ({
ORDER_HISTORY_URL: 'http://account-profile-url.test',
CAREER_LINK_URL: 'http://account-profile-url.test',
})),
}));
jest.mock('@edx/frontend-platform/react', () => ({ jest.mock('@edx/frontend-platform/react', () => ({
AppContext: { AppContext: {
authenticatedUser: { authenticatedUser: {
@@ -15,6 +22,7 @@ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
useEnterpriseDashboardData: () => ({ useEnterpriseDashboardData: () => ({
url: 'url', url: 'url',
dashboard: false,
}), }),
usePlatformSettingsData: () => ({ usePlatformSettingsData: () => ({
courseSearchUrl: '/courseSearchUrl', courseSearchUrl: '/courseSearchUrl',
@@ -22,6 +30,13 @@ jest.mock('hooks', () => ({
}, },
})); }));
jest.mock('../../../components/ModalView/hooks', () => ({
useRecommendationsModal: jest.fn(() => ({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
})),
}));
jest.mock('../hooks', () => ({ jest.mock('../hooks', () => ({
findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`), findCoursesNavDropdownClicked: (url) => jest.fn().mockName(`findCoursesNavDropdownClicked("${url}")`),
})); }));

View File

@@ -13,22 +13,21 @@ exports[`CollapseMenuBody render 1`] = `
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/dashboard/programs" href="undefined/dashboard/programs"
variant="inverse-primary" variant="inverse-primary"
> >
Programs Programs
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/courseSearchUrl" href="undefined/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]} onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
variant="inverse-primary" variant="inverse-primary"
> >
Discover New Discover New
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/support"
variant="inverse-primary" variant="inverse-primary"
> >
Help Help
@@ -42,25 +41,35 @@ exports[`CollapseMenuBody render 1`] = `
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/u/username" href="undefined/u/username"
variant="inverse-primary" variant="inverse-primary"
> >
Profile Profile
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/account/settings" href="undefined/account/settings"
variant="inverse-primary" variant="inverse-primary"
> >
Account Account
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/logout" href="http://account-profile-url.test"
variant="inverse-primary"
>
Orders & Subscriptions
</Button>
<Button
as="a"
variant="inverse-primary" variant="inverse-primary"
> >
Sign Out Sign Out
</Button> </Button>
<ModalView
isOpen={false}
onClose={[MockFunction]}
/>
</div> </div>
`; `;
@@ -79,25 +88,28 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/dashboard/programs" href="undefined/dashboard/programs"
variant="inverse-primary" variant="inverse-primary"
> >
Programs Programs
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/courseSearchUrl" href="undefined/courseSearchUrl"
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]} onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
variant="inverse-primary" variant="inverse-primary"
> >
Discover New Discover New
</Button> </Button>
<Button <Button
as="a" as="a"
href="http://localhost:18000/support"
variant="inverse-primary" variant="inverse-primary"
> >
Help Help
</Button> </Button>
<ModalView
isOpen={false}
onClose={[MockFunction]}
/>
</div> </div>
`; `;

View File

@@ -36,7 +36,9 @@ export const CollapsedHeader = () => {
/> />
<BrandLogo /> <BrandLogo />
</header> </header>
<CollapseMenuBody isOpen={isOpen} /> <CollapseMenuBody
isOpen={isOpen}
/>
</> </>
) )
); );

View File

@@ -46,6 +46,10 @@ exports[`ExpandedHeader render 1`] = `
</Button> </Button>
</div> </div>
<AuthenticatedUserDropdown /> <AuthenticatedUserDropdown />
<ModalView
isOpen={false}
onClose={[MockFunction]}
/>
</header> </header>
`; `;

View File

@@ -10,15 +10,32 @@ import { reduxHooks } from 'hooks';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown'; import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import { useIsCollapsed, findCoursesNavClicked } from '../hooks'; import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
import messages from '../messages'; import messages from '../messages';
import ModalView from '../../../components/ModalView';
import BrandLogo from '../BrandLogo'; import BrandLogo from '../BrandLogo';
import usePaintedDoorExperimentContext from '../../../RecsPaintedDoorExpContext';
import {
trackPaintedDoorRecommendationHomeBtnClicked,
} from '../../../widgets/RecommendationsPanel/recsPaintedDoorExpTrack';
export const ExpandedHeader = () => { export const ExpandedHeader = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData(); const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const isCollapsed = useIsCollapsed(); const isCollapsed = useIsCollapsed();
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl)); const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
const {
experimentVariation,
isPaintedDoorNavbarBtnVariation,
experimentLoading,
} = usePaintedDoorExperimentContext();
const handleSeeAllRecommendationsClick = () => {
toggleRecommendationsModal();
trackPaintedDoorRecommendationHomeBtnClicked(experimentVariation);
};
return ( return (
!isCollapsed && ( !isCollapsed && (
@@ -51,6 +68,15 @@ export const ExpandedHeader = () => {
> >
{formatMessage(messages.discoverNew)} {formatMessage(messages.discoverNew)}
</Button> </Button>
{(!experimentLoading && isPaintedDoorNavbarBtnVariation) && (
<Button
variant="inverse-primary"
className="p-4"
onClick={handleSeeAllRecommendationsClick}
>
{formatMessage(messages.recommendedForYou)}
</Button>
)}
<span className="flex-grow-1" /> <span className="flex-grow-1" />
<Button <Button
as="a" as="a"
@@ -63,6 +89,11 @@ export const ExpandedHeader = () => {
</div> </div>
<AuthenticatedUserDropdown /> <AuthenticatedUserDropdown />
<ModalView
isOpen={isRecommendationsModalOpen}
onClose={toggleRecommendationsModal}
variation={experimentVariation}
/>
</header> </header>
) )
); );

View File

@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
import ExpandedHeader from '.'; import ExpandedHeader from '.';
import { useIsCollapsed } from '../hooks'; import { useIsCollapsed } from '../hooks';
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
jest.mock('data/services/lms/urls', () => ({ jest.mock('data/services/lms/urls', () => ({
programsUrl: 'programsUrl', programsUrl: 'programsUrl',
@@ -19,21 +20,34 @@ jest.mock('hooks', () => ({
jest.mock('../hooks', () => ({ jest.mock('../hooks', () => ({
useIsCollapsed: jest.fn(), useIsCollapsed: jest.fn(),
useRecommendationsModal: jest.fn(),
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`), findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
})); }));
jest.mock('../../../components/ModalView/hooks', () => ({
useRecommendationsModal: jest.fn(),
}));
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown'); jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('../BrandLogo', () => 'BrandLogo'); jest.mock('../BrandLogo', () => 'BrandLogo');
describe('ExpandedHeader', () => { describe('ExpandedHeader', () => {
test('render', () => { test('render', () => {
useIsCollapsed.mockReturnValueOnce(false); useIsCollapsed.mockReturnValueOnce(false);
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
const wrapper = shallow(<ExpandedHeader />); const wrapper = shallow(<ExpandedHeader />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
test('render empty if collapsed', () => { test('render empty if collapsed', () => {
useIsCollapsed.mockReturnValueOnce(true); useIsCollapsed.mockReturnValueOnce(true);
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
const wrapper = shallow(<ExpandedHeader />); const wrapper = shallow(<ExpandedHeader />);
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
expect(wrapper.isEmptyRender()).toBe(true); expect(wrapper.isEmptyRender()).toBe(true);

View File

@@ -51,6 +51,11 @@ const messages = defineMessages({
defaultMessage: 'Discover New', defaultMessage: 'Discover New',
description: 'Header link for switching to discover page.', description: 'Header link for switching to discover page.',
}, },
recommendedForYou: {
id: 'learnerVariantDashboard.recommendedForYou',
defaultMessage: 'Recommended for you',
description: 'Header link for recommended page.',
},
logoAltText: { logoAltText: {
id: 'learnerVariantDashboard.logoAltText', id: 'learnerVariantDashboard.logoAltText',
defaultMessage: 'edX, Inc. Dashboard', defaultMessage: 'edX, Inc. Dashboard',

View File

@@ -8,7 +8,7 @@ import { reduxHooks } from 'hooks';
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg'; import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
import { baseAppUrl } from 'data/services/lms/urls'; import { baseAppUrl } from 'data/services/lms/urls';
import track from '../RecommendationsPanel/track'; import track, { trackRecommendationUnavailable } from '../RecommendationsPanel/track';
import messages from './messages'; import messages from './messages';
import './index.scss'; import './index.scss';
@@ -17,6 +17,11 @@ export const arrowIcon = (<Icon className="mx-1" src={ArrowForward} />);
export const LookingForChallengeWidget = () => { export const LookingForChallengeWidget = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData(); const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
React.useEffect(() => {
trackRecommendationUnavailable();
}, []);
return ( return (
<Card orientation="horizontal" id="looking-for-challenge-widget"> <Card orientation="horizontal" id="looking-for-challenge-widget">
<Card.ImageCap <Card.ImageCap

View File

@@ -1,6 +1,8 @@
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react';
import LookingForChallengeWidget from '.'; import LookingForChallengeWidget from '.';
import { trackRecommendationUnavailable } from '../RecommendationsPanel/track';
jest.mock('hooks', () => ({ jest.mock('hooks', () => ({
reduxHooks: { reduxHooks: {
@@ -12,6 +14,7 @@ jest.mock('hooks', () => ({
jest.mock('../RecommendationsPanel/track', () => ({ jest.mock('../RecommendationsPanel/track', () => ({
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`), findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
trackRecommendationUnavailable: jest.fn(),
})); }));
describe('LookingForChallengeWidget', () => { describe('LookingForChallengeWidget', () => {
@@ -21,4 +24,10 @@ describe('LookingForChallengeWidget', () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
}); });
test('test recommendations unavailable event is fired', () => {
shallow(<LookingForChallengeWidget />);
const [cb] = React.useEffect.mock.calls[0];
cb();
expect(trackRecommendationUnavailable).toBeCalled();
});
}); });

View File

@@ -10,8 +10,12 @@ import { reduxHooks } from 'hooks';
import track from './track'; import track from './track';
import CourseCard from './components/CourseCard'; import CourseCard from './components/CourseCard';
import messages from './messages'; import messages from './messages';
import ModalView from '../../components/ModalView';
import './index.scss'; import './index.scss';
import usePaintedDoorExperimentContext from '../../RecsPaintedDoorExpContext';
import { useRecommendationsModal } from '../../components/ModalView/hooks';
import { trackPaintedDoorRecommendationHomeBtnClicked } from './recsPaintedDoorExpTrack';
export const LoadedView = ({ export const LoadedView = ({
courses, courses,
@@ -19,8 +23,21 @@ export const LoadedView = ({
}) => { }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData(); const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
const {
experimentVariation,
isPaintedDoorWidgetBtnVariation,
experimentLoading,
} = usePaintedDoorExperimentContext();
const handleSeeAllRecommendationsClick = () => {
toggleRecommendationsModal();
trackPaintedDoorRecommendationHomeBtnClicked(experimentVariation);
};
return ( return (
<>
<div className="p-4 w-100 panel-background"> <div className="p-4 w-100 panel-background">
<h3 className="pb-2">{isControl === false <h3 className="pb-2">{isControl === false
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)} ? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
@@ -35,6 +52,14 @@ export const LoadedView = ({
))} ))}
</div> </div>
<div className="text-center explore-courses-btn"> <div className="text-center explore-courses-btn">
{!experimentLoading && isPaintedDoorWidgetBtnVariation ? (
<Button
variant="brand"
onClick={handleSeeAllRecommendationsClick}
>
{formatMessage(messages.seeAllRecommendationsButton)}
</Button>
) : (
<Button <Button
variant="tertiary" variant="tertiary"
iconBefore={Search} iconBefore={Search}
@@ -44,8 +69,15 @@ export const LoadedView = ({
> >
{formatMessage(messages.exploreCoursesButton)} {formatMessage(messages.exploreCoursesButton)}
</Button> </Button>
)}
</div> </div>
</div> </div>
<ModalView
isOpen={isRecommendationsModalOpen}
onClose={toggleRecommendationsModal}
variation={experimentVariation}
/>
</>
); );
}; };

View File

@@ -19,11 +19,19 @@ jest.mock('./track', () => ({
findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`), findCoursesWidgetClicked: (href) => jest.fn().mockName(`track.findCoursesWidgetClicked('${href}')`),
})); }));
jest.mock('./components/CourseCard', () => 'CourseCard'); jest.mock('./components/CourseCard', () => 'CourseCard');
jest.mock('../../components/ModalView/hooks', () => ({
useRecommendationsModal: jest.fn(() => ({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
})),
}));
describe('RecommendationsPanel LoadedView', () => { describe('RecommendationsPanel LoadedView', () => {
const props = { const props = {
courses: mockData.courses, courses: mockData.courses,
isControl: null, isControl: null,
setIsRecommendationsModalOpen: () => {},
isRecommendationsModalOpen: false,
}; };
describe('snapshot', () => { describe('snapshot', () => {
test('without personalize recommendation', () => { test('without personalize recommendation', () => {

View File

@@ -1,9 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RecommendationsPanel LoadedView snapshot with personalize recommendation 1`] = ` exports[`RecommendationsPanel LoadedView snapshot with personalize recommendation 1`] = `
<div <Fragment>
<div
className="p-4 w-100 panel-background" className="p-4 w-100 panel-background"
> >
<h3 <h3
className="pb-2" className="pb-2"
> >
@@ -72,13 +73,19 @@ exports[`RecommendationsPanel LoadedView snapshot with personalize recommendatio
Explore courses Explore courses
</Button> </Button>
</div> </div>
</div> </div>
<ModalView
isOpen={false}
onClose={[MockFunction]}
/>
</Fragment>
`; `;
exports[`RecommendationsPanel LoadedView snapshot without personalize recommendation 1`] = ` exports[`RecommendationsPanel LoadedView snapshot without personalize recommendation 1`] = `
<div <Fragment>
<div
className="p-4 w-100 panel-background" className="p-4 w-100 panel-background"
> >
<h3 <h3
className="pb-2" className="pb-2"
> >
@@ -147,5 +154,10 @@ exports[`RecommendationsPanel LoadedView snapshot without personalize recommenda
Explore courses Explore courses
</Button> </Button>
</div> </div>
</div> </div>
<ModalView
isOpen={false}
onClose={[MockFunction]}
/>
</Fragment>
`; `;

View File

@@ -19,7 +19,10 @@ export const RecommendationsPanel = () => {
} }
if (isLoaded && courses.length > 0) { if (isLoaded && courses.length > 0) {
return ( return (
<LoadedView courses={courses} isControl={isControl} /> <LoadedView
courses={courses}
isControl={isControl}
/>
); );
} }
if (isFailed) { if (isFailed) {

View File

@@ -7,10 +7,14 @@ import mockData from './mockData';
import LoadedView from './LoadedView'; import LoadedView from './LoadedView';
import LoadingView from './LoadingView'; import LoadingView from './LoadingView';
import RecommendationsPanel from '.'; import RecommendationsPanel from '.';
import { useRecommendationsModal } from '../../components/ModalView/hooks';
jest.mock('./hooks', () => ({ jest.mock('./hooks', () => ({
useRecommendationPanelData: jest.fn(), useRecommendationPanelData: jest.fn(),
})); }));
jest.mock('../../components/ModalView/hooks', () => ({
useRecommendationsModal: jest.fn(),
}));
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget'); jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('./LoadingView', () => 'LoadingView'); jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./LoadedView', () => 'LoadedView'); jest.mock('./LoadedView', () => 'LoadedView');
@@ -21,6 +25,8 @@ describe('RecommendationsPanel snapshot', () => {
const defaultLoadedViewProps = { const defaultLoadedViewProps = {
courses: [], courses: [],
isControl: false, isControl: false,
setIsRecommendationsModalOpen: jest.fn(),
isRecommendationsModalOpen: false,
}; };
const defaultValues = { const defaultValues = {
isFailed: false, isFailed: false,
@@ -29,6 +35,10 @@ describe('RecommendationsPanel snapshot', () => {
...defaultLoadedViewProps, ...defaultLoadedViewProps,
}; };
it('displays LoadingView if request is loading', () => { it('displays LoadingView if request is loading', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({ hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues, ...defaultValues,
isLoading: true, isLoading: true,
@@ -36,16 +46,23 @@ describe('RecommendationsPanel snapshot', () => {
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />)); expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
}); });
it('displays LoadedView with courses if request is loaded', () => { it('displays LoadedView with courses if request is loaded', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({ hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues, ...defaultValues,
courses, courses,
isLoaded: true, isLoaded: true,
}); });
expect(shallow(<RecommendationsPanel />)).toMatchObject( expect(JSON.stringify(shallow(<RecommendationsPanel />)))
shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />), .toEqual(JSON.stringify(shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />)));
);
}); });
it('displays LookingForChallengeWidget if request is failed', () => { it('displays LookingForChallengeWidget if request is failed', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({ hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues, ...defaultValues,
isFailed: true, isFailed: true,
@@ -55,6 +72,10 @@ describe('RecommendationsPanel snapshot', () => {
); );
}); });
it('defaults to LookingForChallengeWidget if no flags are true', () => { it('defaults to LookingForChallengeWidget if no flags are true', () => {
useRecommendationsModal.mockReturnValueOnce({
isRecommendationsModalOpen: false,
toggleRecommendationsModal: jest.fn(),
});
hooks.useRecommendationPanelData.mockReturnValueOnce({ hooks.useRecommendationPanelData.mockReturnValueOnce({
...defaultValues, ...defaultValues,
}); });

View File

@@ -16,6 +16,11 @@ const messages = defineMessages({
defaultMessage: 'Explore courses', defaultMessage: 'Explore courses',
description: 'Button to explore more courses on recommendations panel', description: 'Button to explore more courses on recommendations panel',
}, },
seeAllRecommendationsButton: {
id: 'RecommendationsPanel.seeAllRecommendationsButton',
defaultMessage: 'See all recommendations',
description: 'Button to see all recommendations',
},
}); });
export default messages; export default messages;

View File

@@ -0,0 +1,35 @@
import { StrictDict } from 'utils';
import { createEventTracker } from 'data/services/segment/utils';
export const eventNames = StrictDict({
variationGroup: 'edx.bi.user.recommendation_home.variation.group',
recommendationHomeBtnClicked: 'edx.bi.user.recommendation_home.btn.clicked',
recommendationHomeModalInterestBtnClicked: 'edx.bi.user.recommendation_home.modal.interest_btn.clicked',
recommendationHomeModalSkipBtnClicked: 'edx.bi.user.recommendation_home.modal.skip_btn.clicked',
});
export const trackPaintedDoorVariationGroup = (variation) => {
createEventTracker(eventNames.variationGroup, {
variation,
page: 'dashboard',
});
};
export const trackPaintedDoorRecommendationHomeBtnClicked = (variation) => {
createEventTracker(eventNames.recommendationHomeBtnClicked, {
variation,
page: 'dashboard',
});
};
export const trackPaintedDoorRecommendationHomeInterestBtnClicked = (variation) => {
createEventTracker(eventNames.recommendationHomeModalInterestBtnClicked, {
variation,
});
};
export const trackPaintedDoorRecommendationHomeSkipBtnClicked = (variation) => {
createEventTracker(eventNames.recommendationHomeModalSkipBtnClicked, {
variation,
});
};

View File

@@ -4,6 +4,7 @@ import track from 'tracking';
export const eventNames = StrictDict({ export const eventNames = StrictDict({
recommendedCourseClicked: 'edx.bi.user.recommended.course.click', recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
recommendationsUnavailable: 'edx.bi.user.recommendations.unavailable',
}); });
export const linkNames = StrictDict({ export const linkNames = StrictDict({
@@ -23,7 +24,15 @@ export const recommendedCourseClicked = (courseKey, isControl, href) => createLi
href, href,
); );
export const trackRecommendationUnavailable = () => {
createEventTracker(eventNames.recommendationsUnavailable, {
type: 'personalized',
page: 'dashboard',
});
};
export default { export default {
findCoursesWidgetClicked, findCoursesWidgetClicked,
recommendedCourseClicked, recommendedCourseClicked,
trackRecommendationUnavailable,
}; };