feat: local pagination/sort and api updates
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
44
src/containers/CourseFilterControls/ActiveCourseFilters.jsx
Normal file
44
src/containers/CourseFilterControls/ActiveCourseFilters.jsx
Normal 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;
|
||||
82
src/containers/CourseFilterControls/CourseFilterControls.jsx
Normal file
82
src/containers/CourseFilterControls/CourseFilterControls.jsx
Normal 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;
|
||||
21
src/containers/CourseFilterControls/components/Checkbox.jsx
Normal file
21
src/containers/CourseFilterControls/components/Checkbox.jsx
Normal 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;
|
||||
@@ -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;
|
||||
39
src/containers/CourseFilterControls/components/SortForm.jsx
Normal file
39
src/containers/CourseFilterControls/components/SortForm.jsx
Normal 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;
|
||||
29
src/containers/CourseFilterControls/hooks.js
Normal file
29
src/containers/CourseFilterControls/hooks.js
Normal 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;
|
||||
6
src/containers/CourseFilterControls/index.jsx
Normal file
6
src/containers/CourseFilterControls/index.jsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import CourseFilterControls from './CourseFilterControls';
|
||||
|
||||
export { default as ActiveCourseFilters } from './ActiveCourseFilters';
|
||||
|
||||
export { CourseFilterControls };
|
||||
export default CourseFilterControls;
|
||||
20
src/containers/CourseFilterControls/index.scss
Normal file
20
src/containers/CourseFilterControls/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
55
src/containers/CourseFilterControls/messages.js
Normal file
55
src/containers/CourseFilterControls/messages.js
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
11
src/containers/CourseList/messages.js
Normal file
11
src/containers/CourseList/messages.js
Normal 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;
|
||||
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
4
src/containers/Dashboard/index.scss
Normal file
4
src/containers/Dashboard/index.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
#course-list-heading-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
21
src/containers/EnterpriseDashboardModal/hooks.js
Normal file
21
src/containers/EnterpriseDashboardModal/hooks.js
Normal 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;
|
||||
44
src/containers/EnterpriseDashboardModal/hooks.test.js
Normal file
44
src/containers/EnterpriseDashboardModal/hooks.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
55
src/containers/EnterpriseDashboardModal/index.jsx
Normal file
55
src/containers/EnterpriseDashboardModal/index.jsx
Normal 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;
|
||||
22
src/containers/EnterpriseDashboardModal/index.test.jsx
Normal file
22
src/containers/EnterpriseDashboardModal/index.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
@@ -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>
|
||||
|
||||
@@ -24,6 +24,7 @@ export const useConfirmEmailBannerData = () => {
|
||||
const openConfirmModalButtonClick = () => {
|
||||
dispatch(thunkActions.app.sendConfirmEmail());
|
||||
openConfirmModal();
|
||||
closePageBanner();
|
||||
};
|
||||
|
||||
const userConfirmEmailButtonClick = () => {
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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`] = `""`;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
46
src/containers/SelectSessionModal/hooks.js
Normal file
46
src/containers/SelectSessionModal/hooks.js
Normal 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;
|
||||
110
src/containers/SelectSessionModal/hooks.test.js
Normal file
110
src/containers/SelectSessionModal/hooks.test.js
Normal 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
64
src/containers/SelectSessionModal/index.jsx
Normal file
64
src/containers/SelectSessionModal/index.jsx
Normal 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;
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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) },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -16,8 +16,10 @@ import {
|
||||
* GET Actions
|
||||
*********************************************************************************/
|
||||
const initializeList = () => Promise.resolve({
|
||||
enrollments: fakeData.courseRunData,
|
||||
entitlements: fakeData.entitlementData,
|
||||
courses: [
|
||||
...fakeData.courseRunData,
|
||||
...fakeData.entitlementData,
|
||||
],
|
||||
...fakeData.globalData,
|
||||
});
|
||||
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -125,6 +125,7 @@ jest.mock('@edx/paragon/icons', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('data/constants/app', () => ({
|
||||
...jest.requireActual('data/constants/app'),
|
||||
locationId: 'fake-location-id',
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user