Bw/recommendations panel (#63)

Co-authored-by: Shafqat Farhan <shafqat.farhan@arbisoft.com>
This commit is contained in:
Ben Warzeski
2022-11-04 15:01:56 -04:00
committed by GitHub
parent b8245d6631
commit dde8d45df3
62 changed files with 1149 additions and 425 deletions

View File

@@ -92,7 +92,7 @@ exports[`CertificateBanner snapshot is restricted and verified 1`] = `
exports[`CertificateBanner snapshot not passing and audit 1`] = `
<Banner>
Grade required to pass the course: 0.8
Grade required to pass the course: 0.8%
</Banner>
`;
@@ -114,6 +114,6 @@ exports[`CertificateBanner snapshot not passing and not audit and not finished 1
<Banner
variant="warning"
>
Grade required for a certificate: 0.8
Grade required for a certificate: 0.8%
</Banner>
`;

View File

@@ -39,7 +39,7 @@ export const messages = StrictDict({
passingGrade: {
id: 'learner-dash.courseCard.banners.passingGrade',
description: 'Message to learners with minimum passing grade for the course',
defaultMessage: 'Grade required to pass the course: {minPassingGrade}',
defaultMessage: 'Grade required to pass the course: {minPassingGrade}\u200f%',
},
notEligibleForCert: {
id: 'learner-dash.courseCard.banners.notEligibleForCert',
@@ -64,7 +64,7 @@ export const messages = StrictDict({
certMinGrade: {
id: 'learner-dash.courseCard.banners.certMinGrade',
description: 'Passing grade requirement message',
defaultMessage: 'Grade required for a certificate: {minPassingGrade}',
defaultMessage: 'Grade required for a certificate: {minPassingGrade}\u200f%',
},
downloadCertificate: {
id: 'learner-dash.courseCard.banners.downloadCertificate',

View File

@@ -14,6 +14,8 @@ import {
} from '@edx/paragon';
import { Close, Tune } from '@edx/paragon/icons';
import { hooks as appHooks } from 'data/redux';
import FilterForm from './components/FilterForm';
import SortForm from './components/SortForm';
import useCourseFilterControlsData from './hooks';
@@ -28,6 +30,7 @@ export const CourseFilterControls = ({
setFilters,
}) => {
const { formatMessage } = useIntl();
const hasCourses = appHooks.useHasCourses();
const {
isOpen,
open,
@@ -50,6 +53,7 @@ export const CourseFilterControls = ({
variant="outline-primary"
iconBefore={Tune}
onClick={open}
disabled={!hasCourses}
>
{formatMessage(messages.refine)}
</Button>

View File

@@ -1,14 +1,23 @@
import { shallow } from 'enzyme';
import { breakpoints, useWindowSize } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import CourseFilterControls from './CourseFilterControls';
import useCourseFilterControlsData from './hooks';
jest.mock('data/redux', () => ({
hooks: { useHasCourses: jest.fn() },
}));
jest.mock('./hooks', () => jest.fn().mockName('useCourseFilterControlsData'));
jest.mock('./components/FilterForm', () => 'FilterForm');
jest.mock('./components/SortForm', () => 'SortForm');
appHooks.useHasCourses.mockReturnValue(true);
describe('CourseFilterControls', () => {
const props = {
sortBy: 'test-sort-by',
@@ -30,13 +39,23 @@ describe('CourseFilterControls', () => {
handleSortChange: jest.fn().mockName('handleSortChange'),
});
describe('snapshot', () => {
test('is mobile', () => {
describe('no courses', () => {
test('snapshot', () => {
appHooks.useHasCourses.mockReturnValueOnce(false);
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe('mobile', () => {
test('snapshot', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth - 1 });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper).toMatchSnapshot();
});
test('is not mobile', () => {
});
describe('is not mobile', () => {
test('snapshot', () => {
useWindowSize.mockReturnValueOnce({ width: breakpoints.small.minWidth });
const wrapper = shallow(<CourseFilterControls {...props} />);
expect(wrapper).toMatchSnapshot();

View File

@@ -1,10 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseFilterControls snapshot is mobile 1`] = `
exports[`CourseFilterControls is not mobile snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={false}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
>
Refine
</Button>
<Form>
<ModalPopup
isOpen={false}
onClose={[MockFunction close]}
placement="bottom-end"
positionRef="test-target"
>
<div
className="bg-white p-3 rounded shadow d-flex flex-row"
id="course-filter-controls-card"
>
<div
className="filter-form-col"
>
<FilterForm
filters={
Array [
"test-filter",
]
}
handleFilterChange={[MockFunction handleFilterChange]}
/>
</div>
<hr
className="h-100 bg-primary-200 mx-3 my-0"
/>
<div
className="filter-form-col text-left m-1"
>
<SortForm
handleSortChange={[MockFunction handleSortChange]}
sortBy="test-sort-by"
/>
</div>
</div>
</ModalPopup>
</Form>
</div>
`;
exports[`CourseFilterControls mobile snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={false}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"
@@ -63,11 +116,12 @@ exports[`CourseFilterControls snapshot is mobile 1`] = `
</div>
`;
exports[`CourseFilterControls snapshot is not mobile 1`] = `
exports[`CourseFilterControls no courses snapshot 1`] = `
<div
id="course-filter-controls"
>
<Button
disabled={true}
iconBefore={[MockFunction icons.Tune]}
onClick={[MockFunction open]}
variant="outline-primary"

View File

@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoCoursesView snapshot 1`] = `
<div
className="d-flex align-items-center justify-content-center mb-4.5"
id="no-courses-content-view"
>
<Image
alt="No Courses view banner"
src="icon/mock/path"
/>
<h1>
Looking for a new challenge?
</h1>
<p>
Explore our courses to add them to your dashboard.
</p>
<Button
as="a"
href="course-search-url"
variant="brand"
>
Explore courses
</Button>
</div>
`;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Image } from '@edx/paragon';
import { Search } from '@edx/paragon/icons';
import emptyCourseSVG from 'assets/empty-course.svg';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
import './index.scss';
export const NoCoursesView = () => {
const { courseSearchUrl } = appHooks.usePlatformSettingsData();
const { formatMessage } = useIntl();
return (
<div
id="no-courses-content-view"
className="d-flex align-items-center justify-content-center mb-4.5"
>
<Image src={emptyCourseSVG} alt={formatMessage(messages.bannerAlt)} />
<h1>
{formatMessage(messages.lookingForChallengePrompt)}
</h1>
<p>
{formatMessage(messages.exploreCoursesPrompt)}
</p>
<Button
variant="brand"
as="a"
href={courseSearchUrl}
iconBefore={Search}
>
{formatMessage(messages.exploreCoursesButton)}
</Button>
</div>
);
};
export default NoCoursesView;

View File

@@ -1,13 +1,15 @@
@import "@edx/paragon/scss/core/core";
.empty-course-hero {
display: flex;
#no-courses-content-view {
border: 2px solid $light-400;
flex-direction: column;
align-items: center;
padding-bottom: map-get($spacers, 5);
padding-top: map-get($spacers, 5);
height: 100%;
& > * {
margin-top: map-get($spacers, 3);
margin-bottom: map-get($spacers, 3);
}
}
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import EmptyCourse from '.';
jest.mock('data/redux', () => ({
hooks: {
useRecommendedCoursesData: jest.fn(() => ({ courses: [], isPersonalizedRecommendation: false })),
useRequestIsPending: jest.fn(),
usePlatformSettingsData: () => ({
courseSearchUrl: 'course-search-url',
}),
},
}));
jest.mock('containers/Dashboard/hooks', () => ({
useIsDashboardCollapsed: jest.fn(() => true),
}));
describe('NoCoursesView', () => {
test('snapshot', () => {
expect(shallow(<EmptyCourse />)).toMatchSnapshot();
});
});

View File

@@ -2,20 +2,25 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
lookingForChallengePrompt: {
id: 'EmptyCourse.lookingForChallengePrompt',
id: 'Dashboard.NoCoursesView.lookingForChallengePrompt',
defaultMessage: 'Looking for a new challenge?',
description: 'Prompt user for new challenge',
},
exploreCoursesPrompt: {
id: 'EmptyCourse.exploreCoursesPrompt',
id: 'Dashboard.NoCoursesView.exploreCoursesPrompt',
defaultMessage: 'Explore our courses to add them to your dashboard.',
description: 'Prompt user to explore more courses',
},
exploreCoursesButton: {
id: 'EmptyCourse.exploreCoursesButton',
id: 'Dashboard.NoCoursesView.exploreCoursesButton',
defaultMessage: 'Explore courses',
description: 'Button to explore more courses',
},
bannerAlt: {
id: 'Dashboard.NoCoursesView.bannerAlt',
defaultMessage: 'No Courses view banner',
description: 'No Courses view basnner',
},
});
export default messages;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseList snapshots collapsed with multiple courses and pages 1`] = `
exports[`CourseList collapsed with multiple courses and pages snapshot 1`] = `
<div
className="course-list-container"
>
@@ -39,7 +39,7 @@ exports[`CourseList snapshots collapsed with multiple courses and pages 1`] = `
key="baz"
/>
<Pagination
className="mx-auto"
className="mx-auto mb-2"
onPageSelect={[MockFunction setPageNumber]}
pageCount={3}
paginationLabel="Course List"
@@ -49,7 +49,55 @@ exports[`CourseList snapshots collapsed with multiple courses and pages 1`] = `
</div>
`;
exports[`CourseList snapshots with filters 1`] = `
exports[`CourseList no courses snapshot 1`] = `
<div
className="course-list-container"
>
<div
className="course-list-heading-container"
>
<h2
className="course-list-title"
>
My Courses
</h2>
<div
className="course-filter-controls-container"
>
<CourseFilterControls />
</div>
</div>
<div
className="d-flex flex-column flex-grow-1"
/>
</div>
`;
exports[`CourseList no filters snapshot 1`] = `
<div
className="course-list-container"
>
<div
className="course-list-heading-container"
>
<h2
className="course-list-title"
>
My Courses
</h2>
<div
className="course-filter-controls-container"
>
<CourseFilterControls />
</div>
</div>
<div
className="d-flex flex-column flex-grow-1"
/>
</div>
`;
exports[`CourseList with filters snapshot 1`] = `
<div
className="course-list-container"
>
@@ -82,7 +130,7 @@ exports[`CourseList snapshots with filters 1`] = `
</div>
`;
exports[`CourseList snapshots with multiple courses and pages 1`] = `
exports[`CourseList with multiple courses and pages snapshot 1`] = `
<div
className="course-list-container"
>
@@ -116,7 +164,7 @@ exports[`CourseList snapshots with multiple courses and pages 1`] = `
key="baz"
/>
<Pagination
className="mx-auto"
className="mx-auto mb-2"
onPageSelect={[MockFunction setPageNumber]}
pageCount={3}
paginationLabel="Course List"
@@ -125,27 +173,3 @@ exports[`CourseList snapshots with multiple courses and pages 1`] = `
</div>
</div>
`;
exports[`CourseList snapshots with no filters 1`] = `
<div
className="course-list-container"
>
<div
className="course-list-heading-container"
>
<h2
className="course-list-title"
>
My Courses
</h2>
<div
className="course-filter-controls-container"
>
<CourseFilterControls />
</div>
</div>
<div
className="d-flex flex-column flex-grow-1"
/>
</div>
`;

View File

@@ -3,11 +3,13 @@ import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Pagination } from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import {
ActiveCourseFilters,
CourseFilterControls,
} from 'containers/CourseFilterControls';
import CourseCard from 'containers/CourseCard';
import NoCoursesView from './NoCoursesView';
import { useCourseListData, useIsCollapsed } from './hooks';
@@ -17,6 +19,7 @@ import './index.scss';
export const CourseList = () => {
const { formatMessage } = useIntl();
const hasCourses = appHooks.useHasCourses();
const {
filterOptions,
setPageNumber,
@@ -33,25 +36,32 @@ export const CourseList = () => {
<CourseFilterControls {...filterOptions} />
</div>
</div>
{showFilters && (
<div id="course-list-active-filters-container">
<ActiveCourseFilters {...filterOptions} />
</div>
)}
<div className="d-flex flex-column flex-grow-1">
{visibleList.map(({ cardId }) => (
<CourseCard key={cardId} cardId={cardId} />
))}
{numPages > 1 && (
<Pagination
variant={isCollapsed ? 'reduced' : 'secondary'}
paginationLabel="Course List"
className="mx-auto"
pageCount={numPages}
onPageSelect={setPageNumber}
/>
{hasCourses
? (
<>
{showFilters && (
<div id="course-list-active-filters-container">
<ActiveCourseFilters {...filterOptions} />
</div>
)}
<div className="d-flex flex-column flex-grow-1">
{visibleList.map(({ cardId }) => (
<CourseCard key={cardId} cardId={cardId} />
))}
{numPages > 1 && (
<Pagination
variant={isCollapsed ? 'reduced' : 'secondary'}
paginationLabel="Course List"
className="mx-auto mb-2"
pageCount={numPages}
onPageSelect={setPageNumber}
/>
)}
</div>
</>
) : (
<NoCoursesView />
)}
</div>
</div>
);
};

View File

@@ -1,7 +1,12 @@
import { shallow } from 'enzyme';
import CourseList from '.';
import { hooks as appHooks } from 'data/redux';
import { useCourseListData, useIsCollapsed } from './hooks';
import CourseList from '.';
jest.mock('data/redux', () => ({
hooks: { useHasCourses: jest.fn() },
}));
jest.mock('./hooks', () => ({
useCourseListData: jest.fn(),
@@ -14,6 +19,8 @@ jest.mock('containers/CourseFilterControls', () => ({
CourseFilterControls: 'CourseFilterControls',
}));
appHooks.useHasCourses.mockReturnValue(true);
describe('CourseList', () => {
const defaultCourseListData = {
filterOptions: {},
@@ -31,28 +38,39 @@ describe('CourseList', () => {
return shallow(<CourseList />);
};
describe('snapshots', () => {
test('with no filters', () => {
describe('no courses', () => {
test('snapshot', () => {
appHooks.useHasCourses.mockReturnValue(true);
const wrapper = createWrapper();
expect(wrapper).toMatchSnapshot();
});
test('with filters', () => {
});
describe('no filters', () => {
test('snapshot', () => {
const wrapper = createWrapper();
expect(wrapper).toMatchSnapshot();
});
});
describe('with filters', () => {
test('snapshot', () => {
const wrapper = createWrapper({
filterOptions: {
abitary: 'filter',
},
filterOptions: { abitary: 'filter' },
showFilters: true,
});
expect(wrapper).toMatchSnapshot();
});
test('with multiple courses and pages', () => {
});
describe('with multiple courses and pages', () => {
test('snapshot', () => {
const wrapper = createWrapper({
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }],
numPages: 3,
});
expect(wrapper).toMatchSnapshot();
});
test('collapsed with multiple courses and pages', () => {
});
describe('collapsed with multiple courses and pages', () => {
test('snapshot', () => {
useIsCollapsed.mockReturnValueOnce(true);
const wrapper = createWrapper({
visibleList: [{ cardId: 'foo' }, { cardId: 'bar' }, { cardId: 'baz' }],

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Container, Col, Row } from '@edx/paragon';
import CourseList from 'containers/CourseList';
import WidgetSidebar from 'containers/WidgetSidebar';
import hooks from './hooks';
export const columnConfig = {
@@ -20,22 +20,26 @@ export const columnConfig = {
},
};
export const LoadedView = () => {
export const DashboardLayout = ({ children, sidebar }) => {
const isCollapsed = hooks.useIsDashboardCollapsed();
return (
<Container fluid size="xl">
<Row>
<Col {...columnConfig.courseList} className="course-list-column">
<CourseList />
{children}
</Col>
<Col {...columnConfig.sidebar} className="sidebar-column">
{!isCollapsed && (<h2 className="course-list-title">&nbsp;</h2>)}
<WidgetSidebar />
{sidebar}
</Col>
</Row>
</Container>
);
};
DashboardLayout.propTypes = {
children: PropTypes.node.isRequired,
sidebar: PropTypes.node.isRequired,
};
export default LoadedView;
export default DashboardLayout;

View File

@@ -1,20 +1,22 @@
import { shallow } from 'enzyme';
import { Col, Row } from '@edx/paragon';
import CourseList from 'containers/CourseList';
import WidgetSidebar from 'containers/WidgetSidebar';
import hooks from './hooks';
import LoadedView, { columnConfig } from './LoadedView';
import DashboardLayout, { columnConfig } from './DashboardLayout';
jest.mock('./hooks', () => ({
useIsDashboardCollapsed: jest.fn(() => true),
}));
describe('LoadedView', () => {
describe('DashboardLayout', () => {
const children = 'test-children';
const props = {
sidebar: 'test-sidebar-content',
};
const render = () => shallow(<DashboardLayout sidebar={props.sidebar}>{children}</DashboardLayout>);
const testColumns = () => {
it('loads courseList and sidebar column layout', () => {
const columns = shallow(<LoadedView />).find(Row).find(Col);
const columns = render().find(Row).find(Col);
Object.keys(columnConfig.courseList).forEach(size => {
expect(columns.at(0).props()[size]).toEqual(columnConfig.courseList[size]);
});
@@ -22,25 +24,25 @@ describe('LoadedView', () => {
expect(columns.at(1).props()[size]).toEqual(columnConfig.sidebar[size]);
});
});
it('displays CourseList in first column', () => {
const columns = shallow(<LoadedView />).find(Row).find(Col);
expect(columns.at(0).find(CourseList).length).toEqual(1);
it('displays children in first column', () => {
const columns = render().find(Row).find(Col);
expect(columns.at(0).contains(children)).toEqual(true);
});
it('displays WidgetSidebar in second column', () => {
const columns = shallow(<LoadedView />).find(Row).find(Col);
expect(columns.at(1).find(WidgetSidebar).length).toEqual(1);
it('displays sidebar prop in second column', () => {
const columns = render().find(Row).find(Col);
expect(columns.at(1).contains(props.sidebar)).toEqual(true);
});
};
const testSnapshot = () => {
test('snapshot', () => {
expect(shallow(<LoadedView />)).toMatchSnapshot();
expect(render()).toMatchSnapshot();
});
};
describe('collapsed', () => {
testColumns();
testSnapshot();
it('does not show spacer component above widget sidebar', () => {
const columns = shallow(<LoadedView />).find(Col);
const columns = render().find(Col);
expect(columns.at(1).find('h2').length).toEqual(0);
});
});
@@ -50,7 +52,7 @@ describe('LoadedView', () => {
testColumns();
testSnapshot();
it('shows a blank (nbsp) h2 spacer component above widget sidebar', () => {
const columns = shallow(<LoadedView />).find(Col);
const columns = render().find(Col);
// nonbreaking space equivalent
expect(columns.at(1).find('h2').text()).toEqual('\xA0');
});

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LoadedView collapsed snapshot 1`] = `
exports[`DashboardLayout collapsed snapshot 1`] = `
<Container
fluid={true}
size="xl"
@@ -33,7 +33,7 @@ exports[`LoadedView collapsed snapshot 1`] = `
}
}
>
<CourseList />
test-children
</Col>
<Col
className="sidebar-column"
@@ -62,13 +62,13 @@ exports[`LoadedView collapsed snapshot 1`] = `
}
}
>
<WidgetSidebar />
test-sidebar-content
</Col>
</Row>
</Container>
`;
exports[`LoadedView not collapsed snapshot 1`] = `
exports[`DashboardLayout not collapsed snapshot 1`] = `
<Container
fluid={true}
size="xl"
@@ -101,7 +101,7 @@ exports[`LoadedView not collapsed snapshot 1`] = `
}
}
>
<CourseList />
test-children
</Col>
<Col
className="sidebar-column"
@@ -135,7 +135,7 @@ exports[`LoadedView not collapsed snapshot 1`] = `
>
 
</h2>
<WidgetSidebar />
test-sidebar-content
</Col>
</Row>
</Container>

View File

@@ -2,7 +2,7 @@
exports[`Dashboard snapshots courses loaded, show select session modal, no available dashboards snapshot 1`] = `
<div
className="d-flex flex-column p-2 pt-3"
className="d-flex flex-column p-2 pt-0"
id="dashboard-container"
>
<h1
@@ -14,14 +14,18 @@ exports[`Dashboard snapshots courses loaded, show select session modal, no avail
<div
id="dashboard-content"
>
<LoadedView />
<DashboardLayout
sidebar={<LoadedWidgetSidebar />}
>
<CourseList />
</DashboardLayout>
</div>
</div>
`;
exports[`Dashboard snapshots courses still loading snapshot 1`] = `
<div
className="d-flex flex-column p-2 pt-3"
className="d-flex flex-column p-2 pt-0"
id="dashboard-container"
>
<h1
@@ -39,7 +43,7 @@ exports[`Dashboard snapshots courses still loading snapshot 1`] = `
exports[`Dashboard snapshots there are no courses, there ARE available dashboards snapshot 1`] = `
<div
className="d-flex flex-column p-2 pt-3"
className="d-flex flex-column p-2 pt-0"
id="dashboard-container"
>
<h1
@@ -51,7 +55,11 @@ exports[`Dashboard snapshots there are no courses, there ARE available dashboard
<div
id="dashboard-content"
>
<EmptyCourse />
<DashboardLayout
sidebar={<NoCoursesWidgetSidebar />}
>
<CourseList />
</DashboardLayout>
</div>
</div>
`;

View File

@@ -2,14 +2,16 @@ import React from 'react';
import { hooks as appHooks } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';
import EmptyCourse from 'containers/EmptyCourse';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CourseList from 'containers/CourseList';
import LoadedSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import LoadingView from './LoadingView';
import LoadedView from './LoadedView';
import DashboardLayout from './DashboardLayout';
import hooks from './hooks';
import './index.scss';
export const Dashboard = () => {
@@ -20,7 +22,7 @@ export const Dashboard = () => {
const initIsPending = appHooks.useRequestIsPending(RequestKeys.initialize);
const showSelectSessionModal = appHooks.useShowSelectSessionModal();
return (
<div id="dashboard-container" className="d-flex flex-column p-2 pt-3">
<div id="dashboard-container" className="d-flex flex-column p-2 pt-0">
<h1 className="sr-only">{pageTitle}</h1>
{!initIsPending && (
<>
@@ -29,9 +31,13 @@ export const Dashboard = () => {
</>
)}
<div id="dashboard-content">
{initIsPending && (<LoadingView />)}
{(!initIsPending && hasCourses) && (<LoadedView />)}
{(!initIsPending && !hasCourses) && (<EmptyCourse />)}
{initIsPending
? (<LoadingView />)
: (
<DashboardLayout sidebar={hasCourses ? <LoadedSidebar /> : <NoCoursesSidebar />}>
<CourseList />
</DashboardLayout>
)}
</div>
</div>
);

View File

@@ -2,11 +2,14 @@ import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import EmptyCourse from 'containers/EmptyCourse';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import SelectSessionModal from 'containers/SelectSessionModal';
import CourseList from 'containers/CourseList';
import LoadedView from './LoadedView';
import LoadedWidgetSidebar from 'containers/WidgetContainers/LoadedSidebar';
import NoCoursesWidgetSidebar from 'containers/WidgetContainers/NoCoursesSidebar';
import DashboardLayout from './DashboardLayout';
import LoadingView from './LoadingView';
import hooks from './hooks';
import Dashboard from '.';
@@ -25,10 +28,12 @@ jest.mock('data/redux', () => ({
},
}));
jest.mock('containers/EmptyCourse', () => 'EmptyCourse');
jest.mock('containers/EnterpriseDashboardModal', () => 'EnterpriseDashboardModal');
jest.mock('containers/CourseList', () => 'CourseList');
jest.mock('containers/WidgetContainers/LoadedSidebar', () => 'LoadedWidgetSidebar');
jest.mock('containers/WidgetContainers/NoCoursesSidebar', () => 'NoCoursesWidgetSidebar');
jest.mock('./LoadingView', () => 'LoadingView');
jest.mock('./LoadedView', () => 'LoadedView');
jest.mock('./DashboardLayout', () => 'DashboardLayout');
jest.mock('./hooks', () => ({
useInitializeDashboard: jest.fn(),
@@ -115,7 +120,9 @@ describe('Dashboard', () => {
initIsPending: false,
showSelectSessionModal: true,
},
content: ['LoadedView', <LoadedView />],
content: ['LoadedView', (
<DashboardLayout sidebar={<LoadedWidgetSidebar />}><CourseList /></DashboardLayout>
)],
showEnterpriseModal: false,
showSelectSessionModal: true,
});
@@ -129,7 +136,9 @@ describe('Dashboard', () => {
initIsPending: false,
showSelectSessionModal: false,
},
content: ['EmptyCourse', <EmptyCourse />],
content: ['Dashboard layout with no courses sidebar and content', (
<DashboardLayout sidebar={<NoCoursesWidgetSidebar />}><CourseList /></DashboardLayout>
)],
showEnterpriseModal: true,
showSelectSessionModal: false,
});

View File

@@ -1,11 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
myCourses: {
id: 'dashboard.mycourses',
defaultMessage: 'My Courses',
description: 'Course list heading',
},
});
export default messages;

View File

@@ -1,94 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SuggestedCourses snapshot has 3 suggested courses 1`] = `
<Container
size="md"
>
<h3
className="text-center"
>
Some courses you may be interested in
</h3>
<div
className="d-flex"
>
<Card
className="m-3"
key="Suggested course 1"
>
<Card.ImageCap
src="https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg"
srcAlt="Course image banner"
/>
<Card.Header
className="mb-2"
title="Suggested course 1"
/>
<Card.Body
className="h-100"
/>
<Card.Footer>
<ActionRow>
<Button
as="a"
>
View Course
</Button>
</ActionRow>
</Card.Footer>
</Card>
<Card
className="m-3"
key="Suggested course 2"
>
<Card.ImageCap
src="https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg"
srcAlt="Course image banner"
/>
<Card.Header
className="mb-2"
title="Suggested course 2"
/>
<Card.Body
className="h-100"
/>
<Card.Footer>
<ActionRow>
<Button
as="a"
>
View Course
</Button>
</ActionRow>
</Card.Footer>
</Card>
<Card
className="m-3"
key="Suggested course 3"
>
<Card.ImageCap
src="https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg"
srcAlt="Course image banner"
/>
<Card.Header
className="mb-2"
title="Suggested course 3"
/>
<Card.Body
className="h-100"
/>
<Card.Footer>
<ActionRow>
<Button
as="a"
>
View Course
</Button>
</ActionRow>
</Card.Footer>
</Card>
</div>
</Container>
`;
exports[`SuggestedCourses snapshot no suggested courses 1`] = `""`;

View File

@@ -1,42 +0,0 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Card,
Button,
Container,
} from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
export const SuggestedCourses = () => {
const suggestedCourses = appHooks.useSuggestedCoursesData();
const { formatMessage } = useIntl();
if (!suggestedCourses.length) { return null; }
return (
<Container size="md">
<h3 className="text-center">{formatMessage(messages.header)}</h3>
<div className="d-flex">
{suggestedCourses.map((course) => (
<Card key={course.courseName} className="m-3">
<Card.ImageCap
src={course.bannerImgSrc}
srcAlt={formatMessage(messages.courseImageAlt)}
/>
<Card.Header title={course.courseName} className="mb-2" />
<Card.Body className="h-100" />
<Card.Footer>
<ActionRow>
<Button as="a">{formatMessage(messages.viewCourseButton)}</Button>
</ActionRow>
</Card.Footer>
</Card>
))}
</div>
</Container>
);
};
export default SuggestedCourses;

View File

@@ -1,41 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import SuggestedCourses from '.';
jest.mock('data/redux', () => ({
hooks: {
useSuggestedCoursesData: jest.fn(),
},
}));
const suggestedCourses = [
{
bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
courseName: 'Suggested course 1',
courseUrl: 'www.edx/suggested-course',
},
{
bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
courseName: 'Suggested course 2',
courseUrl: 'www.edx/suggested-course',
},
{
bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg',
courseName: 'Suggested course 3',
courseUrl: 'www.edx/suggested-course',
},
];
describe('SuggestedCourses snapshot', () => {
test('no suggested courses', () => {
appHooks.useSuggestedCoursesData.mockReturnValueOnce([]);
expect(shallow(<SuggestedCourses />)).toMatchSnapshot();
});
test('has 3 suggested courses', () => {
appHooks.useSuggestedCoursesData.mockReturnValueOnce(suggestedCourses);
expect(shallow(<SuggestedCourses />)).toMatchSnapshot();
});
});

View File

@@ -1,21 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
header: {
id: 'leanerDashboard.emptyCourse.suggestedCourses.header',
defaultMessage: 'Some courses you may be interested in',
description: 'Header for suggesting courses',
},
viewCourseButton: {
id: 'leanerDashboard.emptyCourse.suggestedCourses.viewCourseButton',
defaultMessage: 'View Course',
description: 'Actions button to visit the suggested course',
},
courseImageAlt: {
id: 'leanerDashboard.emptyCourse.suggestedCourses.courseImageAlt',
defaultMessage: 'Course image banner',
description: 'alt for course image',
},
});
export default messages;

View File

@@ -1,40 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyCourse snapshot 1`] = `
<div
className="p-3"
>
<div
className="empty-course-hero"
>
<Image
alt="empty course banner"
src="icon/mock/path"
/>
<h1>
<FormattedMessage
defaultMessage="Looking for a new challenge?"
description="Prompt user for new challenge"
id="EmptyCourse.lookingForChallengePrompt"
/>
</h1>
<p>
<FormattedMessage
defaultMessage="Explore our courses to add them to your dashboard."
description="Prompt user to explore more courses"
id="EmptyCourse.exploreCoursesPrompt"
/>
</p>
<Button
variant="brand"
>
<FormattedMessage
defaultMessage="Explore courses"
description="Button to explore more courses"
id="EmptyCourse.exploreCoursesButton"
/>
</Button>
</div>
<SuggestedCourses />
</div>
`;

View File

@@ -1,32 +0,0 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button,
Image,
} from '@edx/paragon';
import emptyCourseSVG from 'assets/empty-course.svg';
import SuggestedCourses from './SuggestedCourses';
import messages from './messages';
import './index.scss';
export const EmptyCourse = () => (
<div className="p-3">
<div className="empty-course-hero">
<Image src={emptyCourseSVG} alt="empty course banner" />
<h1>
<FormattedMessage {...messages.lookingForChallengePrompt} />
</h1>
<p>
<FormattedMessage {...messages.exploreCoursesPrompt} />
</p>
<Button variant="brand">
<FormattedMessage {...messages.exploreCoursesButton} />
</Button>
</div>
<SuggestedCourses />
</div>
);
export default EmptyCourse;

View File

@@ -1,12 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import EmptyCourse from '.';
jest.mock('./SuggestedCourses', () => 'SuggestedCourses');
describe('EmptyCourse', () => {
test('snapshot', () => {
expect(shallow(<EmptyCourse />)).toMatchSnapshot();
});
});

View File

@@ -7,7 +7,7 @@ exports[`WidgetSidebar snapshots default 1`] = `
<div
className="d-flex"
>
<LookingForChallengeWidget />
<RecommendationsPanel />
</div>
</div>
`;

View File

@@ -1,11 +1,11 @@
import React from 'react';
import LookingForChallengeWidget from './widgets/LookingForChallengeWidget';
import RecommendationsPanel from 'widgets/RecommendationsPanel';
export const WidgetSidebar = () => (
<div className="widget-sidebar">
<div className="d-flex">
<LookingForChallengeWidget />
<RecommendationsPanel />
</div>
</div>
);

View File

@@ -2,7 +2,7 @@ import { shallow } from 'enzyme';
import WidgetSidebar from '.';
jest.mock('./widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
describe('WidgetSidebar', () => {
describe('snapshots', () => {

View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WidgetSidebar snapshots default 1`] = `
<div
className="widget-sidebar px-2"
>
<div
className="d-flex"
>
<RecommendationsPanel />
</div>
</div>
`;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import RecommendationsPanel from 'widgets/RecommendationsPanel';
export const WidgetSidebar = () => (
<div className="widget-sidebar px-2">
<div className="d-flex">
<RecommendationsPanel />
</div>
</div>
);
export default WidgetSidebar;

View File

@@ -0,0 +1,14 @@
import { shallow } from 'enzyme';
import WidgetSidebar from '.';
jest.mock('widgets/LookingForChallengeWidget', () => 'LookingForChallengeWidget');
describe('WidgetSidebar', () => {
describe('snapshots', () => {
test('default', () => {
const wrapper = shallow(<WidgetSidebar />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LookingForChallengeWidget snapshots default 1`] = `
<Card
id="looking-for-challenge-widget"
orientation="horizontal"
>
<Card.ImageCap
src="icon/mock/path"
srcAlt="course side widget"
/>
<Card.Body
className="m-auto pr-2"
>
<h4>
Looking for a new challenge?
</h4>
<h5>
<Hyperlink
className="d-flex align-items-center"
destination="course-search-url"
variant="brand"
>
<format-message-function
message={
Object {
"defaultMessage": "Find a course {arrow}",
"description": "Button to explore more courses",
"id": "WidgetSidebar.findCoursesButton",
}
}
values={
Object {
"arrow": <Icon
className="mx-1"
/>,
}
}
/>
</Hyperlink>
</h5>
</Card.Body>
</Card>
`;

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Card, Hyperlink, Icon } from '@edx/paragon';
import { ArrowForward } from '@edx/paragon/icons';
import { hooks } from 'data/redux';
import moreCoursesSVG from 'assets/more-courses-sidewidget.svg';
import messages from './messages';
import './index.scss';
export const arrowIcon = (<Icon className="mx-1" src={ArrowForward} />);
export const LookingForChallengeWidget = () => {
const { formatMessage } = useIntl();
const { courseSearchUrl } = hooks.usePlatformSettingsData();
return (
<Card orientation="horizontal" id="looking-for-challenge-widget">
<Card.ImageCap
src={moreCoursesSVG}
srcAlt="course side widget"
/>
<Card.Body className="m-auto pr-2">
<h4>
{formatMessage(messages.lookingForChallengePrompt)}
</h4>
<h5>
<Hyperlink variant="brand" destination={courseSearchUrl} className="d-flex align-items-center">
{formatMessage(messages.findCoursesButton, { arrow: arrowIcon })}
</Hyperlink>
</h5>
</Card.Body>
</Card>
);
};
export default LookingForChallengeWidget;

View File

@@ -1,6 +0,0 @@
#looking-for-challenge-widget {
.pgn__card-wrapper-image-cap {
overflow: visible;
min-width: auto;
}
}

View File

@@ -1,20 +0,0 @@
import { shallow } from 'enzyme';
import LookingForChallengeWidget from '.';
jest.mock('data/redux', () => ({
hooks: {
usePlatformSettingsData: () => ({
courseSearchUrl: 'course-search-url',
}),
},
}));
describe('LookingForChallengeWidget', () => {
describe('snapshots', () => {
test('default', () => {
const wrapper = shallow(<LookingForChallengeWidget />);
expect(wrapper).toMatchSnapshot();
});
});
});

View File

@@ -1,16 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
lookingForChallengePrompt: {
id: 'WidgetSidebar.lookingForChallengePrompt',
defaultMessage: 'Looking for a new challenge?',
description: 'Prompt user for new challenge',
},
findCoursesButton: {
id: 'WidgetSidebar.findCoursesButton',
defaultMessage: 'Find a course {arrow}',
description: 'Button to explore more courses',
},
});
export default messages;