feat: add recommendations modal (#189)
This commit is contained in:
51
src/components/ModalView/__snapshots__/index.test.jsx.snap
Normal file
51
src/components/ModalView/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,51 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ModalView snapshot should renders default ModalView 1`] = `
|
||||
<div
|
||||
className="containers"
|
||||
>
|
||||
<ModalDialog
|
||||
hasCloseButton={false}
|
||||
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
|
||||
variant="tertiary"
|
||||
>
|
||||
Skip for now
|
||||
</Component>
|
||||
<Button
|
||||
variant="primary"
|
||||
>
|
||||
Count me in!
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</Component>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
`;
|
||||
18
src/components/ModalView/hooks.js
Normal file
18
src/components/ModalView/hooks.js
Normal 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,
|
||||
};
|
||||
};
|
||||
21
src/components/ModalView/hooks.test.js
Normal file
21
src/components/ModalView/hooks.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
56
src/components/ModalView/index.jsx
Normal file
56
src/components/ModalView/index.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ModalDialog, ActionRow, Button } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
export const ModalView = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="containers">
|
||||
<ModalDialog
|
||||
title=""
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
hasCloseButton={false}
|
||||
isFullscreenScroll
|
||||
>
|
||||
<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">
|
||||
{formatMessage(messages.modalSkipButton)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button variant="primary">{formatMessage(messages.modalCountMeButton)}</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModalView.defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
ModalView.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ModalView;
|
||||
16
src/components/ModalView/index.test.jsx
Normal file
16
src/components/ModalView/index.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
31
src/components/ModalView/messages.js
Normal file
31
src/components/ModalView/messages.js
Normal 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;
|
||||
@@ -12,9 +12,10 @@ import { reduxHooks } from 'hooks';
|
||||
|
||||
import { findCoursesNavDropdownClicked } from '../hooks';
|
||||
|
||||
import ModalView from '../../../components/ModalView';
|
||||
import messages from '../messages';
|
||||
|
||||
export const CollapseMenuBody = ({ isOpen }) => {
|
||||
export const CollapseMenuBody = ({ isOpen, isRecommendationsModalOpen, setIsRecommendationsModalOpen }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
|
||||
@@ -40,6 +41,12 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="inverse-primary"
|
||||
onClick={setIsRecommendationsModalOpen}
|
||||
>
|
||||
{formatMessage(messages.recommendedForYou)}
|
||||
</Button>
|
||||
<Button as="a" href={getConfig().SUPPORT_URL} variant="inverse-primary">
|
||||
{formatMessage(messages.help)}
|
||||
</Button>
|
||||
@@ -92,6 +99,7 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<ModalView isOpen={isRecommendationsModalOpen} onClose={setIsRecommendationsModalOpen} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
@@ -99,6 +107,13 @@ export const CollapseMenuBody = ({ isOpen }) => {
|
||||
|
||||
CollapseMenuBody.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
isRecommendationsModalOpen: PropTypes.bool,
|
||||
setIsRecommendationsModalOpen: PropTypes.func,
|
||||
};
|
||||
|
||||
CollapseMenuBody.defaultProps = {
|
||||
isRecommendationsModalOpen: false,
|
||||
setIsRecommendationsModalOpen: () => {},
|
||||
};
|
||||
|
||||
export default CollapseMenuBody;
|
||||
|
||||
@@ -3,6 +3,13 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
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', () => ({
|
||||
AppContext: {
|
||||
authenticatedUser: {
|
||||
@@ -15,6 +22,7 @@ jest.mock('hooks', () => ({
|
||||
reduxHooks: {
|
||||
useEnterpriseDashboardData: () => ({
|
||||
url: 'url',
|
||||
dashboard: false,
|
||||
}),
|
||||
usePlatformSettingsData: () => ({
|
||||
courseSearchUrl: '/courseSearchUrl',
|
||||
|
||||
@@ -13,22 +13,27 @@ exports[`CollapseMenuBody render 1`] = `
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
href="undefined/dashboard/programs"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
href="undefined/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Recommended for you
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
@@ -42,25 +47,35 @@ exports[`CollapseMenuBody render 1`] = `
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/u/username"
|
||||
href="undefined/u/username"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Profile
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/account/settings"
|
||||
href="undefined/account/settings"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Account
|
||||
</Button>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
<ModalView
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -79,25 +94,34 @@ exports[`CollapseMenuBody render unauthenticated 1`] = `
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/dashboard/programs"
|
||||
href="undefined/dashboard/programs"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Programs
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("http://localhost:18000/courseSearchUrl")]}
|
||||
href="undefined/courseSearchUrl"
|
||||
onClick={[MockFunction findCoursesNavDropdownClicked("undefined/courseSearchUrl")]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Recommended for you
|
||||
</Button>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/support"
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
<ModalView
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -20,6 +20,8 @@ exports[`CollapsedHeader renders 1`] = `
|
||||
</header>
|
||||
<mockConstructor
|
||||
isOpen={false}
|
||||
isRecommendationsModalOpen={false}
|
||||
setIsRecommendationsModalOpen={[MockFunction]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -43,6 +45,8 @@ exports[`CollapsedHeader renders with isOpen true 1`] = `
|
||||
</header>
|
||||
<mockConstructor
|
||||
isOpen={true}
|
||||
isRecommendationsModalOpen={false}
|
||||
setIsRecommendationsModalOpen={[MockFunction]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Menu, Close } from '@edx/paragon/icons';
|
||||
import { IconButton, Icon } from '@edx/paragon';
|
||||
|
||||
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
||||
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
|
||||
|
||||
import CollapseMenuBody from './CollapseMenuBody';
|
||||
import BrandLogo from '../BrandLogo';
|
||||
@@ -15,6 +16,7 @@ export const CollapsedHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { isOpen, toggleIsOpen } = useLearnerDashboardHeaderData();
|
||||
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
|
||||
|
||||
return (
|
||||
isCollapsed && (
|
||||
@@ -36,7 +38,11 @@ export const CollapsedHeader = () => {
|
||||
/>
|
||||
<BrandLogo />
|
||||
</header>
|
||||
<CollapseMenuBody isOpen={isOpen} />
|
||||
<CollapseMenuBody
|
||||
isOpen={isOpen}
|
||||
setIsRecommendationsModalOpen={toggleRecommendationsModal}
|
||||
isRecommendationsModalOpen={isRecommendationsModalOpen}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import CollapsedHeader from '.';
|
||||
|
||||
import { useLearnerDashboardHeaderData, useIsCollapsed } from '../hooks';
|
||||
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
|
||||
|
||||
jest.mock('../BrandLogo', () => jest.fn(() => 'BrandLogo'));
|
||||
jest.mock('./CollapseMenuBody', () => jest.fn(() => 'CollapseMenuBody'));
|
||||
@@ -15,14 +16,26 @@ jest.mock('../hooks', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../../components/ModalView/hooks', () => ({
|
||||
useRecommendationsModal: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CollapsedHeader', () => {
|
||||
it('renders', () => {
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('render nothing if not collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -32,6 +45,10 @@ describe('CollapsedHeader', () => {
|
||||
isOpen: true,
|
||||
toggleIsOpen: jest.fn().mockName('toggleIsOpen'),
|
||||
});
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
const wrapper = shallow(<CollapsedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -33,6 +33,13 @@ exports[`ExpandedHeader render 1`] = `
|
||||
>
|
||||
Discover New
|
||||
</Button>
|
||||
<Button
|
||||
className="p-4"
|
||||
onClick={[MockFunction]}
|
||||
variant="inverse-primary"
|
||||
>
|
||||
Recommended for you
|
||||
</Button>
|
||||
<span
|
||||
className="flex-grow-1"
|
||||
/>
|
||||
@@ -46,6 +53,10 @@ exports[`ExpandedHeader render 1`] = `
|
||||
</Button>
|
||||
</div>
|
||||
<AuthenticatedUserDropdown />
|
||||
<ModalView
|
||||
isOpen={false}
|
||||
onClose={[MockFunction]}
|
||||
/>
|
||||
</header>
|
||||
`;
|
||||
|
||||
|
||||
@@ -10,13 +10,16 @@ import { reduxHooks } from 'hooks';
|
||||
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
|
||||
import { useIsCollapsed, findCoursesNavClicked } from '../hooks';
|
||||
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
|
||||
import messages from '../messages';
|
||||
import ModalView from '../../../components/ModalView';
|
||||
import BrandLogo from '../BrandLogo';
|
||||
|
||||
export const ExpandedHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
|
||||
|
||||
const exploreCoursesClick = findCoursesNavClicked(urls.baseAppUrl(courseSearchUrl));
|
||||
|
||||
@@ -51,6 +54,13 @@ export const ExpandedHeader = () => {
|
||||
>
|
||||
{formatMessage(messages.discoverNew)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="inverse-primary"
|
||||
className="p-4"
|
||||
onClick={toggleRecommendationsModal}
|
||||
>
|
||||
{formatMessage(messages.recommendedForYou)}
|
||||
</Button>
|
||||
<span className="flex-grow-1" />
|
||||
<Button
|
||||
as="a"
|
||||
@@ -63,6 +73,7 @@ export const ExpandedHeader = () => {
|
||||
</div>
|
||||
|
||||
<AuthenticatedUserDropdown />
|
||||
<ModalView isOpen={isRecommendationsModalOpen} onClose={toggleRecommendationsModal} />
|
||||
</header>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shallow } from 'enzyme';
|
||||
import ExpandedHeader from '.';
|
||||
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
import { useRecommendationsModal } from '../../../components/ModalView/hooks';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
programsUrl: 'programsUrl',
|
||||
@@ -19,21 +20,34 @@ jest.mock('hooks', () => ({
|
||||
|
||||
jest.mock('../hooks', () => ({
|
||||
useIsCollapsed: jest.fn(),
|
||||
useRecommendationsModal: jest.fn(),
|
||||
findCoursesNavClicked: (url) => jest.fn().mockName(`findCoursesNavClicked("${url}")`),
|
||||
}));
|
||||
|
||||
jest.mock('../../../components/ModalView/hooks', () => ({
|
||||
useRecommendationsModal: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
|
||||
jest.mock('../BrandLogo', () => 'BrandLogo');
|
||||
|
||||
describe('ExpandedHeader', () => {
|
||||
test('render', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(false);
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
const wrapper = shallow(<ExpandedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('render empty if collapsed', () => {
|
||||
useIsCollapsed.mockReturnValueOnce(true);
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
const wrapper = shallow(<ExpandedHeader />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
|
||||
@@ -51,6 +51,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Discover New',
|
||||
description: 'Header link for switching to discover page.',
|
||||
},
|
||||
recommendedForYou: {
|
||||
id: 'learnerVariantDashboard.recommendedForYou',
|
||||
defaultMessage: 'Recommended for you',
|
||||
description: 'Header link for recommended page.',
|
||||
},
|
||||
logoAltText: {
|
||||
id: 'learnerVariantDashboard.logoAltText',
|
||||
defaultMessage: 'edX, Inc. Dashboard',
|
||||
|
||||
@@ -1,51 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Search } from '@edx/paragon/icons';
|
||||
import { baseAppUrl } from 'data/services/lms/urls';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { reduxHooks } from 'hooks';
|
||||
import track from './track';
|
||||
import CourseCard from './components/CourseCard';
|
||||
import messages from './messages';
|
||||
import ModalView from '../../components/ModalView';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const LoadedView = ({
|
||||
courses,
|
||||
isControl,
|
||||
setIsRecommendationsModalOpen,
|
||||
isRecommendationsModalOpen,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseSearchUrl } = reduxHooks.usePlatformSettingsData();
|
||||
|
||||
return (
|
||||
<div className="p-4 w-100 panel-background">
|
||||
<h3 className="pb-2">{isControl === false
|
||||
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
|
||||
</h3>
|
||||
<div>
|
||||
{courses.map((course) => (
|
||||
<CourseCard
|
||||
key={course.courseKey}
|
||||
course={course}
|
||||
isControl={isControl}
|
||||
/>
|
||||
))}
|
||||
<>
|
||||
<div className="p-4 w-100 panel-background">
|
||||
<h3 className="pb-2">{isControl === false
|
||||
? formatMessage(messages.recommendationsHeading) : formatMessage(messages.popularCoursesHeading)}
|
||||
</h3>
|
||||
<div>
|
||||
{courses.map((course) => (
|
||||
<CourseCard
|
||||
key={course.courseKey}
|
||||
course={course}
|
||||
isControl={isControl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center explore-courses-btn">
|
||||
<Button
|
||||
variant="brand"
|
||||
onClick={setIsRecommendationsModalOpen}
|
||||
>
|
||||
{formatMessage(messages.seeAllRecommendationsButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center explore-courses-btn">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
iconBefore={Search}
|
||||
as="a"
|
||||
href={baseAppUrl(courseSearchUrl)}
|
||||
onClick={track.findCoursesWidgetClicked(baseAppUrl(courseSearchUrl))}
|
||||
>
|
||||
{formatMessage(messages.exploreCoursesButton)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ModalView isOpen={isRecommendationsModalOpen} onClose={setIsRecommendationsModalOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -61,6 +59,8 @@ LoadedView.propTypes = {
|
||||
marketingUrl: PropTypes.string,
|
||||
})).isRequired,
|
||||
isControl: PropTypes.oneOf([true, false, null]),
|
||||
setIsRecommendationsModalOpen: PropTypes.func.isRequired,
|
||||
isRecommendationsModalOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default LoadedView;
|
||||
|
||||
@@ -24,6 +24,8 @@ describe('RecommendationsPanel LoadedView', () => {
|
||||
const props = {
|
||||
courses: mockData.courses,
|
||||
isControl: null,
|
||||
setIsRecommendationsModalOpen: () => {},
|
||||
isRecommendationsModalOpen: false,
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
test('without personalize recommendation', () => {
|
||||
|
||||
@@ -1,151 +1,157 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecommendationsPanel LoadedView snapshot with personalize recommendation 1`] = `
|
||||
<div
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Recommendations for you
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<Fragment>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
|
||||
variant="tertiary"
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
Recommendations for you
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={false}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="brand"
|
||||
>
|
||||
See All Recommendations
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ModalView
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`RecommendationsPanel LoadedView snapshot without personalize recommendation 1`] = `
|
||||
<div
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Popular courses
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<Fragment>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
className="p-4 w-100 panel-background"
|
||||
>
|
||||
<Button
|
||||
as="a"
|
||||
href="http://localhost:18000/course-search-url"
|
||||
iconBefore={[MockFunction icons.Search]}
|
||||
onClick={[MockFunction track.findCoursesWidgetClicked('http://localhost:18000/course-search-url')]}
|
||||
variant="tertiary"
|
||||
<h3
|
||||
className="pb-2"
|
||||
>
|
||||
Explore courses
|
||||
</Button>
|
||||
Popular courses
|
||||
</h3>
|
||||
<div>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-1",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 1",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-1"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-2",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 2 with a really really really long name for some reason",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-2"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-3",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 3",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-3"
|
||||
/>
|
||||
<CourseCard
|
||||
course={
|
||||
Object {
|
||||
"courseKey": "cs-4",
|
||||
"logoImageUrl": "https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg",
|
||||
"marketingUrl": "www.edx/recommended-course",
|
||||
"title": "Recommended course 4",
|
||||
}
|
||||
}
|
||||
isControl={null}
|
||||
key="cs-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-center explore-courses-btn"
|
||||
>
|
||||
<Button
|
||||
onClick={[Function]}
|
||||
variant="brand"
|
||||
>
|
||||
See All Recommendations
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ModalView
|
||||
isOpen={false}
|
||||
onClose={[Function]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import LookingForChallengeWidget from 'widgets/LookingForChallengeWidget';
|
||||
import LoadingView from './LoadingView';
|
||||
import LoadedView from './LoadedView';
|
||||
import hooks from './hooks';
|
||||
import { useRecommendationsModal } from '../../components/ModalView/hooks';
|
||||
|
||||
export const RecommendationsPanel = () => {
|
||||
const {
|
||||
@@ -14,12 +15,19 @@ export const RecommendationsPanel = () => {
|
||||
isLoading,
|
||||
} = hooks.useRecommendationPanelData();
|
||||
|
||||
const { isRecommendationsModalOpen, toggleRecommendationsModal } = useRecommendationsModal();
|
||||
|
||||
if (isLoading) {
|
||||
return (<LoadingView />);
|
||||
}
|
||||
if (isLoaded && courses.length > 0) {
|
||||
return (
|
||||
<LoadedView courses={courses} isControl={isControl} />
|
||||
<LoadedView
|
||||
courses={courses}
|
||||
isControl={isControl}
|
||||
setIsRecommendationsModalOpen={toggleRecommendationsModal}
|
||||
isRecommendationsModalOpen={isRecommendationsModalOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isFailed) {
|
||||
|
||||
@@ -7,10 +7,14 @@ import mockData from './mockData';
|
||||
import LoadedView from './LoadedView';
|
||||
import LoadingView from './LoadingView';
|
||||
import RecommendationsPanel from '.';
|
||||
import { useRecommendationsModal } from '../../components/ModalView/hooks';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
useRecommendationPanelData: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../components/ModalView/hooks', () => ({
|
||||
useRecommendationsModal: jest.fn(),
|
||||
}));
|
||||
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
|
||||
jest.mock('./LoadingView', () => 'LoadingView');
|
||||
jest.mock('./LoadedView', () => 'LoadedView');
|
||||
@@ -21,6 +25,8 @@ describe('RecommendationsPanel snapshot', () => {
|
||||
const defaultLoadedViewProps = {
|
||||
courses: [],
|
||||
isControl: false,
|
||||
setIsRecommendationsModalOpen: jest.fn(),
|
||||
isRecommendationsModalOpen: false,
|
||||
};
|
||||
const defaultValues = {
|
||||
isFailed: false,
|
||||
@@ -29,6 +35,10 @@ describe('RecommendationsPanel snapshot', () => {
|
||||
...defaultLoadedViewProps,
|
||||
};
|
||||
it('displays LoadingView if request is loading', () => {
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
isLoading: true,
|
||||
@@ -36,16 +46,23 @@ describe('RecommendationsPanel snapshot', () => {
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(shallow(<LoadingView />));
|
||||
});
|
||||
it('displays LoadedView with courses if request is loaded', () => {
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
courses,
|
||||
isLoaded: true,
|
||||
});
|
||||
expect(shallow(<RecommendationsPanel />)).toMatchObject(
|
||||
shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />),
|
||||
);
|
||||
expect(JSON.stringify(shallow(<RecommendationsPanel />)))
|
||||
.toEqual(JSON.stringify(shallow(<LoadedView {...defaultLoadedViewProps} courses={courses} />)));
|
||||
});
|
||||
it('displays LookingForChallengeWidget if request is failed', () => {
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
isFailed: true,
|
||||
@@ -55,6 +72,10 @@ describe('RecommendationsPanel snapshot', () => {
|
||||
);
|
||||
});
|
||||
it('defaults to LookingForChallengeWidget if no flags are true', () => {
|
||||
useRecommendationsModal.mockReturnValueOnce({
|
||||
isRecommendationsModalOpen: false,
|
||||
toggleRecommendationsModal: jest.fn(),
|
||||
});
|
||||
hooks.useRecommendationPanelData.mockReturnValueOnce({
|
||||
...defaultValues,
|
||||
});
|
||||
|
||||
@@ -16,6 +16,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Explore courses',
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user