feat: local pagination/sort and api updates

This commit is contained in:
Ben Warzeski
2022-08-09 14:08:42 -04:00
parent 6679e612fe
commit d736046ba5
92 changed files with 1614 additions and 1241 deletions

View File

@@ -12,6 +12,9 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
@import "~@edx/frontend-component-footer/dist/_footer";
#root {
input[type=checkbox] {
transform: none;
}
display: flex;
flex-direction: column;
min-height: 100vh;

View File

@@ -2,7 +2,7 @@
exports[`CourseCard component snapshot 1`] = `
<div
className="mb-3 course-card"
className="mb-4.5 course-card"
data-testid="CourseCard"
>
<Card
@@ -24,7 +24,7 @@ exports[`CourseCard component snapshot 1`] = `
<Card.Header
actions={
<CourseCardMenu
courseNumber="test-course-number"
cardId="test-card-id"
/>
}
title={
@@ -37,19 +37,19 @@ exports[`CourseCard component snapshot 1`] = `
/>
<Card.Section>
<CourseCardDetails
courseNumber="test-course-number"
cardId="test-card-id"
/>
</Card.Section>
<Card.Footer
orientation="vertical"
textElement={
<RelatedProgramsBadge
courseNumber="test-course-number"
cardId="test-card-id"
/>
}
>
<CourseCardActions
courseNumber="test-course-number"
cardId="test-card-id"
/>
</Card.Footer>
</Card.Body>
@@ -59,13 +59,13 @@ exports[`CourseCard component snapshot 1`] = `
data-testid="CourseCardBanners"
>
<CourseBanner
courseNumber="test-course-number"
/>
<CertificateBanner
courseNumber="test-course-number"
cardId="test-card-id"
/>
<EntitlementBanner
courseNumber="test-course-number"
cardId="test-card-id"
/>
<CertificateBanner
cardId="test-card-id"
/>
</div>
</div>

View File

@@ -11,15 +11,15 @@ import Banner from 'components/Banner';
import messages from './messages';
export const CertificateBanner = ({ courseNumber }) => {
const certificate = appHooks.useCardCertificateData(courseNumber);
export const CertificateBanner = ({ cardId }) => {
const certificate = appHooks.useCardCertificateData(cardId);
const {
isAudit,
isVerified,
hasFinished,
} = appHooks.useCardEnrollmentData(courseNumber);
const { isPassing } = appHooks.useCardGradeData(courseNumber);
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(courseNumber);
} = appHooks.useCardEnrollmentData(cardId);
const { isPassing } = appHooks.useCardGradeData(cardId);
const { minPassingGrade, progressUrl } = appHooks.useCardCourseRunData(cardId);
const { supportEmail, billingEmail } = appHooks.usePlatformSettingsData();
const { formatMessage } = useIntl();
@@ -96,7 +96,7 @@ export const CertificateBanner = ({ courseNumber }) => {
return null;
};
CertificateBanner.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
export default CertificateBanner;

View File

@@ -16,7 +16,7 @@ jest.mock('data/redux', () => ({
},
}));
const courseNumber = 'my-test-course-number';
const cardId = 'my-test-course-number';
let el;
@@ -50,15 +50,15 @@ const render = (overrides = {}) => {
...enrollmentData,
...enrollment,
});
el = shallow(<CourseBanner courseNumber={courseNumber} />);
el = shallow(<CourseBanner cardId={cardId} />);
};
describe('CourseBanner', () => {
it('initializes data with course number from enrollment, course and course run data', () => {
render();
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
test('no display if learner is verified', () => {
render({ enrollment: { isVerified: true } });

View File

@@ -8,14 +8,14 @@ import { hooks as appHooks } from 'data/redux';
import Banner from 'components/Banner';
import messages from './messages';
export const CourseBanner = ({ courseNumber }) => {
export const CourseBanner = ({ cardId }) => {
const {
isVerified,
isAuditAccessExpired,
canUpgrade,
} = appHooks.useCardEnrollmentData(courseNumber);
const courseRun = appHooks.useCardCourseRunData(courseNumber);
const course = appHooks.useCardCourseData(courseNumber);
} = appHooks.useCardEnrollmentData(cardId);
const courseRun = appHooks.useCardCourseRunData(cardId);
const course = appHooks.useCardCourseData(cardId);
const { formatMessage } = useIntl();
if (isVerified) { return null; }
@@ -52,7 +52,7 @@ export const CourseBanner = ({ courseNumber }) => {
return null;
};
CourseBanner.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
export default CourseBanner;

View File

@@ -16,7 +16,7 @@ jest.mock('data/redux', () => ({
},
}));
const courseNumber = 'my-test-course-number';
const cardId = 'my-test-course-number';
let el;
@@ -50,15 +50,15 @@ const render = (overrides = {}) => {
...enrollmentData,
...enrollment,
});
el = shallow(<CourseBanner courseNumber={courseNumber} />);
el = shallow(<CourseBanner cardId={cardId} />);
};
describe('CourseBanner', () => {
it('initializes data with course number from enrollment, course and course run data', () => {
render();
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
test('no display if learner is verified', () => {
render({ enrollment: { isVerified: true } });

View File

@@ -7,10 +7,10 @@ import { hooks as appHooks } from 'data/redux';
import { dateFormatter } from 'utils';
import Banner from 'components/Banner';
import useSelectSession from 'containers/SelectSession/hooks';
import useSelectSessionModalData from 'containers/SelectSessionModal/hooks';
import messages from './messages';
export const EntitlementBanner = ({ courseNumber }) => {
export const EntitlementBanner = ({ cardId }) => {
const {
isEntitlement,
hasSessions,
@@ -18,9 +18,9 @@ export const EntitlementBanner = ({ courseNumber }) => {
changeDeadline,
showExpirationWarning,
isExpired,
} = appHooks.useCardEntitlementsData(courseNumber);
} = appHooks.useCardEntitlementsData(cardId);
const { supportEmail } = appHooks.usePlatformSettingsData();
const { openSessionModal } = useSelectSession({ courseNumber });
const { openSessionModal } = useSelectSessionModalData({ cardId });
const { formatDate, formatMessage } = useIntl();
if (!isEntitlement) {
@@ -42,7 +42,7 @@ export const EntitlementBanner = ({ courseNumber }) => {
{formatMessage(messages.entitlementsExpiringSoon, {
changeDeadline: dateFormatter(formatDate, changeDeadline),
selectSessionButton: (
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal}>
<Button variant="link" size="inline" className="m-0 p-0" onClick={openSessionModal(cardId)}>
{formatMessage(messages.selectSession)}
</Button>
),
@@ -60,7 +60,7 @@ export const EntitlementBanner = ({ courseNumber }) => {
return null;
};
EntitlementBanner.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
export default EntitlementBanner;

View File

@@ -11,11 +11,11 @@ jest.mock('data/redux', () => ({
useCardEntitlementsData: jest.fn(),
},
}));
jest.mock('containers/SelectSession/hooks', () => () => ({
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'),
jest.mock('containers/SelectSessionModal/hooks', () => () => ({
openSessionModal: (cardId) => jest.fn().mockName(`useSelectSessionModalData.openSessionModal(${cardId})`),
}));
const courseNumber = 'my-test-course-number';
const cardId = 'my-test-course-number';
let el;
@@ -32,13 +32,13 @@ const render = (overrides = {}) => {
const { entitlements = {} } = overrides;
appHooks.useCardEntitlementsData.mockReturnValueOnce({ ...entitlementsData, ...entitlements });
appHooks.usePlatformSettingsData.mockReturnValueOnce(platformData);
el = shallow(<EntitlementBanner courseNumber={courseNumber} />);
el = shallow(<EntitlementBanner cardId={cardId} />);
};
describe('EntitlementBanner', () => {
it('initializes data with course number from entitlements', () => {
render();
expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(cardId);
});
test('no display if not an entitlement', () => {
render({ entitlements: { isEntitlement: false } });

View File

@@ -15,7 +15,7 @@ exports[`EntitlementBanner snapshot: expiration warning 1`] = `
"changeDeadline": "11/11/2022",
"selectSessionButton": <Button
className="m-0 p-0"
onClick={[MockFunction useSelectSession.openSessionModal]}
onClick={[MockFunction useSelectSessionModalData.openSessionModal(my-test-course-number)]}
size="inline"
variant="link"
>

View File

@@ -5,7 +5,7 @@ exports[`CourseCard Actions component does not render secondary button if null i
data-test-id="CourseCardActions"
>
<Button
courseNumber="test-course-number"
cardId="test-course-number"
prop1="primary-prop1"
prop2="primary-prop2"
>
@@ -19,14 +19,14 @@ exports[`CourseCard Actions component loads primary and secondary button props f
data-test-id="CourseCardActions"
>
<Button
courseNumber="test-course-number"
cardId="test-course-number"
prop1="primary-prop1"
prop2="primary-prop2"
>
primary-children
</Button>
<Button
courseNumber="test-course-number"
cardId="test-course-number"
prop1="primary-prop1"
prop2="primary-prop2"
>

View File

@@ -2,18 +2,18 @@ import { Locked } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import useSelectSession from 'containers/SelectSession/hooks';
import useSelectSessionModalData from 'containers/SelectSessionModal/hooks';
import messages from './messages';
export const useCardActionData = ({ courseNumber }) => {
export const useCardActionData = ({ cardId }) => {
const { formatMessage } = useIntl();
const {
canUpgrade,
isAudit,
isAuditAccessExpired,
isVerified,
} = appHooks.useCardEnrollmentData(courseNumber);
const { isPending, isArchived } = appHooks.useCardCourseRunData(courseNumber);
} = appHooks.useCardEnrollmentData(cardId);
const { isPending, isArchived } = appHooks.useCardCourseRunData(cardId);
const {
isEntitlement,
canViewCourse,
@@ -21,8 +21,8 @@ export const useCardActionData = ({ courseNumber }) => {
isExpired,
canChange,
hasSessions,
} = appHooks.useCardEntitlementsData(courseNumber);
const { openSessionModal } = useSelectSession({ courseNumber });
} = appHooks.useCardEntitlementsData(cardId);
const { openSessionModal } = useSelectSessionModalData();
let primary;
let secondary = null;
@@ -31,7 +31,7 @@ export const useCardActionData = ({ courseNumber }) => {
primary = {
children: formatMessage(messages.selectSession),
disabled: !(canChange && hasSessions),
onClick: openSessionModal,
onClick: openSessionModal(cardId),
};
} else {
primary = {

View File

@@ -15,11 +15,11 @@ jest.mock('data/redux', () => ({
},
}));
jest.mock('containers/SelectSession/hooks', () => () => ({
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'),
jest.mock('containers/SelectSessionModal/hooks', () => () => ({
openSessionModal: (cardId) => jest.fn().mockName(`useSelectSession.openSessionModal(${cardId})`),
}));
const courseNumber = 'my-test-course-number';
const cardId = 'my-test-course-number';
const enrollmentData = {
canUpgrade: false,
@@ -48,7 +48,7 @@ describe('CourseCardActions hooks', () => {
appHooks.useCardCourseRunData.mockReturnValueOnce({ ...courseRunData, ...courseRun });
appHooks.useCardEnrollmentData.mockReturnValueOnce({ ...enrollmentData, ...enrollment });
appHooks.useCardEntitlementsData.mockReturnValueOnce({ ...entitlementData, ...entitlement });
out = hooks.useCardActionData({ courseNumber });
out = hooks.useCardActionData({ cardId });
};
describe('entitlement', () => {
describe('secondary action', () => {

View File

@@ -5,8 +5,8 @@ import { Button } from '@edx/paragon';
import useCardActionData from './hooks';
export const CourseCardActions = ({ courseNumber }) => {
const { primary, secondary } = useCardActionData({ courseNumber });
export const CourseCardActions = ({ cardId }) => {
const { primary, secondary } = useCardActionData({ cardId });
return (
<div data-test-id="CourseCardActions">
{(secondary !== null) && (
@@ -17,7 +17,7 @@ export const CourseCardActions = ({ courseNumber }) => {
);
};
CourseCardActions.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
export default CourseCardActions;

View File

@@ -9,7 +9,7 @@ jest.mock('./hooks', () => ({
default: jest.fn(),
}));
const courseNumber = 'test-course-number';
const cardId = 'test-course-number';
describe('CourseCard Actions component', () => {
it('loads primary and secondary button props from hook', () => {
@@ -18,17 +18,17 @@ describe('CourseCard Actions component', () => {
prop1: 'primary-prop1',
prop2: 'primary-prop2',
children: 'primary-children',
courseNumber: args.courseNumber,
cardId: args.cardId,
},
secondary: {
prop1: 'primary-prop1',
prop2: 'primary-prop2',
children: 'primary-children',
courseNumber: args.courseNumber,
cardId: args.cardId,
},
});
hooks.mockImplementationOnce(mockHook);
expect(shallow(<CourseCardActions courseNumber={courseNumber} />)).toMatchSnapshot();
expect(shallow(<CourseCardActions cardId={cardId} />)).toMatchSnapshot();
});
it('does not render secondary button if null is returned for secondary props', () => {
const mockHook = (args) => ({
@@ -36,11 +36,11 @@ describe('CourseCard Actions component', () => {
prop1: 'primary-prop1',
prop2: 'primary-prop2',
children: 'primary-children',
courseNumber: args.courseNumber,
cardId: args.cardId,
},
secondary: null,
});
hooks.mockImplementationOnce(mockHook);
expect(shallow(<CourseCardActions courseNumber={courseNumber} />)).toMatchSnapshot();
expect(shallow(<CourseCardActions cardId={cardId} />)).toMatchSnapshot();
});
});

View File

@@ -6,7 +6,6 @@ exports[`CourseCard Details component does not have change session button on reg
>
provider-name
test-course-number
acess-message
</span>
@@ -18,7 +17,6 @@ exports[`CourseCard Details component has change session button on entitlement c
>
provider-name
test-course-number
access-message
@@ -28,15 +26,7 @@ exports[`CourseCard Details component has change session button on entitlement c
size="inline"
variant="link"
>
<div
message={
Object {
"defaultMessage": "Change or leave session",
"description": "Button for trigger change or leave session for entitlement course",
"id": "learner-dash.courseCard.CourseCardDetails.changeOrLeaveSessionButton",
}
}
/>
Change or leave session
</Button>
</span>
`;

View File

@@ -1,51 +1,55 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import useSelectSession from 'containers/SelectSession/hooks';
import useSelectSessionModalData from 'containers/SelectSessionModal/hooks';
import * as module from './hooks';
import messages from './messages';
export const useAccessMessage = ({ courseNumber }) => {
export const useAccessMessage = ({ cardId }) => {
const { formatMessage, formatDate } = useIntl();
const {
accessExpirationDate,
isAudit,
isAuditAccessExpired,
} = appHooks.useCardEnrollmentData(courseNumber);
const { isArchived, endDate } = appHooks.useCardCourseRunData(courseNumber);
if (isAudit) {
const enrollment = appHooks.useCardEnrollmentData(cardId);
const courseRun = appHooks.useCardCourseRunData(cardId);
if (enrollment.isEnrolled) {
if (enrollment.isAudit) {
const {
accessExpirationDate,
isAuditAccessExpired,
} = enrollment;
return formatMessage(
isAuditAccessExpired ? messages.accessExpired : messages.accessExpires,
{ accessExpirationDate: formatDate(accessExpirationDate) },
);
}
const { isArchived, endDate } = courseRun;
return formatMessage(
isAuditAccessExpired ? messages.accessExpired : messages.accessExpires,
{ accessExpirationDate: formatDate(accessExpirationDate) },
isArchived ? messages.courseEnded : messages.courseEnds,
{ endDate: formatDate(endDate) },
);
}
return formatMessage(
isArchived ? messages.courseEnded : messages.courseEnds,
{ endDate: formatDate(endDate) },
);
return null;
};
export const useCardDetailsData = ({ courseNumber }) => {
export const useCardDetailsData = ({ cardId }) => {
const { formatMessage } = useIntl();
const providerName = appHooks.useCardProviderData(courseNumber).name;
const providerName = appHooks.useCardProviderData(cardId).name;
const { courseNumber } = appHooks.useCardCourseData(cardId);
const {
isEntitlement,
isFulfilled,
canChange,
} = appHooks.useCardEntitlementsData(courseNumber);
} = appHooks.useCardEntitlementsData(cardId);
const { openSessionModalWithLeaveOption: openSessionModal } = useSelectSession({ courseNumber });
const { openSessionModal } = useSelectSessionModalData();
return {
providerName: providerName || formatMessage(messages.unknownProviderName),
accessMessage: module.useAccessMessage({ courseNumber }),
accessMessage: module.useAccessMessage({ cardId }),
isEntitlement,
isFulfilled,
canChange,
openSessionModal,
openSessionModal: openSessionModal(cardId),
formatMessage,
courseNumber,
};
};

View File

@@ -8,22 +8,24 @@ import messages from './messages';
jest.mock('data/redux', () => ({
hooks: {
useCardCourseData: jest.fn(),
useCardCourseRunData: jest.fn(),
useCardEnrollmentData: jest.fn(),
useCardEntitlementsData: jest.fn(),
useCardProviderData: jest.fn(),
},
}));
jest.mock('containers/SelectSession/hooks', () => () => ({
openSessionModalWithLeaveOption: jest.fn().mockName('useSelectSession.openSessionModalWithLeaveOptionFunction'),
jest.mock('containers/SelectSessionModal/hooks', () => () => ({
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModalFunction'),
}));
const courseNumber = 'my-test-course-number';
const cardId = 'my-test-card-id';
const courseNumber = 'test-course-number';
const useAccessMessage = 'test-access-message';
const mockAccessMessage = (args) => ({ courseNumber: args.coursenumber, useAccessMessage });
const mockAccessMessage = (args) => ({ cardId: args.cardId, useAccessMessage });
const hookKeys = keyStore(hooks);
describe('CourseCard hooks', () => {
describe('CourseCardDetails hooks', () => {
let out;
const { formatMessage, formatDate } = useIntl();
beforeEach(() => {
@@ -53,7 +55,8 @@ describe('CourseCard hooks', () => {
...entitlementData,
...entitlement,
});
out = hooks.useCardDetailsData({ courseNumber });
appHooks.useCardCourseData.mockReturnValueOnce({ courseNumber });
out = hooks.useCardDetailsData({ cardId });
};
beforeEach(() => {
runHook({});
@@ -61,8 +64,8 @@ describe('CourseCard hooks', () => {
it('forwards formatMessage from useIntl', () => {
expect(out.formatMessage).toEqual(formatMessage);
});
it('forwards useAccessMessage output, called with courseNumber', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ courseNumber }));
it('forwards useAccessMessage output, called with cardId', () => {
expect(out.accessMessage).toEqual(mockAccessMessage({ cardId }));
});
it('forwards provider name if it exists, else formatted unknown provider name', () => {
expect(out.providerName).toEqual(providerData.name);
@@ -70,15 +73,17 @@ describe('CourseCard hooks', () => {
expect(out.providerName).toEqual(formatMessage(messages.unknownProviderName));
});
});
describe('useAccessMessage', () => {
const enrollmentData = {
isEnrolled: true,
accessExpirationDate: 'test-expiration-date',
isAudit: false,
isAuditAccessExpired: false,
};
const courseRunData = {
isFinished: false,
endDate: 'test-end-date',
endDate: '10/20/1000',
};
const runHook = ({ enrollment = {}, courseRun = {} }) => {
appHooks.useCardCourseRunData.mockReturnValueOnce({
@@ -89,13 +94,15 @@ describe('CourseCard hooks', () => {
...enrollmentData,
...enrollment,
});
out = hooks.useAccessMessage({ courseNumber });
out = hooks.useAccessMessage({ cardId });
};
it('loads data from enrollment and course run data based on course number', () => {
runHook({});
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardCourseRunData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
describe('if audit, and expired', () => {
it('returns accessExpired message with accessExpirationDate from cardData', () => {
runHook({ enrollment: { isAudit: true, isAuditAccessExpired: true } });

View File

@@ -7,7 +7,7 @@ import useCardDetailsData from './hooks';
import messages from './messages';
export const CourseCardDetails = ({ courseNumber }) => {
export const CourseCardDetails = ({ cardId }) => {
const {
providerName,
accessMessage,
@@ -16,11 +16,18 @@ export const CourseCardDetails = ({ courseNumber }) => {
canChange,
openSessionModal,
formatMessage,
} = useCardDetailsData({ courseNumber });
courseNumber,
} = useCardDetailsData({ cardId });
return (
<span data-testid="CourseCardDetails">
{providerName} {courseNumber} {accessMessage}
{providerName} {courseNumber}
{!(isEntitlement && !isFulfilled) && (
<>
{' • '}
{accessMessage}
</>
)}
{isEntitlement && isFulfilled && canChange ? (
<>
{' • '}
@@ -34,7 +41,7 @@ export const CourseCardDetails = ({ courseNumber }) => {
};
CourseCardDetails.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
CourseCardDetails.defaultProps = {};

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from 'testUtils';
import CourseCardDetails from '.';
import hooks from './hooks';
@@ -10,7 +12,7 @@ jest.mock('./hooks', () => ({
default: jest.fn(),
}));
const courseNumber = 'test-course-number';
const cardId = 'test-card-id';
describe('CourseCard Details component', () => {
it('has change session button on entitlement course', () => {
@@ -18,14 +20,14 @@ describe('CourseCard Details component', () => {
providerName: 'provider-name',
accessMessage: 'access-message',
openSessionModal: jest.fn().mockName('useSelectSession.openSessionModal'),
formatMessage: (message, values) => <div {...{ message, values }} />,
formatMessage,
isEntitlement: true,
isFulfilled: true,
canChange: true,
...args,
});
hooks.mockImplementationOnce(mockHook({ isEntitlement: true }));
const el = shallow(<CourseCardDetails courseNumber={courseNumber} />);
const el = shallow(<CourseCardDetails cardId={cardId} />);
expect(el).toMatchSnapshot();
// it has 3 separator, 4 column
expect(el.text().match(/•/g)).toHaveLength(3);
@@ -43,7 +45,7 @@ describe('CourseCard Details component', () => {
...args,
});
hooks.mockImplementationOnce(mockHook({ isEntitlement: false }));
const el = shallow(<CourseCardDetails courseNumber={courseNumber} />);
const el = shallow(<CourseCardDetails cardId={cardId} />);
expect(el).toMatchSnapshot();
// it has 2 separator, 3 column
expect(el.text().match(/•/g)).toHaveLength(2);

View File

@@ -8,7 +8,7 @@ import EmailSettingsModal from 'containers/EmailSettingsModal';
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
import useCourseCardMenuData from './hooks';
export const CourseCardMenu = ({ courseNumber }) => {
export const CourseCardMenu = ({ cardId }) => {
const {
emailSettingsModal,
unenrollModal,
@@ -34,18 +34,18 @@ export const CourseCardMenu = ({ courseNumber }) => {
<UnenrollConfirmModal
show={unenrollModal.isVisible}
closeModal={unenrollModal.hide}
courseNumber={courseNumber}
cardId={cardId}
/>
<EmailSettingsModal
show={emailSettingsModal.isVisible}
closeModal={emailSettingsModal.hide}
courseNumber={courseNumber}
cardId={cardId}
/>
</>
);
};
CourseCardMenu.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
export default CourseCardMenu;

View File

@@ -11,8 +11,8 @@ exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = `
useRelatedProgramsBadge.programsMessage
</Button>
<RelatedProgramsModal
cardId="test-course-number"
closeModal={[MockFunction useRelatedProgramsBadge.closeModal]}
courseNumber="test-course-number"
isOpen={true}
/>
</Fragment>

View File

@@ -11,10 +11,10 @@ export const state = StrictDict({
isOpen: (val) => React.useState(val), // eslint-disable-line
});
export const useRelatedProgramsBadgeData = ({ courseNumber }) => {
export const useRelatedProgramsBadgeData = ({ cardId }) => {
const [isOpen, setIsOpen] = module.state.isOpen(false);
const { formatMessage } = useIntl();
const numPrograms = appHooks.useCardRelatedProgramsData(courseNumber).length;
const numPrograms = appHooks.useCardRelatedProgramsData(cardId).length;
let programsMessage = '';
if (numPrograms) {
programsMessage = formatMessage(

View File

@@ -12,7 +12,7 @@ jest.mock('data/redux', () => ({
},
}));
const courseNumber = 'my-test-course-number';
const cardId = 'test-card-id';
const state = new MockUseState(hooks);
const numPrograms = 27;
@@ -33,7 +33,7 @@ describe('RelatedProgramsBadge hooks', () => {
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({
length: numPrograms,
});
out = hooks.useRelatedProgramsBadgeData({ courseNumber });
out = hooks.useRelatedProgramsBadgeData({ cardId });
});
afterEach(state.restore);
@@ -55,17 +55,17 @@ describe('RelatedProgramsBadge hooks', () => {
expect(out.isOpen).toEqual(state.stateVals.isOpen);
});
test('forwards numPrograms from relatedPrograms.length for the courseNumber', () => {
test('forwards numPrograms from relatedPrograms.length for the cardId', () => {
expect(out.numPrograms).toEqual(numPrograms);
});
test('returns empty programsMessage if no programs', () => {
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 0 });
out = hooks.useRelatedProgramsBadgeData({ courseNumber });
out = hooks.useRelatedProgramsBadgeData({ cardId });
expect(out.programsMessage).toEqual('');
});
test('returns badgeLabelSingular programsMessage if 1 programs', () => {
appHooks.useCardRelatedProgramsData.mockReturnValueOnce({ length: 1 });
out = hooks.useRelatedProgramsBadgeData({ courseNumber });
out = hooks.useRelatedProgramsBadgeData({ cardId });
expect(out.programsMessage).toEqual(formatMessage(
messages.badgeLabelSingular,
{ numPrograms: 1 },

View File

@@ -8,14 +8,14 @@ import { Program } from '@edx/paragon/icons';
import RelatedProgramsBadgeModal from 'containers/RelatedProgramsModal';
import useRelatedProgramsBadgeData from './hooks';
export const RelatedProgramsBadge = ({ courseNumber }) => {
export const RelatedProgramsBadge = ({ cardId }) => {
const {
isOpen,
openModal,
closeModal,
numPrograms,
programsMessage,
} = useRelatedProgramsBadgeData({ courseNumber });
} = useRelatedProgramsBadgeData({ cardId });
return (numPrograms > 0) && (
<>
<Button
@@ -27,12 +27,12 @@ export const RelatedProgramsBadge = ({ courseNumber }) => {
>
{programsMessage}
</Button>
<RelatedProgramsBadgeModal {...{ isOpen, closeModal, courseNumber }} />
<RelatedProgramsBadgeModal {...{ isOpen, closeModal, cardId }} />
</>
);
};
RelatedProgramsBadge.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
export default RelatedProgramsBadge;

View File

@@ -15,16 +15,16 @@ const hookProps = {
programsMessage: 'useRelatedProgramsBadge.programsMessage',
};
const courseNumber = 'test-course-number';
const cardId = 'test-course-number';
describe('RelatedProgramsBadge component', () => {
test('empty render: no programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce({ ...hookProps, numPrograms: 0 });
const el = shallow(<RelatedProgramsBadge courseNumber={courseNumber} />);
const el = shallow(<RelatedProgramsBadge cardId={cardId} />);
expect(el.isEmptyRender()).toEqual(true);
});
test('snapshot: 3 programs', () => {
useRelatedProgramsBadge.mockReturnValueOnce(hookProps);
expect(shallow(<RelatedProgramsBadge courseNumber={courseNumber} />)).toMatchSnapshot();
expect(shallow(<RelatedProgramsBadge cardId={cardId} />)).toMatchSnapshot();
});
});

View File

@@ -1,11 +1,13 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
export const useCardData = ({ courseNumber }) => {
export const useCardData = ({ cardId }) => {
const { formatMessage } = useIntl();
const { title, bannerUrl } = appHooks.useCardCourseData(courseNumber);
const { title, bannerUrl } = appHooks.useCardCourseData(cardId);
const { isEnrolled } = appHooks.useCardEnrollmentData(cardId);
return {
isEnrolled,
title,
bannerUrl,
formatMessage,

View File

@@ -7,10 +7,11 @@ import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
useCardCourseData: jest.fn(),
useCardEnrollmentData: jest.fn(),
},
}));
const courseNumber = 'my-test-course-number';
const cardId = 'my-test-course-number';
describe('CourseCard hooks', () => {
let out;
@@ -29,7 +30,8 @@ describe('CourseCard hooks', () => {
...courseData,
...course,
});
out = hooks.useCardData({ courseNumber });
appHooks.useCardEnrollmentData.mockReturnValue({ isEnrolled: 'test-is-enrolled' });
out = hooks.useCardData({ cardId });
};
beforeEach(() => {
runHook({});
@@ -38,7 +40,7 @@ describe('CourseCard hooks', () => {
expect(out.formatMessage).toEqual(formatMessage);
});
it('passes course title and banner URL form course data', () => {
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(out.title).toEqual(courseData.title);
expect(out.bannerUrl).toEqual(courseData.bannerUrl);
});

View File

@@ -17,14 +17,15 @@ import CourseCardActions from './components/CourseCardActions';
import messages from './messages';
import CourseCardDetails from './components/CourseCardDetails';
export const CourseCard = ({ courseNumber }) => {
export const CourseCard = ({ cardId }) => {
const {
isEnrolled,
title,
bannerUrl,
formatMessage,
} = useCardData({ courseNumber });
} = useCardData({ cardId });
return (
<div className="mb-3 course-card" data-testid="CourseCard">
<div className="mb-4.5 course-card" data-testid="CourseCard">
<Card orientation="horizontal">
<Card.ImageCap
src={bannerUrl}
@@ -33,30 +34,30 @@ export const CourseCard = ({ courseNumber }) => {
<Card.Body>
<Card.Header
title={<span data-testid="CourseCardTitle">{title}</span>}
actions={<CourseCardMenu courseNumber={courseNumber} />}
actions={<CourseCardMenu cardId={cardId} />}
/>
<Card.Section>
<CourseCardDetails courseNumber={courseNumber} />
<CourseCardDetails cardId={cardId} />
</Card.Section>
<Card.Footer
orientation="vertical"
textElement={<RelatedProgramsBadge courseNumber={courseNumber} />}
textElement={<RelatedProgramsBadge cardId={cardId} />}
>
<CourseCardActions courseNumber={courseNumber} />
<CourseCardActions cardId={cardId} />
</Card.Footer>
</Card.Body>
</Card>
<div className="course-card-banners" data-testid="CourseCardBanners">
<CourseBanner courseNumber={courseNumber} />
<CertificateBanner courseNumber={courseNumber} />
<EntitlementBanner courseNumber={courseNumber} />
<CourseBanner cardId={cardId} />
<EntitlementBanner cardId={cardId} />
{isEnrolled && <CertificateBanner cardId={cardId} />}
</div>
</div>
);
};
CourseCard.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
CourseCard.defaultProps = {};

View File

@@ -23,14 +23,18 @@ const dataProps = {
title: 'hooks.title',
bannerUrl: 'hooks.bannerUrl',
formatMessage: jest.fn(msg => ({ formatted: msg })),
isEnrolled: true,
};
const courseNumber = 'test-course-number';
const cardId = 'test-card-id';
describe('CourseCard component', () => {
test('snapshot', () => {
hooks.mockReturnValueOnce(dataProps);
expect(shallow(<CourseCard courseNumber={courseNumber} />)).toMatchSnapshot();
expect(hooks).toHaveBeenCalledWith({ courseNumber });
expect(shallow(<CourseCard cardId={cardId} />)).toMatchSnapshot();
expect(hooks).toHaveBeenCalledWith({ cardId });
});
test('snapshot: not enrolled (no certificate card)', () => {
hooks.mockReturnValueOnce({ ...dataProps, isEnrolled: true });
});
});

View File

@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Chip } from '@edx/paragon';
import { CloseSmall } from '@edx/paragon/icons';
import messages from './messages';
import './index.scss';
export const ActiveCourseFilters = ({
filters,
setFilters,
handleRemoveFilter,
}) => {
const { formatMessage } = useIntl();
return (
<div id="course-list-active-filters">
{filters.map(filter => (
<Chip
variant="primary"
key={filter}
iconAfter={CloseSmall}
onClick={handleRemoveFilter(filter)}
>
{formatMessage(messages[filter])}
</Chip>
))}
<Button variant="link" onClick={setFilters.clear}>
{formatMessage(messages.clearAll)}
</Button>
</div>
);
};
ActiveCourseFilters.propTypes = {
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
setFilters: PropTypes.shape({
remove: PropTypes.func,
clear: PropTypes.func,
}).isRequired,
handleRemoveFilter: PropTypes.func.isRequired,
};
export default ActiveCourseFilters;

View File

@@ -0,0 +1,82 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
Button,
ModalPopup,
} from '@edx/paragon';
import { Tune } from '@edx/paragon/icons';
import FilterForm from './components/FilterForm';
import SortForm from './components/SortForm';
import useCourseFilterControlsData from './hooks';
import messages from './messages';
import './index.scss';
export const CourseFilterControls = ({
sortBy,
setSortBy,
filters,
setFilters,
}) => {
const { formatMessage } = useIntl();
const {
isOpen,
open,
close,
target,
setTarget,
handleFilterChange,
handleSortChange,
} = useCourseFilterControlsData({
setFilters,
setSortBy,
});
return (
<div id="course-filter-controls">
<Button
ref={setTarget}
variant="outline-primary"
iconBefore={Tune}
onClick={open}
>
{formatMessage(messages.refine)}
</Button>
<ModalPopup
positionRef={target}
isOpen={isOpen}
onClose={close}
placement="bottom-end"
>
<Form>
<div
id="course-filter-controls-card"
className="bg-white p-3 rounded shadow d-flex flex-row"
>
<div className="filter-form-col">
<FilterForm {...{ filters, handleFilterChange }} />
</div>
<hr className="h-100 bg-primary-200 m-1" />
<div className="filter-form-col text-left m-1">
<SortForm {...{ sortBy, handleSortChange }} />
</div>
</div>
</Form>
</ModalPopup>
</div>
);
};
CourseFilterControls.propTypes = {
sortBy: PropTypes.string.isRequired,
setSortBy: PropTypes.func.isRequired,
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
setFilters: PropTypes.shape({
add: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
}).isRequired,
};
export default CourseFilterControls;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@edx/paragon';
import messages from '../messages';
export const Checkbox = ({ filterKey }) => {
const { formatMessage } = useIntl();
return (
<Form.Checkbox className="py-2" value={filterKey}>
{formatMessage(messages[filterKey])}
</Form.Checkbox>
);
};
Checkbox.propTypes = {
filterKey: PropTypes.string.isRequired,
};
export default Checkbox;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FilterKeys } from 'data/constants/app';
import { Form } from '@edx/paragon';
import Checkbox from './Checkbox';
export const FilterForm = ({
filters,
handleFilterChange,
}) => (
<Form.Group>
<div className="filter-form-heading mb-1">Course Status</div>
<Form.CheckboxSet
name="course-status-filters"
onChange={handleFilterChange}
values={filters}
>
<Checkbox {...{ filterKey: FilterKeys.inProgress }} />
<Checkbox {...{ filterKey: FilterKeys.notStarted }} />
<Checkbox {...{ filterKey: FilterKeys.done }} />
<Checkbox {...{ filterKey: FilterKeys.notEnrolled }} />
<Checkbox {...{ filterKey: FilterKeys.upgraded }} />
</Form.CheckboxSet>
</Form.Group>
);
FilterForm.propTypes = {
filters: PropTypes.arrayOf(PropTypes.string).isRequired,
handleFilterChange: PropTypes.func.isRequired,
};
export default FilterForm;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import { SortKeys } from 'data/constants/app';
import { Form } from '@edx/paragon';
import messages from '../messages';
export const SortForm = ({
handleSortChange,
sortBy,
}) => {
const { formatMessage } = useIntl();
return (
<>
<div className="filter-form-heading mb-1">{formatMessage(messages.sort)}</div>
<Form.RadioSet
name="sort"
onChange={handleSortChange}
value={sortBy}
>
<Form.Radio className="py-2" value={SortKeys.enrolled}>
{formatMessage(messages.sortLastEnrolled)}
</Form.Radio>
<Form.Radio className="py-2" value={SortKeys.title}>
{formatMessage(messages.sortTitle)}
</Form.Radio>
</Form.RadioSet>
</>
);
};
SortForm.propTypes = {
handleSortChange: PropTypes.func.isRequired,
sortBy: PropTypes.string.isRequired,
};
export default SortForm;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { useToggle } from '@edx/paragon';
export const useCourseFilterControlsData = ({
setFilters,
setSortBy,
}) => {
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = React.useState(null);
const handleFilterChange = ({ target: { checked, value } }) => {
const update = checked ? setFilters.add : setFilters.remove;
update(value);
};
const handleSortChange = ({ target: { value } }) => {
setSortBy(value);
};
return {
isOpen,
open,
close,
target,
setTarget,
handleFilterChange,
handleSortChange,
};
};
export default useCourseFilterControlsData;

View File

@@ -0,0 +1,6 @@
import CourseFilterControls from './CourseFilterControls';
export { default as ActiveCourseFilters } from './ActiveCourseFilters';
export { CourseFilterControls };
export default CourseFilterControls;

View File

@@ -0,0 +1,20 @@
#course-filter-controls-card {
width: 512px;
height: 288px;
&.no-enrollments {
height: 172px;
}
.filter-form-heading {
font-weight: bold;
font-size: 18px;
}
hr {
width: 1px;
};
.filter-form-col {
width: 256px;
display: inline-block;
text-align: left;
}
}

View File

@@ -0,0 +1,55 @@
import { StrictDict } from 'utils';
export const messages = StrictDict({
inProgress: {
id: 'learner-dash.courseListFilters.inProgress',
description: 'in-progress filter checkbox label for course list filters',
defaultMessage: 'In-Progress',
},
notStarted: {
id: 'learner-dash.courseListFilters.notStarted',
description: 'Not-Started filter checkbox label for course list filters',
defaultMessage: 'Not Started',
},
done: {
id: 'learner-dash.courseListFilters.done',
description: 'done filter checkbox label for course list filters',
defaultMessage: 'Done',
},
notEnrolled: {
id: 'learner-dash.courseListFilters.notEnrolled',
description: 'not-enrolled filter checkbox label for course list filters',
defaultMessage: 'Not Enrolled',
},
upgraded: {
id: 'learner-dash.courseListFilters.upgraded',
description: 'upgraded filter checkbox label for course list filters',
defaultMessage: 'Upgraded',
},
clearAll: {
id: 'learner-dash.courseListFilters.clearAll',
description: 'clear all filters button text',
defaultMessage: 'Clear all',
},
sort: {
id: 'learner-dash.courseListFilters.sort',
description: 'Sort radio form heading',
defaultMessage: 'Sort',
},
sortLastEnrolled: {
id: 'learner-dash.courseListFilters.sortLastEnrolled',
description: 'Last enrolled sort option text',
defaultMessage: 'Last enrolled',
},
sortTitle: {
id: 'learner-dash.courseListFilters.sortTitle',
description: 'Title sort option text',
defaultMessage: 'Title (A-Z)',
},
refine: {
id: 'learner-dash.courseListFilters.refine',
description: 'Filter button container text',
defaultMessage: 'Refine',
},
});
export default messages;

View File

@@ -1,20 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Pagination,
useCheckboxSetValues,
} from '@edx/paragon';
import { hooks as appHooks } from 'data/redux';
import { ListPageSize, SortKeys } from 'data/constants/app';
import { ActiveCourseFilters, CourseFilterControls } from 'containers/CourseFilterControls';
import CourseCard from 'containers/CourseCard';
import SelectSession from 'containers/SelectSession';
export const CourseList = ({ courseListData }) => (
<div className="d-flex flex-column flex-grow-1">
{courseListData.map((courseNumber) => (
<CourseCard key={courseNumber} courseNumber={courseNumber} />
))}
<SelectSession />
</div>
);
import messages from './messages';
export const useCourseListData = () => {
const [pageNumber, setPageNumber] = React.useState(1);
const [sortBy, setSortBy] = React.useState(SortKeys.title);
const [filters, setFilters] = useCheckboxSetValues([]);
const { numPages, visible } = appHooks.useCurrentCourseList({
sortBy,
isAscending: true,
filters,
pageNumber,
pageSize: ListPageSize,
});
const handleRemoveFilter = (filter) => () => setFilters.remove(filter);
return {
numPages,
setPageNumber,
visibleList: visible,
filterOptions: {
sortBy,
setSortBy,
filters,
setFilters,
handleRemoveFilter,
},
showFilters: filters.length > 0,
};
};
export const CourseList = () => {
const {
filterOptions,
setPageNumber,
numPages,
showFilters,
visibleList,
} = useCourseListData();
return (
<div className="course-list-container">
<div id="course-list-heading-container">
<h2 className="my-2">
<FormattedMessage {...messages.myCourses} />
</h2>
<div
id="course-filter-controls-container"
className="text-right"
>
<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} />
))}
<Pagination
variant="secondary"
paginationLabel="Course List"
pageCount={numPages}
onPageSelect={setPageNumber}
/>
</div>
</div>
);
};
CourseList.propTypes = {
courseListData: PropTypes.arrayOf(PropTypes.string).isRequired,
};
export default CourseList;

View File

@@ -0,0 +1,11 @@
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,44 +1,41 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { selectors, thunkActions } from 'data/redux';
import {
thunkActions,
hooks as appHooks,
} from 'data/redux';
import CourseList from 'containers/CourseList';
import WidgetSidebar from 'containers/WidgetSidebar';
import EmptyCourse from 'containers/EmptyCourse';
import SelectSessionModal from 'containers/SelectSessionModal';
import EnterpriseDashboardModal from 'containers/EnterpriseDashboardModal';
import messages from './messages';
import * as module from '.';
import './index.scss';
export const useDashboardData = ({ dispatch }) => {
export const Dashboard = () => {
const dispatch = useDispatch();
React.useEffect(
() => { dispatch(thunkActions.app.initialize()); },
[dispatch],
);
return {
enrollments: useSelector(selectors.app.enrollments),
entitlements: useSelector(selectors.app.entitlements),
};
};
export const Dashboard = () => {
const dispatch = useDispatch();
const {
enrollments,
// entitlements,
} = module.useDashboardData({ dispatch });
const hasCourses = appHooks.useHasCourses();
const hasAvailableDashboards = appHooks.useHasAvailableDashboards();
return (
<div className="d-flex flex-column p-2">
{enrollments.length ? (
<div className="d-flex flex-column p-2" id="course-dashboard">
{hasAvailableDashboards && <EnterpriseDashboardModal />}
{hasCourses ? (
<>
<h2 className="py-2">
<FormattedMessage {...messages.myCourse} />
</h2>
<div className="d-flex">
<CourseList courseListData={enrollments} />
<WidgetSidebar />
<div className="d-flex" style={{ margin: 'auto' }}>
<div className="w-100 mw-md mr-4">
<SelectSessionModal />
<CourseList />
</div>
<div id="dashboard-sidebar-container mw-xs">
<WidgetSidebar />
</div>
</div>
</>
) : (

View File

@@ -0,0 +1,4 @@
#course-list-heading-container {
display: flex;
justify-content: space-between;
}

View File

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

View File

@@ -12,10 +12,10 @@ export const state = StrictDict({
export const useEmailData = ({
closeModal,
courseNumber,
cardId,
// dispatch,
}) => {
const { isEmailEnabled } = appHooks.useCardEnrollmentData(courseNumber);
const { isEmailEnabled } = appHooks.useCardEnrollmentData(cardId);
const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled);
const onToggle = React.useCallback(
() => setToggleValue(!toggleValue),

View File

@@ -9,7 +9,7 @@ jest.mock('data/redux', () => ({
},
}));
const courseNumber = 'my-test-course-number';
const cardId = 'my-test-course-number';
const closeModal = jest.fn();
const state = new MockUseState(hooks);
@@ -26,12 +26,12 @@ describe('EmailSettingsModal hooks', () => {
beforeEach(() => {
state.mock();
appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEmailEnabled: true });
out = hooks.useEmailData({ closeModal, courseNumber });
out = hooks.useEmailData({ closeModal, cardId });
});
afterEach(state.restore);
test('loads enrollment data based on course number', () => {
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardEnrollmentData).toHaveBeenCalledWith(cardId);
});
test('initializes toggle value to cardData.isEmailEnabled', () => {
@@ -39,7 +39,7 @@ describe('EmailSettingsModal hooks', () => {
expect(out.toggleValue).toEqual(true);
appHooks.useCardEnrollmentData.mockReturnValueOnce({ isEmailEnabled: false });
out = hooks.useEmailData({ closeModal, courseNumber });
out = hooks.useEmailData({ closeModal, cardId });
state.expectInitializedWith(state.keys.toggle, false);
expect(out.toggleValue).toEqual(false);
});

View File

@@ -18,14 +18,14 @@ import messages from './messages';
export const EmailSettingsModal = ({
closeModal,
show,
courseNumber,
cardId,
}) => {
const dispatch = useDispatch();
const {
toggleValue,
onToggle,
save,
} = useEmailData({ dispatch, closeModal, courseNumber });
} = useEmailData({ dispatch, closeModal, cardId });
const { formatMessage } = useIntl();
return (
@@ -52,7 +52,7 @@ export const EmailSettingsModal = ({
);
};
EmailSettingsModal.propTypes = {
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
closeModal: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
};

View File

@@ -19,7 +19,7 @@ const hookProps = {
const props = {
closeModal: jest.fn().mockName('closeModal'),
show: true,
courseNumber: 'test-course-number',
cardId: 'test-course-number',
};
const dispatch = useDispatch();
@@ -33,11 +33,11 @@ describe('EmailSettingsModal', () => {
hooks.mockReturnValueOnce(hookProps);
shallow(<EmailSettingsModal {...props} />);
});
it('calls hook w/ dispatch from redux hook, and closeModal, courseNumber from props', () => {
it('calls hook w/ dispatch from redux hook, and closeModal, cardId from props', () => {
expect(hooks).toHaveBeenCalledWith({
closeModal: props.closeModal,
dispatch,
courseNumber: props.courseNumber,
cardId: props.cardId,
});
});
});

View File

@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnterpriseDashboard snapshot 1`] = `
<ModalDialog
hasCloseButton={false}
onClose={[MockFunction useEnterpriseDashboardHook.handleClick]}
title=""
>
<div
className="bg-white p-3 rounded shadow"
style={
Object {
"textAlign": "start",
}
}
>
<h4>
You have access to the edX, Inc. dashboard
</h4>
<p>
To access the coureses available to you through edX, Inc., visit the edX, Inc. dashboard now.
</p>
<ActionRow>
<Button
onClick={[MockFunction useEnterpriseDashboardHook.handleClick]}
variant="tertiary"
>
Dismiss
</Button>
<Button
href="/edx-dashboard"
type="a"
>
Go To Dashboard
</Button>
</ActionRow>
</div>
</ModalDialog>
`;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { hooks as appHooks } from 'data/redux';
import { StrictDict } from 'utils';
import * as module from './hooks';
export const state = StrictDict({
showModal: (val) => React.useState(val), // eslint-disable-line
});
export const useEnterpriseDashboardHook = () => {
const [showModal, setShowModal] = module.state.showModal(true);
const { mostRecentDashboard } = appHooks.useEnterpriseDashboardData();
const handleClick = () => setShowModal(false);
return {
showModal,
handleClick,
mostRecentDashboard,
};
};
export default useEnterpriseDashboardHook;

View File

@@ -0,0 +1,44 @@
import { MockUseState } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
useEnterpriseDashboardData: jest.fn(),
},
}));
const state = new MockUseState(hooks);
const enterpriseDashboardData = {
mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' },
};
describe('EnterpriseDashboard hooks', () => {
appHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData });
describe('state values', () => {
state.testGetter(state.keys.showModal);
});
describe('behavior', () => {
let out;
beforeEach(() => {
state.mock();
out = hooks.useEnterpriseDashboardHook();
});
afterEach(state.restore);
test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => {
expect(out.mostRecentDashboard).toMatchObject(enterpriseDashboardData.mostRecentDashboard);
});
test('modal initializes to shown when rendered and closes on click', () => {
state.expectInitializedWith(state.keys.showModal, true);
out.handleClick();
expect(state.values.showModal).toEqual(false);
});
});
});

View File

@@ -0,0 +1,55 @@
import React from 'react';
// import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ModalDialog, ActionRow, Button,
} from '@edx/paragon';
import messages from './messages';
import useEnterpriseDashboardHook from './hooks';
export const EnterpriseDashboardModal = () => {
const { formatMessage } = useIntl();
const {
showModal,
handleClick,
mostRecentDashboard,
} = useEnterpriseDashboardHook();
return (
<ModalDialog
isOpen={showModal}
onClose={handleClick}
hasCloseButton={false}
title=""
>
<div
className="bg-white p-3 rounded shadow"
style={{ textAlign: 'start' }}
>
<h4>
{formatMessage(messages.enterpriseDialogHeader, {
label: mostRecentDashboard.label,
})}
</h4>
<p>
{formatMessage(messages.enterpriseDialogBody, {
label: mostRecentDashboard.label,
})}
</p>
<ActionRow>
<Button variant="tertiary" onClick={handleClick}>
{formatMessage(messages.enterpriseDialogDismissButton)}
</Button>
<Button type="a" href={mostRecentDashboard.url}>
{formatMessage(messages.enterpriseDialogConfirmButton)}
</Button>
</ActionRow>
</div>
</ModalDialog>
);
};
EnterpriseDashboardModal.propTypes = {};
export default EnterpriseDashboardModal;

View File

@@ -0,0 +1,22 @@
import { shallow } from 'enzyme';
import EnterpriseDashboard from '.';
import useEnterpriseDashboardHook from './hooks';
jest.mock('./hooks', () => ({
__esModule: true,
default: jest.fn(),
}));
describe('EnterpriseDashboard', () => {
test('snapshot', () => {
const hookData = {
mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' },
showDialog: false,
handleClick: jest.fn().mockName('useEnterpriseDashboardHook.handleClick'),
};
useEnterpriseDashboardHook.mockReturnValueOnce({ ...hookData });
const el = shallow(<EnterpriseDashboard />);
expect(el).toMatchSnapshot();
});
});

View File

@@ -1,11 +1,6 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
dashboard: {
id: 'leanerDashboard.menu.dashboard.label',
defaultMessage: 'Dashboard',
description: 'The text for the user menu Dashboard navigation link.',
},
enterpriseDialogHeader: {
id: 'leanerDashboard.enterpriseDialogHeader',
defaultMessage: 'You have access to the {label} dashboard',

View File

@@ -6,11 +6,12 @@ import { useIntl } from '@edx/frontend-platform/i18n';
import { Dropdown, Icon } from '@edx/paragon';
import { Person } from '@edx/paragon/icons';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
import EnterpriseDashboard from './EnterpriseDashboard';
export const AuthenticatedUserDropdown = ({ username }) => {
const { formatMessage } = useIntl();
const { availableDashboards } = appHooks.useEnterpriseDashboardData();
return (
<>
<Dropdown className="user-dropdown">
@@ -21,7 +22,18 @@ export const AuthenticatedUserDropdown = ({ username }) => {
</span>
</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu-right">
<EnterpriseDashboard />
<Dropdown.Header>SWITCH DASHBOARD</Dropdown.Header>
<Dropdown.Item as="a" href="/dashboard">Personal</Dropdown.Item>
{availableDashboards && availableDashboards.map((dashboard) => (
<Dropdown.Item
as="a"
href={dashboard.url}
key={dashboard.label}
>
{dashboard.label} {formatMessage(messages.dashboard)}
</Dropdown.Item>
))}
<Dropdown.Divider />
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
{formatMessage(messages.profile)}
</Dropdown.Item>
@@ -36,6 +48,7 @@ export const AuthenticatedUserDropdown = ({ username }) => {
<Dropdown.Item href={getConfig().SUPPORT_URL}>
{formatMessage(messages.help)}
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item href={getConfig().LOGOUT_URL}>
{formatMessage(messages.signOut)}
</Dropdown.Item>

View File

@@ -24,6 +24,7 @@ export const useConfirmEmailBannerData = () => {
const openConfirmModalButtonClick = () => {
dispatch(thunkActions.app.sendConfirmEmail());
openConfirmModal();
closePageBanner();
};
const userConfirmEmailButtonClick = () => {

View File

@@ -1,130 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EnterpriseDashboard snapshot initilized 1`] = `
<Fragment>
<Dropdown.Item
active={false}
key="Personal"
>
Personal
Dashboard
</Dropdown.Item>
<Dropdown.Item
active={true}
key="edX, Inc."
>
edX, Inc.
Dashboard
</Dropdown.Item>
<Dropdown.Item
active={false}
key="Harvard"
>
Harvard
Dashboard
</Dropdown.Item>
<ModalDialog
hasCloseButton={false}
isOpen={false}
onClose={[MockFunction hooks.nullMethod]}
title=""
>
<div
className="bg-white p-3 rounded shadow"
style={
Object {
"textAlign": "start",
}
}
>
<h4>
You have access to the undefined dashboard
</h4>
<p>
To access the coureses available to you through undefined, visit the undefined dashboard now.
</p>
<ActionRow>
<Button
onClick={[MockFunction cancelSelectDashboardItem]}
variant="tertiary"
>
Dismiss
</Button>
<Button
type="a"
>
Go To Dashboard
</Button>
</ActionRow>
</div>
</ModalDialog>
</Fragment>
`;
exports[`EnterpriseDashboard snapshot select item and open modal 1`] = `
<Fragment>
<Dropdown.Item
active={false}
key="Personal"
>
Personal
Dashboard
</Dropdown.Item>
<Dropdown.Item
active={true}
key="edX, Inc."
>
edX, Inc.
Dashboard
</Dropdown.Item>
<Dropdown.Item
active={false}
key="Harvard"
>
Harvard
Dashboard
</Dropdown.Item>
<ModalDialog
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
title=""
>
<div
className="bg-white p-3 rounded shadow"
style={
Object {
"textAlign": "start",
}
}
>
<h4>
You have access to the Personal dashboard
</h4>
<p>
To access the coureses available to you through Personal, visit the Personal dashboard now.
</p>
<ActionRow>
<Button
onClick={[MockFunction cancelSelectDashboardItem]}
variant="tertiary"
>
Dismiss
</Button>
<Button
href="/dashboard"
type="a"
>
Go To Dashboard
</Button>
</ActionRow>
</div>
</ModalDialog>
</Fragment>
`;

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { hooks as appHooks } from 'data/redux';
import { StrictDict } from 'utils';
import * as module from './hooks';
export const state = StrictDict({
showDialog: (val) => React.useState(val), // eslint-disable-line
selectedItem: (val) => React.useState(val), // eslint-disable-line
});
export const useEnterpriseDashboardHook = () => {
const { availableDashboards, mostRecentDashboard } = appHooks.useEnterpriseDashboardData();
const [showDialog, setShowDialog] = module.state.showDialog(false);
const [selectedItem, setSelectedItem] = module.state.selectedItem({});
const beginSelectDashboardItem = (val) => () => {
setSelectedItem(val);
setShowDialog(true);
};
const cancelSelectDashboardItem = () => {
setSelectedItem({});
setShowDialog(false);
};
return {
availableDashboards,
mostRecentDashboard,
showDialog,
selectedItem,
beginSelectDashboardItem,
cancelSelectDashboardItem,
};
};
export default useEnterpriseDashboardHook;

View File

@@ -1,60 +0,0 @@
import { MockUseState } from 'testUtils';
import { hooks as appHooks } from 'data/redux';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
useEnterpriseDashboardData: jest.fn(),
},
}));
const state = new MockUseState(hooks);
const enterpriseDashboardData = {
availableDashboards: [
{ label: 'Personal', url: '/dashboard' },
{ label: 'edX, Inc.', url: '/edx-dashboard' },
{ label: 'Harvard', url: '/harvard-dashboard' },
],
mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' },
};
describe('EnterpriseDashboard hooks', () => {
appHooks.useEnterpriseDashboardData.mockReturnValue({ ...enterpriseDashboardData });
describe('state values', () => {
state.testGetter(state.keys.showDialog);
state.testGetter(state.keys.selectedItem);
});
describe('behavior', () => {
let out;
beforeEach(() => {
state.mock();
out = hooks.useEnterpriseDashboardHook();
});
afterEach(state.restore);
test('useEnterpriseDashboardHook to return dashboard data from redux hooks', () => {
expect(out.availableDashboards).toMatchObject(enterpriseDashboardData.availableDashboards);
expect(out.mostRecentDashboard).toMatchObject(enterpriseDashboardData.mostRecentDashboard);
});
test('modal is open on begin select dashboard item', () => {
state.expectInitializedWith('showDialog', false);
state.expectInitializedWith('selectedItem', {});
const selectedItem = { abitary: 'not so true' };
out.beginSelectDashboardItem(selectedItem)();
expect(state.values.showDialog).toEqual(true);
expect(state.values.selectedItem).toMatchObject(selectedItem);
});
test('modal is close on cancel select dashboard item', () => {
out.cancelSelectDashboardItem();
expect(state.values.selectedItem).toMatchObject({});
expect(state.values.showDialog).toEqual(false);
});
});
});

View File

@@ -1,73 +0,0 @@
import React from 'react';
// import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Dropdown, ModalDialog, ActionRow, Button,
} from '@edx/paragon';
import { nullMethod } from 'hooks';
import messages from './messages';
import useEnterpriseDashboardHook from './hooks';
export const EnterpriseDashboard = () => {
const { formatMessage } = useIntl();
const {
availableDashboards,
mostRecentDashboard,
showDialog,
selectedItem,
beginSelectDashboardItem,
cancelSelectDashboardItem,
} = useEnterpriseDashboardHook();
return (
<>
{availableDashboards.map((dashboard) => (
<Dropdown.Item
onClick={beginSelectDashboardItem(dashboard)}
active={dashboard.label === mostRecentDashboard.label}
key={dashboard.label}
>
{dashboard.label} {formatMessage(messages.dashboard)}
</Dropdown.Item>
))}
<ModalDialog
isOpen={showDialog}
onClose={nullMethod}
hasCloseButton={false}
title=""
>
<div
className="bg-white p-3 rounded shadow"
style={{ textAlign: 'start' }}
>
<h4>
{formatMessage(messages.enterpriseDialogHeader, {
label: selectedItem.label,
})}
</h4>
<p>
{formatMessage(messages.enterpriseDialogBody, {
label: selectedItem.label,
})}
</p>
<ActionRow>
<Button variant="tertiary" onClick={cancelSelectDashboardItem}>
{formatMessage(messages.enterpriseDialogDismissButton)}
</Button>
<Button type="a" href={selectedItem.url}>
{formatMessage(messages.enterpriseDialogConfirmButton)}
</Button>
</ActionRow>
</div>
</ModalDialog>
</>
);
};
EnterpriseDashboard.propTypes = {};
export default EnterpriseDashboard;

View File

@@ -1,49 +0,0 @@
import { shallow } from 'enzyme';
import EnterpriseDashboard from '.';
import useEnterpriseDashboardHook from './hooks';
jest.mock('./hooks', () => ({
__esModule: true,
default: jest.fn(),
}));
const enterpriseDashboardData = {
availableDashboards: [
{ label: 'Personal', url: '/dashboard' },
{ label: 'edX, Inc.', url: '/edx-dashboard' },
{ label: 'Harvard', url: '/harvard-dashboard' },
],
mostRecentDashboard: { label: 'edX, Inc.', url: '/edx-dashboard' },
};
describe('EnterpriseDashboard', () => {
describe('snapshot', () => {
const hookReturn = {
...enterpriseDashboardData,
showDialog: false,
selectedItem: {},
beginSelectDashboardItem: jest.fn().mockName('beginSelectDashboardItem'),
cancelSelectDashboardItem: jest
.fn()
.mockName('cancelSelectDashboardItem'),
};
test('initilized', () => {
useEnterpriseDashboardHook.mockReturnValueOnce({ ...hookReturn });
const el = shallow(<EnterpriseDashboard />);
expect(el).toMatchSnapshot();
});
test('select item and open modal', () => {
useEnterpriseDashboardHook.mockReturnValueOnce({
...hookReturn,
selectedItem: enterpriseDashboardData.availableDashboards[0],
showDialog: true,
});
const el = shallow(<EnterpriseDashboard />);
expect(el).toMatchSnapshot();
});
});
});

View File

@@ -6,6 +6,7 @@ import { Program } from '@edx/paragon/icons';
import { Button } from '@edx/paragon';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import GreetingBanner from './GreetingBanner';
import messages from './messages';
import ConfirmEmailBanner from './ConfirmEmailBanner';

View File

@@ -1,6 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
dashboard: {
id: 'leanerDashboard.menu.dashboard.label',
defaultMessage: 'Dashboard',
description: 'The text for the user menu Dashboard navigation link.',
},
help: {
id: 'leanerDashboard.help.label',
defaultMessage: 'Help',

View File

@@ -1,10 +1,10 @@
import { hooks as appHooks } from 'data/redux';
export const useProgramData = ({
courseNumber,
cardId,
}) => ({
courseTitle: appHooks.useCardCourseData(courseNumber).title,
relatedPrograms: appHooks.useCardRelatedProgramsData(courseNumber).list,
courseTitle: appHooks.useCardCourseData(cardId).title,
relatedPrograms: appHooks.useCardRelatedProgramsData(cardId).list,
});
export default useProgramData;

View File

@@ -9,7 +9,7 @@ jest.mock('data/redux', () => ({
},
}));
const courseNumber = 'test-course-number';
const cardId = 'test-course-number';
const courseTitle = 'test-course-title';
const relatedPrograms = ['some', 'programs'];
@@ -18,9 +18,9 @@ describe('RelatedProgramsModal hooks', () => {
it('forwards course title and related programs list by course number', () => {
appHooks.useCardCourseData.mockReturnValue({ title: courseTitle });
appHooks.useCardRelatedProgramsData.mockReturnValue({ list: relatedPrograms });
const out = hooks.useProgramData({ courseNumber });
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(appHooks.useCardRelatedProgramsData).toHaveBeenCalledWith(courseNumber);
const out = hooks.useProgramData({ cardId });
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(cardId);
expect(appHooks.useCardRelatedProgramsData).toHaveBeenCalledWith(cardId);
expect(out).toEqual({ courseTitle, relatedPrograms });
});
});

View File

@@ -13,10 +13,10 @@ import './index.scss';
export const RelatedProgramsModal = ({
isOpen,
closeModal,
courseNumber,
cardId,
}) => {
const { formatMessage } = useIntl();
const { courseTitle, relatedPrograms } = useProgramData({ courseNumber });
const { courseTitle, relatedPrograms } = useProgramData({ cardId });
return (
<ModalDialog
title={formatMessage(messages.header)}
@@ -50,7 +50,7 @@ export const RelatedProgramsModal = ({
RelatedProgramsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
closeModal: PropTypes.func.isRequired,
courseNumber: PropTypes.string.isRequired,
cardId: PropTypes.string.isRequired,
};
export default RelatedProgramsModal;

View File

@@ -9,7 +9,7 @@ jest.mock('./hooks', () => ({
useProgramData: jest.fn(),
}));
const courseNumber = 'test-course-number';
const cardId = 'test-course-number';
const hookProps = {
courseTitle: 'hookProps.courseTitle',
relatedPrograms: [
@@ -31,7 +31,7 @@ const hookProps = {
const props = {
isOpen: true,
closeModal: jest.fn().mockName('props.closeModal'),
courseNumber,
cardId,
};
describe('RelatedProgramsModal', () => {

View File

@@ -1,82 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow, Button, Form, ModalDialog,
} from '@edx/paragon';
import { nullMethod } from 'hooks';
import { dateFormatter } from 'utils';
import useSelectSession from './hooks';
import messages from './messages';
export const SelectSessionModal = ({ courseNumber }) => {
const {
entitlementSessions,
showSessionModal,
closeSessionModal,
showLeaveSessionInSessionModal,
courseTitle,
} = useSelectSession({
courseNumber,
});
const { formatMessage, formatDate } = useIntl();
let header;
let hint;
if (showLeaveSessionInSessionModal) {
header = formatMessage(messages.changeOrLeaveHeader);
hint = formatMessage(messages.changeOrLeaveHint);
} else {
header = formatMessage(messages.selectSessionHeader, {
courseTitle,
});
hint = formatMessage(messages.selectSessionHint);
}
return (
<ModalDialog
isOpen={showSessionModal}
onClose={nullMethod}
hasCloseButton={false}
title=""
>
<div
className="bg-white p-3 rounded shadow"
size="md"
style={{ textAlign: 'start' }}
>
<h4>{header}</h4>
<Form.Group>
<Form.Label>{hint}</Form.Label>
<Form.RadioSet name="sessions">
{entitlementSessions?.map((entitle) => (
<Form.Radio key={entitle.startDate} value={entitle.startDate}>
{dateFormatter(formatDate, entitle.startDate)} - {dateFormatter(formatDate, entitle.endDate)}
</Form.Radio>
))}
{showLeaveSessionInSessionModal ? (
<Form.Radio value="leave">
{formatMessage(messages.leaveSessionOption)}
</Form.Radio>
) : null}
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button variant="tertiary" onClick={closeSessionModal}>
{formatMessage(messages.nevermind)}
</Button>
<Button>{formatMessage(messages.confirmSession)}</Button>
</ActionRow>
</div>
</ModalDialog>
);
};
SelectSessionModal.propTypes = {
courseNumber: PropTypes.string.isRequired,
};
export default SelectSessionModal;

View File

@@ -1,188 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSessionModal snapshot empty modal with leave option 1`] = `
<ModalDialog
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
title=""
>
<div
className="bg-white p-3 rounded shadow"
size="md"
style={
Object {
"textAlign": "start",
}
}
>
<h4>
Change or leave session?
</h4>
<Form.Group>
<Form.Label>
When you change to a different session any course progress or grades from your current session will be lost.
</Form.Label>
<Form.RadioSet
name="sessions"
>
<Form.Radio
value="leave"
>
Leave session
</Form.Radio>
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button
onClick={[MockFunction useSelectSession.closeSessionModal]}
variant="tertiary"
>
Nevermind
</Button>
<Button>
Confirm Session
</Button>
</ActionRow>
</div>
</ModalDialog>
`;
exports[`SelectSessionModal snapshot modal with leave option 1`] = `
<ModalDialog
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
title=""
>
<div
className="bg-white p-3 rounded shadow"
size="md"
style={
Object {
"textAlign": "start",
}
}
>
<h4>
Change or leave session?
</h4>
<Form.Group>
<Form.Label>
When you change to a different session any course progress or grades from your current session will be lost.
</Form.Label>
<Form.RadioSet
name="sessions"
>
<Form.Radio
key="1/2/2000"
value="1/2/2000"
>
1/2/2000
-
1/2/2020
</Form.Radio>
<Form.Radio
key="2/3/2000"
value="2/3/2000"
>
2/3/2000
-
2/3/2020
</Form.Radio>
<Form.Radio
key="3/4/2000"
value="3/4/2000"
>
3/4/2000
-
3/4/2020
</Form.Radio>
<Form.Radio
value="leave"
>
Leave session
</Form.Radio>
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button
onClick={[MockFunction useSelectSession.closeSessionModal]}
variant="tertiary"
>
Nevermind
</Button>
<Button>
Confirm Session
</Button>
</ActionRow>
</div>
</ModalDialog>
`;
exports[`SelectSessionModal snapshot modal without leave option 1`] = `
<ModalDialog
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
title=""
>
<div
className="bg-white p-3 rounded shadow"
size="md"
style={
Object {
"textAlign": "start",
}
}
>
<h4>
Select a session to access course-title: unit test save life
</h4>
<Form.Group>
<Form.Label>
Remember, if you change your mind you have 2 weeks to unenroll and reclaim your entitlement.
</Form.Label>
<Form.RadioSet
name="sessions"
>
<Form.Radio
key="1/2/2000"
value="1/2/2000"
>
1/2/2000
-
1/2/2020
</Form.Radio>
<Form.Radio
key="2/3/2000"
value="2/3/2000"
>
2/3/2000
-
2/3/2020
</Form.Radio>
<Form.Radio
key="3/4/2000"
value="3/4/2000"
>
3/4/2000
-
3/4/2020
</Form.Radio>
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button
onClick={[MockFunction useSelectSession.closeSessionModal]}
variant="tertiary"
>
Nevermind
</Button>
<Button>
Confirm Session
</Button>
</ActionRow>
</div>
</ModalDialog>
`;

View File

@@ -1,9 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSession snapshot has courseNumber 1`] = `
<SelectSessionModal
courseNumber="some course"
/>
`;
exports[`SelectSession snapshot no courseNumber 1`] = `""`;

View File

@@ -1,35 +0,0 @@
import { hooks as appHooks, actions } from 'data/redux';
import { useDispatch } from 'react-redux';
export const useSelectSession = ({ courseNumber }) => {
const dispatch = useDispatch();
const {
showSessionModal,
showLeaveSessionInSessionModal,
} = appHooks.useSelectSessionsModalData();
const { entitlementSessions } = appHooks.useCardEntitlementsData(courseNumber);
const { title: courseTitle } = appHooks.useCardCourseData(courseNumber);
const updateSessionModal = (showModal, showLeaveOption = false) => dispatch(
actions.app.updateSelectSessionModal({
showSessionModal: showModal,
showLeaveSessionInSessionModal: showLeaveOption,
courseNumber,
}),
);
return {
showSessionModal,
closeSessionModal: () => updateSessionModal(false),
openSessionModal: () => updateSessionModal(true),
openSessionModalWithLeaveOption: () => updateSessionModal(true, true),
showLeaveSessionInSessionModal,
entitlementSessions,
courseTitle,
};
};
export default useSelectSession;

View File

@@ -1,85 +0,0 @@
import { hooks as appHooks, actions } from 'data/redux';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
useCardEntitlementsData: jest.fn(),
useCardCourseData: jest.fn(),
useSelectSessionsModalData: jest.fn(),
},
actions: {
app: {
updateSelectSessionModal: jest.fn(),
},
},
}));
const courseNumber = 'my-test-course-number';
const entitlement = {
showSessionModal: false,
showLeaveSessionInSessionModal: false,
};
const availableSessions = [
{ startDate: '1/2/2000', endDate: '1/2/2020', courseNumber },
{ startDate: '2/3/2000', endDate: '2/3/2020', courseNumber },
{ startDate: '3/4/2000', endDate: '3/4/2020', courseNumber },
];
const cardCourseData = {
title: 'course-title: brown fox',
};
describe('SelectSessionModal hooks', () => {
let out;
beforeEach(() => {
jest.clearAllMocks();
});
describe('useSelectSession', () => {
beforeEach(() => {
appHooks.useSelectSessionsModalData.mockReturnValueOnce({ ...entitlement });
appHooks.useCardEntitlementsData.mockReturnValueOnce({ entitlementSessions: availableSessions });
appHooks.useCardCourseData.mockReturnValueOnce({ ...cardCourseData });
out = hooks.useSelectSession({ courseNumber });
});
test('loads entitlement data based on course number', () => {
expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(courseNumber);
});
test('get course title based on course number', () => {
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(courseNumber);
expect(out.courseTitle).toEqual(cardCourseData.title);
});
test('open session modal', () => {
out.openSessionModal();
expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
showSessionModal: true,
showLeaveSessionInSessionModal: false,
courseNumber,
});
});
test('open session modal with leave option', () => {
out.openSessionModalWithLeaveOption();
expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
showSessionModal: true,
showLeaveSessionInSessionModal: true,
courseNumber,
});
});
test('close session modal', () => {
out.closeSessionModal();
expect(actions.app.updateSelectSessionModal).toHaveBeenCalledWith({
showSessionModal: false,
showLeaveSessionInSessionModal: false,
courseNumber,
});
});
});
});

View File

@@ -1,12 +0,0 @@
import React from 'react';
import { hooks as appHooks } from 'data/redux';
import SelectSessionModal from './SelectSessionModal';
export const SelectSession = () => {
const { courseNumber } = appHooks.useSelectSessionsModalData();
return courseNumber ? <SelectSessionModal courseNumber={courseNumber} /> : null;
};
SelectSession.propTypes = {};
export default SelectSession;

View File

@@ -1,26 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { hooks as appHooks } from 'data/redux';
import SelectSession from '.';
jest.mock('data/redux', () => ({
hooks: {
useSelectSessionsModalData: jest.fn(),
},
}));
describe('SelectSession', () => {
describe('snapshot', () => {
test('no courseNumber', () => {
appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: null });
expect(shallow(<SelectSession />)).toMatchSnapshot();
});
test('has courseNumber', () => {
appHooks.useSelectSessionsModalData.mockReturnValueOnce({ courseNumber: 'some course' });
expect(shallow(<SelectSession />)).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,170 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectSessionModal snapshot empty modal with leave option 1`] = `
<ModalDialog
className="p-4 px-4.5"
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
size="md"
>
<h3>
test-header
</h3>
<Form.Group
className="pt-3"
>
<Form.Label>
test-hint
</Form.Label>
<Form.RadioSet
className="pt-3 pb-4"
name="sessions"
>
<Form.Radio
value="leave"
>
Leave session
</Form.Radio>
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button
onClick={[MockFunction useSelectSessionModalData.closeSessionModal]}
variant="tertiary"
>
Nevermind
</Button>
<Button>
Confirm Session
</Button>
</ActionRow>
</ModalDialog>
`;
exports[`SelectSessionModal snapshot modal with leave option 1`] = `
<ModalDialog
className="p-4 px-4.5"
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
size="md"
>
<h3>
test-header
</h3>
<Form.Group
className="pt-3"
>
<Form.Label>
test-hint
</Form.Label>
<Form.RadioSet
className="pt-3 pb-4"
name="sessions"
>
<Form.Radio
key="1/2/2000"
value="1/2/2000"
>
1/2/2000
-
1/2/2020
</Form.Radio>
<Form.Radio
key="2/3/2000"
value="2/3/2000"
>
2/3/2000
-
2/3/2020
</Form.Radio>
<Form.Radio
key="3/4/2000"
value="3/4/2000"
>
3/4/2000
-
3/4/2020
</Form.Radio>
<Form.Radio
value="leave"
>
Leave session
</Form.Radio>
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button
onClick={[MockFunction useSelectSessionModalData.closeSessionModal]}
variant="tertiary"
>
Nevermind
</Button>
<Button>
Confirm Session
</Button>
</ActionRow>
</ModalDialog>
`;
exports[`SelectSessionModal snapshot modal without leave option 1`] = `
<ModalDialog
className="p-4 px-4.5"
hasCloseButton={false}
isOpen={true}
onClose={[MockFunction hooks.nullMethod]}
size="md"
>
<h3>
test-header
</h3>
<Form.Group
className="pt-3"
>
<Form.Label>
test-hint
</Form.Label>
<Form.RadioSet
className="pt-3 pb-4"
name="sessions"
>
<Form.Radio
key="1/2/2000"
value="1/2/2000"
>
1/2/2000
-
1/2/2020
</Form.Radio>
<Form.Radio
key="2/3/2000"
value="2/3/2000"
>
2/3/2000
-
2/3/2020
</Form.Radio>
<Form.Radio
key="3/4/2000"
value="3/4/2000"
>
3/4/2000
-
3/4/2020
</Form.Radio>
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button
onClick={[MockFunction useSelectSessionModalData.closeSessionModal]}
variant="tertiary"
>
Nevermind
</Button>
<Button>
Confirm Session
</Button>
</ActionRow>
</ModalDialog>
`;

View File

@@ -0,0 +1,46 @@
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
export const useSelectSessionModalData = () => {
const dispatch = useDispatch();
const selectedCardId = appHooks.useSelectSessionModalData().cardId;
const {
entitlementSessions,
isFulfilled,
} = appHooks.useCardEntitlementsData(selectedCardId);
const { title: courseTitle } = appHooks.useCardCourseData(selectedCardId);
const { formatMessage } = useIntl();
let header;
let hint;
if (isFulfilled) {
header = formatMessage(messages.changeOrLeaveHeader);
hint = formatMessage(messages.changeOrLeaveHint);
} else {
header = formatMessage(messages.selectSessionHeader, {
courseTitle,
});
hint = formatMessage(messages.selectSessionHint);
}
const updateCallback = appHooks.useUpdateSelectSessionModalCallback;
return {
showModal: selectedCardId != null,
closeSessionModal: updateCallback(dispatch, null),
openSessionModal: (cardId) => updateCallback(dispatch, cardId),
showLeaveOption: isFulfilled,
entitlementSessions,
hint,
header,
};
};
export default useSelectSessionModalData;

View File

@@ -0,0 +1,110 @@
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { hooks as appHooks } from 'data/redux';
import messages from './messages';
import * as hooks from './hooks';
jest.mock('data/redux', () => ({
hooks: {
useCardEntitlementsData: jest.fn(),
useCardCourseData: jest.fn(),
useSelectSessionModalData: jest.fn(),
useUpdateSelectSessionModalCallback: jest.fn((...args) => ({
updateSelectSession: args,
})),
},
actions: {
app: {
updateSelectSessionModal: jest.fn(),
},
},
}));
const selectedCardId = 'test-selected-card-id';
const selectSessionData = {
cardId: selectedCardId,
};
const entitlementsData = {
entitlementSessions: [
{ startDate: '1/2/2000', endDate: '1/2/2020', cardId: 'session-id-1' },
{ startDate: '2/3/2000', endDate: '2/3/2020', cardId: 'session-id-2' },
{ startDate: '3/4/2000', endDate: '3/4/2020', cardId: 'session-id-3' },
],
isFullfilled: false,
};
const cardCourseData = {
title: 'course-title: brown fox',
};
const { formatMessage } = useIntl();
const dispatch = useDispatch();
describe('SelectSessionModal hooks', () => {
let out;
beforeEach(() => {
jest.clearAllMocks();
});
describe('useSelectSession', () => {
const runHook = ({ selectSession = {}, entitlements = {}, course = {} }) => {
appHooks.useSelectSessionModalData.mockReturnValueOnce({
...selectSessionData,
...selectSession,
});
appHooks.useCardEntitlementsData.mockReturnValueOnce({
...entitlementsData,
...entitlements,
});
appHooks.useCardCourseData.mockReturnValueOnce({
...cardCourseData,
...course,
});
out = hooks.useSelectSessionModalData();
};
beforeEach(() => {
runHook({});
});
describe('initialization', () => {
test('loads entitlement data based on course number', () => {
expect(appHooks.useCardEntitlementsData).toHaveBeenCalledWith(selectedCardId);
});
test('get course title based on course number', () => {
expect(appHooks.useCardCourseData).toHaveBeenCalledWith(selectedCardId);
});
});
describe('output', () => {
test('showModal returns true if selectedCardId is not null or undefined', () => {
expect(out.showModal).toEqual(true);
runHook({ selectSession: { cardId: null } });
expect(out.showModal).toEqual(false);
runHook({ selectSession: { cardId: undefined } });
expect(out.showModal).toEqual(false);
});
test('displays change or leave header and hint if fulfilled', () => {
expect(out.header).toEqual(formatMessage(
messages.selectSessionHeader,
{ courseTitle: cardCourseData.title },
));
expect(out.hint).toEqual(formatMessage(messages.selectSessionHint));
});
test('displays select session header (w/ courseTitle) and hint if unfulfilled', () => {
runHook({ entitlements: { isFulfilled: true } });
expect(out.header).toEqual(formatMessage(messages.changeOrLeaveHeader));
expect(out.hint).toEqual(formatMessage(messages.changeOrLeaveHint));
});
test('closeSessionModal returns update callback wth dispatch and null card id', () => {
expect(out.closeSessionModal).toEqual(
appHooks.useUpdateSelectSessionModalCallback(dispatch, null),
);
});
});
});
});

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
Form,
ModalDialog,
} from '@edx/paragon';
import { nullMethod } from 'hooks';
import { dateFormatter } from 'utils';
import useSelectSessionModalData from './hooks';
import messages from './messages';
export const SelectSessionModal = () => {
const {
entitlementSessions,
showModal,
closeSessionModal,
showLeaveOption,
header,
hint,
} = useSelectSessionModalData();
const { formatMessage, formatDate } = useIntl();
return (
<ModalDialog
isOpen={showModal}
onClose={nullMethod}
hasCloseButton={false}
size="md"
className="p-4 px-4.5"
title={header}
>
<h3>{header}</h3>
<Form.Group className="pt-3">
<Form.Label>{hint}</Form.Label>
<Form.RadioSet name="sessions" className="pt-3 pb-4">
{entitlementSessions?.map((session) => (
<Form.Radio key={session.startDate} value={session.startDate}>
{dateFormatter(formatDate, session.startDate)} - {dateFormatter(formatDate, session.endDate)}
</Form.Radio>
))}
{showLeaveOption && (
<Form.Radio value="leave">
{formatMessage(messages.leaveSessionOption)}
</Form.Radio>
)}
</Form.RadioSet>
</Form.Group>
<ActionRow>
<Button variant="tertiary" onClick={closeSessionModal}>
{formatMessage(messages.nevermind)}
</Button>
<Button>{formatMessage(messages.confirmSession)}</Button>
</ActionRow>
</ModalDialog>
);
};
export default SelectSessionModal;

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import hooks from './hooks';
import SelectSessionModal from './SelectSessionModal';
import SelectSessionModal from '.';
jest.mock('./hooks', () => ({
__esModule: true,
@@ -11,10 +11,11 @@ jest.mock('./hooks', () => ({
const hookReturn = {
entitlementSessions: [],
showSessionModal: true,
closeSessionModal: jest.fn().mockName('useSelectSession.closeSessionModal'),
showLeaveSessionInSessionModal: true,
courseTitle: 'course-title: unit test save life',
showModal: true,
closeSessionModal: jest.fn().mockName('useSelectSessionModalData.closeSessionModal'),
showLeaveOption: true,
header: 'test-header',
hint: 'test-hint',
};
const courseNumber = 'my-test-course-number';
@@ -31,7 +32,7 @@ describe('SelectSessionModal', () => {
hooks.mockReturnValueOnce({
...hookReturn,
});
expect(shallow(<SelectSessionModal courseNumber={courseNumber} />)).toMatchSnapshot();
expect(shallow(<SelectSessionModal />)).toMatchSnapshot();
});
test('modal with leave option ', () => {
@@ -39,16 +40,16 @@ describe('SelectSessionModal', () => {
...hookReturn,
entitlementSessions: [...availableSessions],
});
expect(shallow(<SelectSessionModal courseNumber={courseNumber} />)).toMatchSnapshot();
expect(shallow(<SelectSessionModal />)).toMatchSnapshot();
});
test('modal without leave option ', () => {
hooks.mockReturnValueOnce({
...hookReturn,
entitlementSessions: [...availableSessions],
showLeaveSessionInSessionModal: false,
showLeaveOption: false,
});
expect(shallow(<SelectSessionModal courseNumber={courseNumber} />)).toMatchSnapshot();
expect(shallow(<SelectSessionModal />)).toMatchSnapshot();
});
});
});

View File

@@ -10,7 +10,7 @@ export const messages = StrictDict({
selectSessionHeader: {
id: 'learner-dash.selectSession.selectSessionHeader',
description: 'Header for unfulfilled entitlement',
defaultMessage: 'Select a session to access {courseTitle}',
defaultMessage: 'Select a session',
},
changeOrLeaveHint: {
id: 'learner-dash.selectSession.changeOrLeaveHint',

View File

@@ -1,8 +1,8 @@
@import "@edx/paragon/scss/core/core";
.widget-sidebar {
margin-top: map-get($spacers, 5);
width: 400px;
flex-shrink: 0;
padding-left: map-get($spacers, 2);
padding-right: map-get($spacers, 2);
}
}

View File

@@ -1,4 +1,20 @@
import { getConfig } from '@edx/frontend-platform';
import { StrictDict } from 'utils';
export const routePath = `${getConfig().PUBLIC_PATH}:courseId`;
export const locationId = window.location.pathname.slice(1);
export const SortKeys = StrictDict({
enrolled: 'enrolled',
title: 'title',
});
export const FilterKeys = StrictDict({
inProgress: 'inProgress',
notStarted: 'notStarted',
done: 'done',
notEnrolled: 'notEnrolled',
upgraded: 'upgraded',
});
export const ListPageSize = 50;

View File

@@ -10,30 +10,25 @@ const initialState = {
platformSettings: {},
suggestedCourses: [],
filterState: {},
selectSessionsModal: {},
selectSessionModal: {},
};
export const cardId = (val) => `card-${val}`;
// eslint-disable-next-line no-unused-vars
const app = createSlice({
name: 'app',
initialState,
reducers: {
loadCourses: (state, { payload: { enrollments, entitlements } }) => ({
loadCourses: (state, { payload: { courses } }) => ({
...state,
enrollments: [
...enrollments.map(curr => curr.courseRun.courseNumber),
...entitlements.map(curr => curr.courseRun.courseNumber),
],
courseData: {
...entitlements.reduce(
(obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
{},
),
...enrollments.reduce(
(obj, curr) => ({ ...obj, [curr.courseRun.courseNumber]: curr }),
{},
),
},
courseData: courses.reduce(
(obj, curr, index) => ({
...obj,
[cardId(index)]: { ...curr, cardId: cardId(index) },
}),
{},
),
}),
loadGlobalData: (state, { payload }) => ({
...state,
@@ -44,9 +39,7 @@ const app = createSlice({
}),
updateSelectSessionModal: (state, { payload }) => ({
...state,
selectSessionsModal: {
...payload,
},
selectSessionModal: { cardId: payload },
}),
},
});

View File

@@ -1,4 +1,6 @@
import { initialState, reducer, actions } from './reducer';
import {
cardId, initialState, reducer, actions,
} from './reducer';
describe('app reducer', () => {
describe('reducers', () => {
@@ -12,13 +14,6 @@ describe('app reducer', () => {
},
entitlements: [],
};
// const testValue = 'my-test-value';
// const testAction = (action, expected) => {
// expect(reducer(testState, action)).toEqual({
// ...testState,
// ...expected,
// });
// };
describe('action handlers', () => {
describe('loadCourses', () => {
const courseIds = [
@@ -32,29 +27,29 @@ describe('app reducer', () => {
];
const enrollmentData = [
{
courseRun: { courseNumber: courseIds[0] },
courseRun: { cardId: courseIds[0] },
course: 1,
some: 'data',
},
{
courseRun: { courseNumber: courseIds[1] },
courseRun: { cardId: courseIds[1] },
course: 2,
some: 'other data',
},
{
courseRun: { courseNumber: courseIds[2] },
courseRun: { cardId: courseIds[2] },
course: 3,
some: 'still different data',
},
];
const entitlementData = [
{
courseRun: { courseNumber: entitlementIds[0] },
courseRun: { cardId: entitlementIds[0] },
course: 4,
some: 'STILL different data',
},
{
courseRun: { courseNumber: entitlementIds[1] },
courseRun: { cardId: entitlementIds[1] },
course: 5,
some: 'still DIFFERENT data',
},
@@ -62,23 +57,16 @@ describe('app reducer', () => {
let out;
beforeEach(() => {
out = reducer(testState, actions.loadCourses({
enrollments: enrollmentData,
entitlements: entitlementData,
courses: [...enrollmentData, ...entitlementData],
}));
});
it('loads list of courseRun ids into enrollments field', () => {
expect(out.enrollments).toEqual([
...courseIds,
...entitlementIds,
]);
});
it('loads object keyed by courseRun ids into courseData field', () => {
expect(out.courseData).toEqual({
[courseIds[0]]: enrollmentData[0],
[courseIds[1]]: enrollmentData[1],
[courseIds[2]]: enrollmentData[2],
[entitlementIds[0]]: entitlementData[0],
[entitlementIds[1]]: entitlementData[1],
[cardId(0)]: { ...enrollmentData[0], cardId: cardId(0) },
[cardId(1)]: { ...enrollmentData[1], cardId: cardId(1) },
[cardId(2)]: { ...enrollmentData[2], cardId: cardId(2) },
[cardId(3)]: { ...entitlementData[0], cardId: cardId(3) },
[cardId(4)]: { ...entitlementData[1], cardId: cardId(4) },
});
});
});

View File

@@ -1,6 +1,7 @@
import { createSelector } from 'reselect';
import { StrictDict } from 'utils';
import { FilterKeys } from 'data/constants/app';
import * as module from './selectors';
@@ -10,24 +11,36 @@ const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb);
// top-level app data selectors
export const simpleSelectors = {
enrollments: mkSimpleSelector(app => app.enrollments),
entitlements: mkSimpleSelector(app => app.entitlements),
courseData: mkSimpleSelector(app => app.courseData),
platformSettings: mkSimpleSelector(app => app.platformSettings),
suggestedCourses: mkSimpleSelector(app => app.suggestedCourses),
emailConfirmation: mkSimpleSelector(app => app.emailConfirmation),
enterpriseDashboards: mkSimpleSelector(app => app.enterpriseDashboards),
selectSessionsModal: mkSimpleSelector(app => app.selectSessionsModal),
selectSessionModal: mkSimpleSelector(app => app.selectSessionModal),
};
export const courseCardData = (state, courseNumber) => (
module.simpleSelectors.courseData(state)[courseNumber]
export const numCourses = createSelector(
[module.simpleSelectors.courseData],
(courseData) => Object.keys(courseData).length,
);
export const hasCourses = createSelector([module.numCourses], (num) => num > 0);
export const hasAvailableDashboards = createSelector(
[module.simpleSelectors.enterpriseDashboards],
(data) => !!data.availableDashboards,
);
const mkCardSelector = (sel) => (state, courseNumber) => (
sel(courseCardData(state, courseNumber))
export const courseCardData = (state, cardId) => (
module.simpleSelectors.courseData(state)[cardId]
);
const mkCardSelector = (sel) => (state, cardId) => {
const cardData = module.courseCardData(state, cardId);
if (cardData) {
return sel(cardData);
}
return {};
};
const dateSixMonthsFromNow = new Date();
dateSixMonthsFromNow.setDate(dateSixMonthsFromNow.getDate() + 180);
@@ -43,28 +56,41 @@ export const courseCard = StrictDict({
})),
course: mkCardSelector(({ course }) => ({
bannerUrl: course.bannerUrl,
courseNumber: course.courseNumber,
title: course.title,
website: course.website,
})),
courseRun: mkCardSelector(({ courseRun }) => ({
courseRun: mkCardSelector(({ courseRun }) => (courseRun === null ? {} : {
endDate: courseRun?.endDate,
courseId: courseRun.courseId,
isArchived: courseRun.isArchived,
isStarted: courseRun.isStarted,
isFinished: courseRun.isFinished,
minPassingGrade: courseRun.minPassingGrade,
})),
enrollment: mkCardSelector(({ enrollment }) => ({
accessExpirationDate: enrollment.accessExpirationDate,
canUpgrade: enrollment.canUpgrade,
hasStarted: enrollment.hasStarted,
isAudit: enrollment.isAudit,
isAuditAccessExpired: enrollment.isAuditAccessExpired,
isEmailEnabled: enrollment.isEmailEnabled,
isVerified: enrollment.isVerified,
lastEnrolled: enrollment.lastEnrollment,
isEnrolled: enrollment.isEnrolled,
})),
enrollment: mkCardSelector(({ enrollment }) => {
if (enrollment == null) {
return {
isEnrolled: false,
};
}
return {
accessExpirationDate: enrollment.accessExpirationDate,
canUpgrade: enrollment.canUpgrade,
hasStarted: enrollment.hasStarted,
hasFinished: enrollment.hasFinished,
isAudit: enrollment.isAudit,
isAuditAccessExpired: enrollment.isAuditAccessExpired,
isEmailEnabled: enrollment.isEmailEnabled,
isVerified: enrollment.isVerified,
lastEnrolled: enrollment.lastEnrollment,
isEnrolled: enrollment.isEnrolled,
};
}),
entitlements: mkCardSelector(({ entitlements }) => {
if (!entitlements) {
return {};
}
const deadline = new Date(entitlements.changeDeadline);
const showExpirationWarning = deadline > new Date() && deadline <= dateSixMonthsFromNow;
return {
@@ -96,7 +122,71 @@ export const courseCard = StrictDict({
})),
});
export const currentList = (state, {
sortBy,
isAscending,
filters,
pageNumber,
pageSize,
}) => {
let list = Object.values(module.simpleSelectors.courseData(state));
if (filters.length) {
list = list.filter(course => {
if (filters.includes(FilterKeys.notEnrolled)) {
if (!course.enrollment.isEnrolled) {
return false;
}
}
if (filters.includes(FilterKeys.done)) {
if (!course.enrollment.hasFinished) {
return false;
}
}
if (filters.includes(FilterKeys.upgraded)) {
if (!course.enrollment.isVerified) {
return false;
}
}
if (filters.includes(FilterKeys.inProgress)) {
if (!course.enrollment.hasStarted) {
return false;
}
}
if (filters.includes(FilterKeys.notStarted)) {
if (course.enrollment.hasStarted) {
return false;
}
}
return true;
});
}
if (sortBy === 'enrolled') {
list = list.sort((a, b) => {
const dateA = new Date(a.enrollment.lastEnrolled);
const dateB = new Date(b.enrollment.lastEnrolled);
if (dateA < dateB) { return isAscending ? -1 : 1; }
if (dateA > dateB) { return isAscending ? 1 : 1; }
return 0;
});
} else {
list = list.sort((a, b) => {
const titleA = a.course.title.toLowerCase();
const titleB = b.course.title.toLowerCase();
if (titleA < titleB) { return isAscending ? -1 : 1; }
if (titleA > titleB) { return isAscending ? 1 : 1; }
return 0;
});
}
return {
visible: list.slice((pageNumber - 1) * pageSize, pageNumber * pageSize),
numPages: Math.ceil(list.length / pageSize),
};
};
export default StrictDict({
...simpleSelectors,
courseCard,
currentList,
hasCourses,
hasAvailableDashboards,
});

View File

@@ -1,5 +1,6 @@
import { useSelector } from 'react-redux';
import { actions as appActions } from './app/reducer';
import appSelectors from './app/selectors';
const { courseCard } = appSelectors;
@@ -9,11 +10,17 @@ export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpr
export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings);
// suggested courses is max at 3 at the moment.
export const useSuggestedCoursesData = () => useSelector(appSelectors.suggestedCourses).slice(0, 3);
export const useSelectSessionsModalData = () => useSelector(appSelectors.selectSessionsModal);
export const useSelectSessionModalData = () => useSelector(appSelectors.selectSessionModal);
export const useHasCourses = () => useSelector(appSelectors.hasCourses);
export const useHasAvailableDashboards = () => useSelector(appSelectors.hasAvailableDashboards);
export const useCurrentCourseList = (opts) => useSelector(
state => appSelectors.currentList(state, opts),
);
// eslint-disable-next-line
export const useCourseCardData = (selector) => (courseNumber) => useSelector(
(state) => selector(state, courseNumber),
export const useCourseCardData = (selector) => (cardId) => useSelector(
(state) => selector(state, cardId),
);
export const useCardCertificateData = useCourseCardData(courseCard.certificates);
@@ -24,3 +31,7 @@ export const useCardEntitlementsData = useCourseCardData(courseCard.entitlements
export const useCardGradeData = useCourseCardData(courseCard.grades);
export const useCardProviderData = useCourseCardData(courseCard.provider);
export const useCardRelatedProgramsData = useCourseCardData(courseCard.relatedPrograms);
export const useUpdateSelectSessionModalCallback = (dispatch, cardId) => () => dispatch(
appActions.updateSelectSessionModal(cardId),
);

View File

@@ -14,8 +14,8 @@ import requests from './requests';
*/
export const initialize = () => (dispatch) => (
dispatch(requests.initializeList({
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
dispatch(actions.app.loadCourses({ enrollments, entitlements }));
onSuccess: (({ courses, ...globalData }) => {
dispatch(actions.app.loadCourses({ courses }));
dispatch(actions.app.loadGlobalData(globalData));
}),
}))
@@ -23,8 +23,8 @@ export const initialize = () => (dispatch) => (
export const refreshList = () => (dispatch) => (
dispatch(requests.initializeList({
onSuccess: (({ enrollments, entitlements, ...globalData }) => {
dispatch(actions.app.loadCourses({ enrollments, entitlements }));
onSuccess: (({ courses, ...globalData }) => {
dispatch(actions.app.loadCourses({ courses }));
dispatch(actions.app.loadGlobalData(globalData));
}),
}))

View File

@@ -16,8 +16,10 @@ import {
* GET Actions
*********************************************************************************/
const initializeList = () => Promise.resolve({
enrollments: fakeData.courseRunData,
entitlements: fakeData.entitlementData,
courses: [
...fakeData.courseRunData,
...fakeData.entitlementData,
],
...fakeData.globalData,
});

View File

@@ -37,7 +37,9 @@ export const relatedPrograms = [
},
];
export const genCourseID = (index) => `course-id${index}`;
export const genCardId = (index) => `card-id${index}`;
export const genCourseId = (index) => `course-number${index}-course-id${index}`;
export const genCourseNumber = (index) => `course-number${index}`;
export const genCourseTitle = (index) => `Course Name ${index}`;
const logos = {
@@ -59,7 +61,6 @@ const globalData = {
},
enterpriseDashboards: {
availableDashboards: [
{ label: 'Personal', url: '/dashboard' },
{ label: 'edX, Inc.', url: '/edx-dashboard' },
{ label: 'Harvard', url: '/harvard-dashboard' },
],
@@ -112,15 +113,15 @@ export const genCourseRunData = (data = {}) => ({
});
export const genEnrollmentData = (data = {}) => ({
accessExpirationDate: futureDate,
canUpgrade: data.verified ? null : true,
accessExpirationDate: ((data.isEnrolled === false) ? null : futureDate),
canUpgrade: (data.isVerified ? null : true),
hasFinished: false,
hasStarted: false,
isAudit: true,
isAuditAccessExpired: data.verified ? null : false,
isAudit: !data.isVerified || data.isEnrolled,
isAuditAccessExpired: data.isVerified ? null : false,
isEmailEnabled: false,
isEnrolled: true,
isVerified: false,
lastEnrolled: pastDate,
...data,
});
@@ -137,150 +138,169 @@ export const genCertificateData = (data = {}) => ({
});
export const availableSessions = [
{ startDate: '1/2/2000', endDate: '1/2/2020', courseNumber: genCourseID(100) },
{ startDate: '2/3/2000', endDate: '2/3/2020', courseNumber: genCourseID(101) },
{ startDate: '3/4/2000', endDate: '3/4/2020', courseNumber: genCourseID(102) },
{ startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) },
{ startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) },
{ startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) },
{ startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) },
{ startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) },
{ startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) },
{ startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) },
{ startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) },
{ startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) },
{ startDate: '1/2/2000', endDate: '1/2/2020', courseId: genCourseId(1000) },
{ startDate: '2/3/2000', endDate: '2/3/2020', courseId: genCourseId(1001) },
{ startDate: '3/4/2000', endDate: '3/4/2020', courseId: genCourseId(1002) },
];
export const courseRuns = [
// audit, pending, can upgrade
// audit, can upgrade, course not started,
{},
// audit, can upgrade, course started
{
enrollment: genEnrollmentData({ isAudit: true }),
grades: { isPassing: true },
courseRun: { isPending: true },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
},
// audit, started, cannot upgrade, restricted
{
enrollment: genEnrollmentData({ isAudit: true, canUpgrade: false }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData({ isRestricted: true }),
entitlements: { isEntitlement: false },
},
// audit, started, can upgrade
// audit, can upgrade, course started, learner started
{
enrollment: genEnrollmentData({ isAudit: true, canUpgrade: true }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
enrollment: { hasStarted: true },
},
// audit, started, not passing
// audit, can upgrade, course started, learner started, not passing
{
enrollment: genEnrollmentData({ isAudit: true, canUpgrade: true }),
courseRun: { isStarted: true },
enrollment: { hasStarted: true },
grades: { isPassing: false },
},
// audit, access expired, can upgrade, course started, learner started
{
courseRun: { isStarted: true },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
},
// audit, started, audit access expired, can upgrade
{
enrollment: genEnrollmentData({ isAudit: true, isAuditAccessExpired: true }),
grades: { isPassing: true },
courseRun: { isStarted: true, accessExpirationDate: pastDate },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
},
// audit, started, audit access expired, cannot upgrade
{
enrollment: genEnrollmentData({
isAudit: true,
enrollment: {
hasStarted: true,
isAuditAccessExpired: true,
accessExpirationDate: pastDate,
},
},
// audit, access expired, cannot upgrade, course started, learner started
{
courseRun: { isStarted: true },
enrollment: {
accessExpirationDate: pastDate,
isAuditAccessExpired: true,
hasStarted: true,
canUpgrade: false,
}),
grades: { isPassing: true },
courseRun: { isStarted: true, accessExpirationDate: pastDate },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
},
},
// verified, pending, restricted
// verified, course not started
{ enrollment: { isVerified: true } },
// verified, course started, learner not started
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isPending: true },
certificates: genCertificateData({ isRestricted: true }),
entitlements: { isEntitlement: false },
},
// verified, started
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isStarted: true },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
enrollment: { isVerified: true },
},
// verified, not passing
// verified, course started, learner started, passing
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
courseRun: { isStarted: true },
enrollment: { hasStarted: true, isVerified: true },
},
// verified, course started, learner started, not passing
{
courseRun: { isStarted: true },
grades: { isPassing: false },
courseRun: { isStarted: true },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
enrollment: { hasStarted: true, isVerified: true },
},
// verified, finished, not passing
// verified, learner started, not finished, not passing
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
enrollment: { hasStarted: true, isVerified: true },
grades: { isPassing: false },
courseRun: { isArchived: true, endDate: pastDate },
certificates: genCertificateData(),
entitlements: { isEntitlement: false },
},
// verified, restricted
// verified, learner finished, passing, restricted
{
enrollment: genEnrollmentData({ isVerified: true }),
grades: { isPassing: true },
enrollment: {
hasFinished: true,
hasStarted: true,
isVerified: true,
},
courseRun: { isStarted: true },
certificates: genCertificateData({ isRestricted: true }),
entitlements: { isEntitlement: false },
certificates: { isRestricted: true },
},
// verified, earned but not available
// verified, learner finished, passing, cert earned but not available
{
enrollment: genEnrollmentData({ isVerified: true }),
grades: { isPassing: true },
enrollment: {
hasFinished: true,
hasStarted: true,
isVerified: true,
},
courseRun: { isStarted: true },
certificates: genCertificateData({
certificates: {
isEarned: true,
availableDate: futureDate,
}),
entitlements: { isEntitlement: false },
isAvailable: false,
},
},
// verified, earned, downloadable (web + link)
// verified, learner finished, cert earned, downloadable (web + link)
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
enrollment: {
hasFinished: true,
hasStarted: true,
isVerified: true,
},
courseRun: { isStarted: true },
certificates: genCertificateData({
certificates: {
isEarned: true,
isAvailable: true,
isDownloadable: true,
availableDate: pastDate,
certDownloadUrl: logos.social,
certPreviewUrl: logos.edx,
}),
entitlements: { isEntitlement: false },
},
},
// verified, earned, downloadable (link)
// verified, learner finished, cert earned, downloadable (link only)
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
enrollment: {
hasFinished: true,
hasStarted: true,
isVerified: true,
},
courseRun: { isStarted: true },
certificates: genCertificateData({
certificates: {
isEarned: true,
isAvailable: true,
isDownloadable: true,
availableDate: pastDate,
certDownloadUrl: logos.social,
}),
entitlements: { isEntitlement: false },
},
},
// verified, course archived, learner finished, cert earned, downloadable (web + link)
{
enrollment: {
hasFinished: true,
hasStarted: true,
isVerified: true,
},
courseRun: {
isStarted: true,
isArchived: true,
endDate: pastDate,
},
certificates: {
isEarned: true,
isAvailable: true,
isDownloadable: true,
availableDate: pastDate,
certDownloadUrl: logos.social,
certPreviewUrl: logos.edx,
},
},
// verified, course archived, learner started, not finished, not passing
{
enrollment: { hasStarted: true, isVerified: true },
grades: { isPassing: false },
courseRun: { isArchived: true, endDate: pastDate },
},
// Entitlement Course Run - Cannot view yet
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: { isPending: true },
certificates: genCertificateData(),
enrollment: { isVerified: true },
courseRun: { isStarted: false },
entitlements: {
isEntitlement: true,
isFulfilled: true,
@@ -294,10 +314,8 @@ export const courseRuns = [
},
// Entitlement Course Run - Can View and Change
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
enrollment: { isVerified: true },
courseRun: { isStarted: true },
certificates: genCertificateData(),
entitlements: {
isEntitlement: true,
isFulfilled: true,
@@ -311,10 +329,8 @@ export const courseRuns = [
},
// Entitlement Course Run - Can View but not Change
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
enrollment: { isVerified: true },
courseRun: { isStarted: true },
certificates: genCertificateData(),
entitlements: {
isEntitlement: true,
isFulfilled: true,
@@ -327,14 +343,8 @@ export const courseRuns = [
},
// Entitlement Course Run - Expired
{
enrollment: genEnrollmentData({ isAudit: false, isVerified: true }),
grades: { isPassing: true },
courseRun: {
isStarted: true,
isArchived: true,
endDate: pastDate,
},
certificates: genCertificateData(),
enrollment: { isVerified: true },
courseRun: { isStarted: true, isArchived: true, endDate: pastDate },
entitlements: {
isEntitlement: true,
isFulfilled: true,
@@ -406,28 +416,41 @@ export const entitlementCourses = [
export const courseRunData = courseRuns.map(
(data, index) => {
const title = genCourseTitle(index);
const courseNumber = genCourseID(index);
const cardId = genCardId(index);
const courseId = genCourseId(index);
const courseNumber = genCourseNumber(index);
const providerIndex = index % 3;
const lastEnrolled = new Date();
lastEnrolled.setDate(lastEnrolled.getDate() - index);
const iteratedData = [
{
provider: providers.edx,
course: { title, bannerUrl: logos.edx },
course: { title, bannerUrl: logos.edx, courseNumber },
relatedPrograms,
},
{
provider: providers.mit,
course: { title, bannerUrl: logos.science },
course: { title, bannerUrl: logos.science, courseNumber },
relatedPrograms: [relatedPrograms[0]],
},
{
provider: null,
course: { title, bannerUrl: logos.social },
course: { title, bannerUrl: logos.social, courseNumber },
relatedPrograms: [],
},
];
return {
cardId,
grades: { isPassing: true },
entitlements: null,
...data,
courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
certificates: genCertificateData(data.certificates),
enrollment: genEnrollmentData(data.enrollment),
courseRun: genCourseRunData({
...data.courseRun,
courseId,
lastEnrolled,
}),
...iteratedData[providerIndex],
};
},
@@ -436,31 +459,44 @@ export const courseRunData = courseRuns.map(
export const entitlementData = entitlementCourses.map(
(data, index) => {
const title = genCourseTitle(100 + index);
const courseNumber = genCourseID(100 + index);
const cardId = genCardId(100 + index);
const courseNumber = genCourseNumber(100 + index);
const providerIndex = index % 3;
const iteratedData = [
{
provider: providers.edx,
course: { title, bannerUrl: logos.edx },
course: { courseNumber, title, bannerUrl: logos.edx },
relatedPrograms,
},
{
provider: providers.mit,
course: { title, bannerUrl: logos.science },
course: { courseNumber, title, bannerUrl: logos.science },
relatedPrograms: [relatedPrograms[0]],
},
{
provider: null,
course: { title, bannerUrl: logos.social },
course: { courseNumber, title, bannerUrl: logos.social },
relatedPrograms: [],
},
];
return {
cardId,
enrollment: genEnrollmentData({
isEnrolled: false,
lastEnrolled: null,
accessExpirationDate: null,
canUpgrade: false,
hasFinished: false,
hasStarted: false,
isAudit: false,
isAuditAccessExpired: false,
isEmailEnabled: false,
isVerified: false,
}),
grades: null,
certificates: null,
courseRun: null,
...data,
enrollment: genEnrollmentData(),
grades: { isPassing: true },
certificates: genCertificateData(),
courseRun: genCourseRunData({ ...data.courseRun, courseNumber }),
...iteratedData[providerIndex],
};
},

View File

@@ -14,7 +14,7 @@ export const shapes = StrictDict({
}),
courseRun: PropTypes.shape({
accessExpirationDate: PropTypes.string,
courseNumber: PropTypes.string,
cardId: PropTypes.string,
isArchived: PropTypes.bool,
isFinished: PropTypes.bool,
isPending: PropTypes.bool,
@@ -43,7 +43,7 @@ export const shapes = StrictDict({
availableSessions: PropTypes.shape({
startDate: PropTypes.string,
endDate: PropTypes.string,
courseNumber: PropTypes.string,
cardId: PropTypes.string,
}),
canChange: PropTypes.bool,
isFulfilled: PropTypes.bool,

View File

@@ -125,6 +125,7 @@ jest.mock('@edx/paragon/icons', () => ({
}));
jest.mock('data/constants/app', () => ({
...jest.requireActual('data/constants/app'),
locationId: 'fake-location-id',
}));

View File

@@ -20,6 +20,7 @@ import { RequestKeys, RequestStates } from 'data/constants/requests';
import reducers from 'data/redux';
import messages from 'i18n';
import { selectors } from 'data/redux';
import { cardId as genCardId } from 'data/redux/app/reducer';
import App from 'App';
import Inspector from './inspector';
@@ -79,8 +80,10 @@ const mockApi = () => {
(resolve, reject) => {
resolveFns.init = {
success: () => resolve({
enrollments: fakeData.courseRunData,
entitlements: fakeData.entitlementData,
courses: [
...fakeData.courseRunData,
...fakeData.entitlementData,
],
...fakeData.globalData,
}),
};
@@ -130,27 +133,28 @@ describe('ESG app integration tests', () => {
await inspector.findByText(fakeData.courseRunData[0].course.title);
const cards = inspector.get.courseCards;
let card = cards.at(0);
let courseNumber;
let cardId;
let courseData;
let cardDetails;
await getState();
// Card 1 is Audit, pending, and can upgrade
courseNumber = state.app.enrollments[0];
courseData = state.app.courseData[courseNumber];
cardId = genCardId(0);
courseData = state.app.courseData[cardId];
expect(courseData.enrollment.isAudit).toEqual(true);
expect(courseData.courseRun.isPending).toEqual(true);
expect(courseData.courseRun.isStarted).toEqual(false);
expect(courseData.enrollment.canUpgrade).toEqual(true);
let card = cards.at(0);
inspector.verifyText(
inspector.get.card.header(card),
courseData.course.title,
);
cardDetails = inspector.get.card.details(card);
[
courseData.provider.name,
courseNumber,
courseData.course.courseNumber,
appMessages.withValues.CourseCardDetails.accessExpires({
accessExpirationDate: courseData.enrollment.accessExpirationDate,
}),

View File

@@ -27,9 +27,9 @@ class Inspector {
card: {
header: (card) => within(card).getByTestId('CourseCardTitle'),
details: (card) => within(card).getByTestId('CourseCardDetails'),
banners: (card) => within(card).getByTestId('CourseCardBanners'),
programsBadge: (card) => within(card).getByTestId('RelatedProgramsBadge'),
actions: (card) => within(card).getByTestId('CourseCardActions'),
// banners: (card) => within(card).getByTestId('CourseCardBanners'),
// programsBadge: (card) => within(card).getByTestId('RelatedProgramsBadge'),
// actions: (card) => within(card).getByTestId('CourseCardActions'),
},
};
}