chore: testability updates
This commit is contained in:
@@ -15,7 +15,7 @@ Banner.defaultProps = {
|
||||
};
|
||||
Banner.propTypes = {
|
||||
variant: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
icon: PropTypes.func,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import { CheckCircle } from '@edx/paragon/icons';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
const { cardData } = selectors;
|
||||
|
||||
const restrictedMessage = 'Your Certificate of Achievement is being held pending confirmation that the issuance of your Certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria, and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting ';
|
||||
|
||||
export const CertificateBanner = ({ cardData }) => {
|
||||
const {
|
||||
certificates,
|
||||
courseRun,
|
||||
enrollment,
|
||||
grades,
|
||||
} = cardData;
|
||||
if (certificates.isRestricted) {
|
||||
if (enrollment.isAudit) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{restrictedMessage}<Hyperlink>info@example.com</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
export const CertificateBanner = ({ courseNumber }) => {
|
||||
const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber));
|
||||
|
||||
const isRestricted = cardValue(cardData.isRestricted);
|
||||
const isAudit = cardValue(cardData.isAudit);
|
||||
const isVerified = cardValue(cardData.isVerified);
|
||||
if (isRestricted) {
|
||||
return (
|
||||
<Banner variant="danger">
|
||||
{restrictedMessage}<Hyperlink>info@example.com</Hyperlink>
|
||||
If you would like a refund on your Certificate of Achievement, please contact our billing address <Hyperlink>billing@example.com</Hyperlink>
|
||||
{isVerified && (
|
||||
<>
|
||||
If you would like a refund on your Certificate of Achievement, please contact our billing address <Hyperlink>billing@example.com</Hyperlink>
|
||||
</>
|
||||
)}
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (!grades.isPassing) {
|
||||
if (enrollment.isAudit) {
|
||||
return (
|
||||
<Banner>
|
||||
Grade required to pass the course: {courseRun.minPassingGrade}%
|
||||
</Banner>
|
||||
);
|
||||
const isPassing = cardValue(cardData.isPassing);
|
||||
const minPassingGrade = cardValue(cardData.minPassingGrade);
|
||||
const isCourseRunFinished = cardValue(cardData.isCourseRunFinished);
|
||||
const isCertDownloadable = cardValue(cardData.isCertDownloadable);
|
||||
const isCertEarnedButUnavailable = cardValue(cardData.isCertEarnedButUnavailable);
|
||||
const certAvailableDate = cardValue(cardData.certAvailableDate);
|
||||
if (!isPassing) {
|
||||
if (isAudit) {
|
||||
return (<Banner> Grade required to pass the course: {minPassingGrade}% </Banner>);
|
||||
}
|
||||
if (courseRun.isFinished) {
|
||||
if (isCourseRunFinished) {
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
You are not eligible for a certificate.
|
||||
{' '}
|
||||
<Hyperlink>View grades.</Hyperlink>
|
||||
You are not eligible for a certificate. <Hyperlink>View grades.</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Banner variant="warning">
|
||||
Grade required for a certificate: {courseRun.minPassingGrade}%
|
||||
Grade required for a certificate: {minPassingGrade}%
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificates.isDownloadable) {
|
||||
if (certificates.downloadUrls.preview) {
|
||||
if (isCertDownloadable) {
|
||||
const certDownloadUrl = cardValue(cardData.certDownloadUrl);
|
||||
const certPreviewUrl = cardValue(cardData.certPreviewUrl);
|
||||
if (certPreviewUrl) {
|
||||
return (
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
Congratulations. Your certificate is ready.
|
||||
{' '}
|
||||
<Hyperlink>View Certificate.</Hyperlink>
|
||||
<Hyperlink href={certPreviewUrl}>View Certificate.</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
@@ -70,14 +70,14 @@ export const CertificateBanner = ({ cardData }) => {
|
||||
<Banner variant="success" icon={CheckCircle}>
|
||||
Congratulations. Your certificate is ready.
|
||||
{' '}
|
||||
<Hyperlink>Download Certificate.</Hyperlink>
|
||||
<Hyperlink href={certDownloadUrl}>Download Certificate.</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (certificates.isEarned && !certificates.isAvailable) {
|
||||
if (isCertEarnedButUnavailable) {
|
||||
return (
|
||||
<Banner>
|
||||
Your grade and certificate will be ready after {certificates.availableDate}.
|
||||
Your grade and certificate will be ready after {certAvailableDate}.
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export const CertificateBanner = ({ cardData }) => {
|
||||
return null;
|
||||
};
|
||||
CertificateBanner.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CertificateBanner;
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
/* eslint-disable max-len */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
export const CourseBanner = ({ cardData }) => {
|
||||
const {
|
||||
// course,
|
||||
enrollment,
|
||||
courseRun,
|
||||
} = cardData;
|
||||
if (enrollment.isVerified) {
|
||||
return null;
|
||||
}
|
||||
const isActive = courseRun.isStarted && !courseRun.isFinished;
|
||||
const { canUpgrade, isAuditAccessExpired } = enrollment;
|
||||
const { cardData } = selectors;
|
||||
|
||||
export const CourseBanner = ({ courseNumber }) => {
|
||||
const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber));
|
||||
const isVerified = cardValue(cardData.isVerified);
|
||||
if (isVerified) { return null; }
|
||||
|
||||
const isCourseRunActive = cardValue(cardData.isCourseRunActive);
|
||||
const canUpgrade = cardValue(cardData.canUpgrade);
|
||||
const isAuditAccessExpired = cardValue(cardData.isAuditAccessExpired);
|
||||
if (isAuditAccessExpired) {
|
||||
if (canUpgrade) {
|
||||
return (
|
||||
@@ -31,19 +32,20 @@ export const CourseBanner = ({ cardData }) => {
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
if (isActive && !canUpgrade) {
|
||||
if (isCourseRunActive && !canUpgrade) {
|
||||
const courseWebsite = cardValue(cardData.courseWebsite);
|
||||
return (
|
||||
<Banner>
|
||||
Your upgrade deadline for this course has passed. To upgrade, enroll in a session that is farther in the future.
|
||||
{' '}
|
||||
<Hyperlink href={cardData.course.website}>Explore course details.</Hyperlink>
|
||||
<Hyperlink href={courseWebsite}>Explore course details.</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
CourseBanner.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseBanner;
|
||||
|
||||
@@ -1,42 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import Banner from 'components/Banner';
|
||||
|
||||
export const EntitlementBanner = ({ cardData }) => {
|
||||
const { entitlements } = cardData;
|
||||
if (!entitlements.isEntitlement) {
|
||||
return null;
|
||||
}
|
||||
if (entitlements.isExpired) {
|
||||
return null;
|
||||
}
|
||||
if (!entitlements.isFulfilled) {
|
||||
if (entitlements.canChange) {
|
||||
return (
|
||||
<Banner>You must select a session to access the course.</Banner>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Banner>The deadline to select a session has passed</Banner>
|
||||
);
|
||||
}
|
||||
if (entitlements.canChange) {
|
||||
return (
|
||||
<Banner>
|
||||
You can change sessions until {entitlements.changeDeadline}.
|
||||
{' '}
|
||||
<Hyperlink>Change or leave session</Hyperlink>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
const { cardData } = selectors;
|
||||
|
||||
return null;
|
||||
export const EntitlementBanner = ({ courseNumber }) => {
|
||||
const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber));
|
||||
const isEntitlement = cardValue(cardData.isEntitlement);
|
||||
if (!isEntitlement) {
|
||||
return null;
|
||||
}
|
||||
const isExpired = cardValue(cardData.isEntitlementExpired);
|
||||
const isFulfilled = cardValue(cardData.isEntitlementFulfilled);
|
||||
if (isExpired || isFulfilled) {
|
||||
return null;
|
||||
}
|
||||
const canChange = cardValue(cardData.canChangeEntitlementSession);
|
||||
return canChange
|
||||
? (<Banner>You must select a session to access the course.</Banner>)
|
||||
: (<Banner>The deadline to select a session has passed</Banner>);
|
||||
};
|
||||
EntitlementBanner.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default EntitlementBanner;
|
||||
|
||||
@@ -1,43 +1,50 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { Locked } from '@edx/paragon/icons';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
const { cardData } = selectors;
|
||||
|
||||
export const CourseCardActions = ({ courseNumber }) => {
|
||||
const cardValue = (sel) => useSelector(cardData.cardSelector(sel, courseNumber));
|
||||
const canUpgrade = cardValue(cardData.canUpgrade);
|
||||
const isAudit = cardValue(cardData.isAudit);
|
||||
const isAuditAccessExpired = cardValue(cardData.isAuditAccessExpired);
|
||||
const isVerified = cardValue(cardData.isVerified);
|
||||
const isPending = cardValue(cardData.isCourseRunPending);
|
||||
const isFinished = cardValue(cardData.isCourseRunFinished);
|
||||
|
||||
export const CourseCardActions = ({ cardData: { enrollment, courseRun } }) => {
|
||||
let primary;
|
||||
let secondary = null;
|
||||
if (!enrollment.isVerified) {
|
||||
if (!isVerified) {
|
||||
secondary = (
|
||||
<Button
|
||||
iconBefore={Locked}
|
||||
variant="outline-primary"
|
||||
disabled={!enrollment.canUpgrade}
|
||||
disabled={!canUpgrade}
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (courseRun.isPending) {
|
||||
primary = (
|
||||
<Button>
|
||||
Begin Course
|
||||
</Button>
|
||||
);
|
||||
} else if (!courseRun.isEnded) {
|
||||
if (enrollment.isAudit && enrollment.isAuditAccessExpired) {
|
||||
primary = (<Button disabled>Resume</Button>);
|
||||
} else {
|
||||
primary = (<Button>Resume</Button>);
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
primary = (<Button>Begin Course</Button>);
|
||||
} else if (!isFinished) {
|
||||
primary = (isAudit && isAuditAccessExpired)
|
||||
? (<Button disabled>Resume</Button>)
|
||||
: (<Button>Resume</Button>);
|
||||
} else {
|
||||
primary = (<Button>View Course</Button>);
|
||||
}
|
||||
return (<>{secondary}{primary}</>);
|
||||
};
|
||||
CourseCardActions.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseCardActions;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Dropdown, Icon, IconButton } from '@edx/paragon';
|
||||
import { MoreVert } from '@edx/paragon/icons';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import EmailSettingsModal from 'containers/EmailSettingsModal';
|
||||
import UnenrollConfirmModal from 'containers/UnenrollConfirmModal';
|
||||
import hooks from './hooks';
|
||||
|
||||
export const CourseCardMenu = ({ cardData }) => {
|
||||
export const CourseCardMenu = ({ courseNumber }) => {
|
||||
const {
|
||||
emailSettingsModal,
|
||||
unenrollModal,
|
||||
@@ -34,17 +34,18 @@ export const CourseCardMenu = ({ cardData }) => {
|
||||
<UnenrollConfirmModal
|
||||
show={unenrollModal.isVisible}
|
||||
closeModal={unenrollModal.hide}
|
||||
courseNumber={courseNumber}
|
||||
/>
|
||||
<EmailSettingsModal
|
||||
show={emailSettingsModal.isVisible}
|
||||
closeModal={emailSettingsModal.hide}
|
||||
cardData={cardData}
|
||||
courseNumber={courseNumber}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
CourseCardMenu.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseCardMenu;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable quotes */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, useToggle } from '@edx/paragon';
|
||||
import { Program } from '@edx/paragon/icons';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import RelatedProgramsBadgeModal from 'containers/RelatedProgramsModal';
|
||||
|
||||
export const RelatedProgramsBadge = ({ cardData }) => {
|
||||
export const RelatedProgramsBadge = ({ courseNumber }) => {
|
||||
const [isOpen, open, closeModal] = useToggle(false);
|
||||
return (
|
||||
<>
|
||||
@@ -18,12 +18,12 @@ export const RelatedProgramsBadge = ({ cardData }) => {
|
||||
>
|
||||
2 Related Program
|
||||
</Button>
|
||||
<RelatedProgramsBadgeModal {...{ isOpen, closeModal, cardData }} />
|
||||
<RelatedProgramsBadgeModal {...{ isOpen, closeModal, courseNumber }} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
RelatedProgramsBadge.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RelatedProgramsBadge;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// import PropTypes from 'prop-types';
|
||||
import { Card } from '@edx/paragon';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import { getCardValue } from 'hooks';
|
||||
|
||||
import RelatedProgramsBadge from './components/RelatedProgramsBadge';
|
||||
import CourseCardMenu from './components/CourseCardMenu';
|
||||
@@ -13,52 +17,48 @@ import {
|
||||
} from './components/Banners';
|
||||
import CourseCardActions from './components/CourseCardActions';
|
||||
|
||||
export const CourseCard = ({ cardData }) => {
|
||||
const {
|
||||
course: {
|
||||
title,
|
||||
bannerUrl: imageUrl,
|
||||
},
|
||||
courseRun: {
|
||||
courseNumber,
|
||||
accessExpirationDate,
|
||||
},
|
||||
} = cardData;
|
||||
const providerName = cardData.provider?.name;
|
||||
const { cardData } = selectors;
|
||||
|
||||
export const CourseCard = ({ courseNumber }) => {
|
||||
const cardValue = getCardValue(courseNumber);
|
||||
const title = cardValue(cardData.courseTitle);
|
||||
const bannerUrl = cardValue(cardData.courseBannerUrl);
|
||||
const accessExpirationDate = cardValue(cardData.courseRunAccessExpirationDate);
|
||||
const providerName = cardValue(cardData.providerName);
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<Card orientation="horizontal">
|
||||
<Card.ImageCap
|
||||
src={imageUrl}
|
||||
src={bannerUrl}
|
||||
srcAlt="course thumbnail"
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Header
|
||||
title={title}
|
||||
actions={<CourseCardMenu cardData={cardData} />}
|
||||
actions={<CourseCardMenu courseNumber={courseNumber} />}
|
||||
/>
|
||||
<Card.Section>
|
||||
{providerName || 'Unkown'} • {courseNumber} • Access expires {accessExpirationDate}
|
||||
</Card.Section>
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
textElement={<RelatedProgramsBadge cardData={cardData} />}
|
||||
textElement={<RelatedProgramsBadge courseNumber={courseNumber} />}
|
||||
>
|
||||
<CourseCardActions cardData={cardData} />
|
||||
<CourseCardActions courseNumber={courseNumber} />
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
<div className="course-card-banners">
|
||||
<CourseBanner cardData={cardData} />
|
||||
<CertificateBanner cardData={cardData} />
|
||||
<EntitlementBanner cardData={cardData} />
|
||||
<CourseBanner courseNumber={courseNumber} />
|
||||
<CertificateBanner courseNumber={courseNumber} />
|
||||
<EntitlementBanner courseNumber={courseNumber} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CourseCard.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseCard.defaultProps = {};
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import CourseCard from 'containers/CourseCard';
|
||||
|
||||
export const CourseList = ({ courseListData }) => (
|
||||
<div className="d-flex flex-column flex-grow-1">
|
||||
{courseListData.map((cardData) => (
|
||||
<CourseCard cardData={cardData} />
|
||||
{courseListData.map((courseNumber) => (
|
||||
<CourseCard key={courseNumber} courseNumber={courseNumber} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
CourseList.propTypes = {
|
||||
courseListData: PropTypes.arrayOf(shapes.courseRunCardData).isRequired,
|
||||
courseListData: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export default CourseList;
|
||||
|
||||
@@ -2,19 +2,24 @@ import React from 'react';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
// import { thunkActions } from 'data/redux';
|
||||
import { selectors } from 'data/redux';
|
||||
import { getCardValue } from 'hooks';
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
const { cardData } = selectors;
|
||||
|
||||
export const state = StrictDict({
|
||||
toggle: (val) => React.useState(val),
|
||||
});
|
||||
|
||||
export const modalHooks = ({
|
||||
cardData,
|
||||
closeModal,
|
||||
courseNumber,
|
||||
// dispatch,
|
||||
}) => {
|
||||
const { isEmailEnabled } = cardData.enrollment;
|
||||
const cardValue = getCardValue(courseNumber);
|
||||
const isEmailEnabled = cardValue(cardData.isEmailEnabled);
|
||||
const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled);
|
||||
|
||||
const onToggle = React.useCallback(() => setToggleValue(!toggleValue), [toggleValue]);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { nullMethod } from 'hooks';
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
|
||||
import hooks from './hooks';
|
||||
import messages from './messages';
|
||||
@@ -19,14 +18,14 @@ import messages from './messages';
|
||||
export const EmailSettingsModal = ({
|
||||
closeModal,
|
||||
show,
|
||||
cardData,
|
||||
courseNumber,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
toggleValue,
|
||||
onToggle,
|
||||
save,
|
||||
} = hooks({ dispatch, closeModal, cardData });
|
||||
} = hooks({ dispatch, closeModal, courseNumber });
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
@@ -34,6 +33,7 @@ export const EmailSettingsModal = ({
|
||||
isOpen={show}
|
||||
onClose={nullMethod}
|
||||
hasCloseButton={false}
|
||||
title=""
|
||||
>
|
||||
<div className="bg-white p-3 rounded shadow" style={{ textAlign: 'start' }}>
|
||||
<h4>{formatMessage(messages.header)}</h4>
|
||||
@@ -52,7 +52,7 @@ export const EmailSettingsModal = ({
|
||||
);
|
||||
};
|
||||
EmailSettingsModal.propTypes = {
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import messages from './messages';
|
||||
export const AuthenticatedUserDropdown = ({ intl, username }) => (
|
||||
<>
|
||||
<Dropdown className="user-dropdown">
|
||||
<Dropdown.Toggle invertColors variant="primary">
|
||||
<Dropdown.Toggle id="user" variant="primary">
|
||||
<Icon src={Person} />
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
|
||||
@@ -11,8 +11,6 @@ import messages from './messages';
|
||||
|
||||
export const LearnerDashboardHeader = () => {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const context = useContext(AppContext);
|
||||
console.log({ context });
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<div className="d-flex flex-column bg-primary">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { Program } from '@edx/paragon/icons';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
import './index.scss';
|
||||
|
||||
export const whiteFontWrapper = (node) => (<span className="text-white">{node}</span>);
|
||||
@@ -20,11 +20,23 @@ export const messages = {
|
||||
defaultMessage: '{numCourses} Courses',
|
||||
description: 'Number of courses in a program, displayed at the bottom of program card',
|
||||
},
|
||||
duration: {
|
||||
id: 'learnerDashboard.programCard.duration',
|
||||
defaultMessage: '{numWeeks} Weeks',
|
||||
description: 'Number of weeks in a program, displayed at the bottom of program card',
|
||||
},
|
||||
};
|
||||
|
||||
export const ProgramCard = ({ data }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const numCoursesMessage = formatMessage(messages.courses, { numCourses: data.numberOfCourses });
|
||||
const numCoursesMessage = formatMessage(
|
||||
messages.courses,
|
||||
{ numCourses: data.numberOfCourses },
|
||||
);
|
||||
const durationMessage = formatMessage(
|
||||
messages.duration,
|
||||
{ numWeeks: data.estimatedNumberOfWeeks },
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
className="program-card d-inline-block bg-primary-500 text-white pb-3.5"
|
||||
@@ -46,14 +58,24 @@ export const ProgramCard = ({ data }) => {
|
||||
<Icon src={Program} className="d-inline-block" /> {data.programType}
|
||||
</Badge>
|
||||
<div className="program-summary mt-2">
|
||||
{numCoursesMessage} • {data.estimatedDuration}
|
||||
{numCoursesMessage} • {durationMessage}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
ProgramCard.propTypes = {
|
||||
data: shapes.programCard.isRequired,
|
||||
data: PropTypes.shape({
|
||||
estimatedNumberOfWeeks: PropTypes.number,
|
||||
numberOfCourses: PropTypes.number,
|
||||
bannerUrl: PropTypes.string,
|
||||
logoUrl: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
provider: PropTypes.string,
|
||||
programType: PropTypes.string,
|
||||
programUrl: PropTypes.string,
|
||||
programTypeUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default ProgramCard;
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
import React from 'react';
|
||||
import { selectors } from 'data/redux';
|
||||
import { getCardValue } from 'hooks';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
// import { thunkActions } from 'data/redux';
|
||||
const { cardData } = selectors;
|
||||
const { programs } = cardData;
|
||||
|
||||
import * as module from './hooks';
|
||||
|
||||
export const state = StrictDict({
|
||||
toggle: (val) => React.useState(val),
|
||||
});
|
||||
|
||||
export const modalHooks = ({
|
||||
cardData,
|
||||
closeModal,
|
||||
// dispatch,
|
||||
export const programsModalData = ({
|
||||
courseNumber,
|
||||
}) => {
|
||||
const { isEmailEnabled } = cardData.enrollment;
|
||||
const [toggleValue, setToggleValue] = module.state.toggle(isEmailEnabled);
|
||||
|
||||
const onToggle = React.useCallback(() => setToggleValue(!toggleValue), [toggleValue]);
|
||||
const save = React.useCallback(
|
||||
() => {
|
||||
console.log('save email settings');
|
||||
closeModal();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const cardValue = getCardValue(courseNumber);
|
||||
return {
|
||||
onToggle,
|
||||
save,
|
||||
toggleValue,
|
||||
courseTitle: cardValue(cardData.courseTitle),
|
||||
relatedPrograms: cardValue(cardData.relatedPrograms).map(program => ({
|
||||
estimatedNumberOfWeeks: programs.estimatedNumberOfWeeks(program),
|
||||
numberOfCourses: programs.numberOfCourses(program),
|
||||
programType: programs.programType(program),
|
||||
programTypeUrl: programs.programTypeUrl(program),
|
||||
provider: programs.provider(program),
|
||||
title: programs.title(program),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
export default modalHooks;
|
||||
export default programsModalData;
|
||||
|
||||
@@ -5,14 +5,18 @@ import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CardGrid, ModalDialog } from '@edx/paragon';
|
||||
|
||||
import shapes from 'data/services/lms/shapes';
|
||||
|
||||
import ProgramCard from './components/ProgramCard';
|
||||
import messages from './messages';
|
||||
import programsData from './hooks';
|
||||
import './index.scss';
|
||||
|
||||
export const RelatedProgramsModal = ({ isOpen, closeModal, cardData }) => {
|
||||
export const RelatedProgramsModal = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
courseNumber,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { courseTitle, relatedPrograms } = programsData({ courseNumber });
|
||||
return (
|
||||
<ModalDialog
|
||||
title={formatMessage(messages.header)}
|
||||
@@ -27,14 +31,16 @@ export const RelatedProgramsModal = ({ isOpen, closeModal, cardData }) => {
|
||||
{formatMessage(messages.header)}
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Header as="h4" className="programs-header p-0">
|
||||
{cardData.course.title}
|
||||
{courseTitle}
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body className="pl-0">
|
||||
<p>{formatMessage(messages.description)}</p>
|
||||
<CardGrid
|
||||
columnSizes={{ lg: 6, xlg: 4, xs: 12 }}
|
||||
>
|
||||
{cardData.relatedPrograms.map(programData => <ProgramCard data={programData} />)}
|
||||
{relatedPrograms.map((programData) => (
|
||||
<ProgramCard key={`${programData.programUrl}`} data={programData} />
|
||||
))}
|
||||
</CardGrid>
|
||||
</ModalDialog.Body>
|
||||
</ModalDialog>
|
||||
@@ -43,7 +49,7 @@ export const RelatedProgramsModal = ({ isOpen, closeModal, cardData }) => {
|
||||
RelatedProgramsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
closeModal: PropTypes.func.isRequired,
|
||||
cardData: shapes.courseRunCardData.isRequired,
|
||||
courseNumber: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default RelatedProgramsModal;
|
||||
|
||||
@@ -13,34 +13,34 @@ export const state = StrictDict({
|
||||
submittedReason: (val) => React.useState(val),
|
||||
});
|
||||
|
||||
export const modalHooks = ({ closeModal, dispatch }) => {
|
||||
const [isConfirmed, setIsConfirmed] = module.state.confirmed(false);
|
||||
export const modalStates = StrictDict({
|
||||
confirm: 'confirm',
|
||||
reason: 'reason',
|
||||
finished: 'finished',
|
||||
});
|
||||
|
||||
export const unenrollReasons = () => {
|
||||
const [selectedReason, setSelectedReason] = module.state.selectedReason(null);
|
||||
const [submittedReason, setSubmittedReason] = module.state.submittedReason(null);
|
||||
const [customOption, setCustomOption] = module.state.customReason('');
|
||||
const [isSkipped, setIsSkipped] = module.state.isSkipped(false);
|
||||
const [customOption, setCustomOption] = module.state.customReason('');
|
||||
|
||||
const confirm = React.useCallback(() => setIsConfirmed(true), []);
|
||||
|
||||
const close = () => {
|
||||
closeModal();
|
||||
setIsConfirmed(false);
|
||||
setSelectedReason(null);
|
||||
setSubmittedReason(null);
|
||||
setCustomOption('');
|
||||
setIsSkipped(false);
|
||||
};
|
||||
|
||||
const reason = {
|
||||
value: submittedReason,
|
||||
skip: React.useCallback(() => setIsSkipped(true), [isSkipped]),
|
||||
isSkipped,
|
||||
selectOption: React.useCallback((e) => setSelectedReason(e.target.value), []),
|
||||
return {
|
||||
clear: React.useCallback(() => {
|
||||
setSelectedReason(null);
|
||||
setSubmittedReason(null);
|
||||
setCustomOption('');
|
||||
setIsSkipped(false);
|
||||
}, []),
|
||||
customOption: {
|
||||
value: customOption,
|
||||
onChange: React.useCallback((e) => setCustomOption(e.target.value), []),
|
||||
},
|
||||
isSkipped,
|
||||
isSubmitted: submittedReason !== null || isSkipped,
|
||||
selected: selectedReason,
|
||||
selectOption: React.useCallback((e) => setSelectedReason(e.target.value), []),
|
||||
skip: React.useCallback(() => setIsSkipped(true), [isSkipped]),
|
||||
submit: React.useCallback(() => {
|
||||
if (selectedReason === 'custom') {
|
||||
setSubmittedReason(customOption);
|
||||
@@ -48,8 +48,28 @@ export const modalHooks = ({ closeModal, dispatch }) => {
|
||||
setSubmittedReason(selectedReason);
|
||||
}
|
||||
}, [customOption, selectedReason]),
|
||||
isSubmitted: submittedReason !== null || isSkipped,
|
||||
value: submittedReason,
|
||||
};
|
||||
};
|
||||
|
||||
export const modalHooks = ({ closeModal, dispatch }) => {
|
||||
const [isConfirmed, setIsConfirmed] = module.state.confirmed(false);
|
||||
|
||||
const confirm = React.useCallback(() => setIsConfirmed(true), []);
|
||||
|
||||
const reason = unenrollReasons();
|
||||
const close = () => {
|
||||
closeModal();
|
||||
setIsConfirmed(false);
|
||||
reason.clear();
|
||||
};
|
||||
|
||||
let modalState;
|
||||
if (isConfirmed) {
|
||||
modalState = reason.isSubmitted ? modalStates.finished : modalState.reason;
|
||||
} else {
|
||||
modalState = modalStates.confirm;
|
||||
}
|
||||
|
||||
const closeAndRefresh = React.useCallback(() => {
|
||||
dispatch(thunkActions.app.refreshList());
|
||||
@@ -62,6 +82,7 @@ export const modalHooks = ({ closeModal, dispatch }) => {
|
||||
reason,
|
||||
closeAndRefresh,
|
||||
close,
|
||||
modalState,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import ConfirmPane from './components/ConfirmPane';
|
||||
import ReasonPane from './components/ReasonPane';
|
||||
import FinishedPane from './components/FinishedPane';
|
||||
|
||||
import hooks from './hooks';
|
||||
import hooks, { modalStates } from './hooks';
|
||||
|
||||
export const UnenrollConfirmModal = ({
|
||||
closeModal,
|
||||
@@ -20,26 +20,28 @@ export const UnenrollConfirmModal = ({
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
isConfirmed,
|
||||
confirm,
|
||||
reason,
|
||||
closeAndRefresh,
|
||||
close,
|
||||
modalState,
|
||||
} = hooks({ dispatch, closeModal });
|
||||
return (
|
||||
<ModalDialog
|
||||
isOpen={show}
|
||||
onClose={nullMethod}
|
||||
hasCloseButton={false}
|
||||
title=""
|
||||
>
|
||||
<div className="bg-white p-3 rounded shadow" style={{ textAlign: 'start' }}>
|
||||
{!isConfirmed && <ConfirmPane handleClose={close} handleConfirm={confirm} />}
|
||||
{isConfirmed && !reason.isSubmitted && <ReasonPane reason={reason} />}
|
||||
{isConfirmed && reason.isSubmitted && (
|
||||
<FinishedPane
|
||||
handleClose={closeAndRefresh}
|
||||
gaveReason={!reason.isSkipped}
|
||||
/>
|
||||
{(modalState === modalStates.confirm) && (
|
||||
<ConfirmPane handleClose={close} handleConfirm={confirm} />
|
||||
)}
|
||||
{(modalState === modalStates.finished) && (
|
||||
<FinishedPane handleClose={closeAndRefresh} gaveReason={!reason.isSkipped} />
|
||||
)}
|
||||
{(modalState === modalStates.reason) && (
|
||||
<ReasonPane reason={reason} />
|
||||
)}
|
||||
</div>
|
||||
</ModalDialog>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
enrollments: [],
|
||||
courseData: {},
|
||||
entitlements: [],
|
||||
};
|
||||
|
||||
@@ -11,7 +12,17 @@ const app = createSlice({
|
||||
name: 'app',
|
||||
initialState,
|
||||
reducers: {
|
||||
loadEnrollments: (state, { payload }) => ({ ...state, enrollments: payload }),
|
||||
loadEnrollments: (state, { payload }) => ({
|
||||
...state,
|
||||
enrollments: payload.map(curr => curr.courseRun.courseNumber),
|
||||
courseData: payload.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr.courseRun.courseNumber]: curr,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
loadEntitlements: (state, { payload }) => ({ ...state, entitlements: payload }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,8 +12,17 @@ const mkSimpleSelector = (cb) => createSelector([module.appSelector], cb);
|
||||
export const simpleSelectors = {
|
||||
enrollments: mkSimpleSelector(app => app.enrollments),
|
||||
entitlements: mkSimpleSelector(app => app.entitlements),
|
||||
courseData: mkSimpleSelector(app => app.courseData),
|
||||
};
|
||||
|
||||
export const courseCardData = (state, courseNumber) => (
|
||||
module.simpleSelectors.courseData(state)[courseNumber]
|
||||
);
|
||||
|
||||
export const cardSelector = (sel, courseNumber) => state => sel(state, courseNumber);
|
||||
|
||||
export default StrictDict({
|
||||
...simpleSelectors,
|
||||
courseCardData,
|
||||
cardSelector,
|
||||
});
|
||||
|
||||
2
src/data/redux/cardData/index.js
Normal file
2
src/data/redux/cardData/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as selectors } from './selectors';
|
||||
67
src/data/redux/cardData/selectors.js
Normal file
67
src/data/redux/cardData/selectors.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { keyStore, StrictDict } from 'utils';
|
||||
|
||||
import app from 'data/redux/app/selectors';
|
||||
// import * as module from './selectors';
|
||||
|
||||
const mkCardSelector = (sel) => (state, courseNumber) => (
|
||||
sel(app.courseCardData(state, courseNumber))
|
||||
);
|
||||
|
||||
export const fieldSelectors = {
|
||||
courseTitle: data => data.course.title,
|
||||
courseBannerUrl: data => data.course.bannerUrl,
|
||||
courseRunAccessExpirationDate: data => data.courseRun.accessExpirationDate,
|
||||
courseWebsite: data => data.course.website,
|
||||
providerName: data => data.provider?.name,
|
||||
isVerified: data => data.enrollment.isVerified,
|
||||
isAudit: data => data.enrollment.isAudit,
|
||||
isAuditAccessExpired: data => data.enrollment.isAuditAccessExpired,
|
||||
isCourseRunPending: data => data.courseRun.isPending,
|
||||
isCourseRunStarted: data => data.courseRun.isStarted,
|
||||
isCourseRunFinished: data => data.courseRun.isFinished,
|
||||
isEmailEnabled: data => data.enrollment.isEmailEnabled,
|
||||
canUpgrade: data => data.enrollment.canUpgrade,
|
||||
isRestricted: data => data.certificates.isRestricted,
|
||||
isPassing: data => data.grades.isPassing,
|
||||
minPassingGrade: data => data.courseRun.minPassingGrade,
|
||||
isCertDownloadable: data => data.certificates.isDownloadable,
|
||||
certDownloadUrl: data => data.certificates.downloadUrls.download,
|
||||
certPreviewUrl: data => data.certificates.downloadUrls.preview,
|
||||
isCertEarnedButUnavailable: ({ certificates: { isEarned, isAvailable } }) => (
|
||||
isEarned && !isAvailable
|
||||
),
|
||||
certAvailableDate: data => data.certificates.availableDate,
|
||||
isEntitlement: data => data.entitlements.isEntitlement,
|
||||
isEntitlementFulfilled: data => data.entitlements.isFulfilled,
|
||||
isEntitlementExpired: data => data.entitlements.isExpired,
|
||||
canChangeEntitlementSession: data => data.entitlements.canChange,
|
||||
entitlementSessions: data => data.entitlements.availableSessions,
|
||||
relatedPrograms: data => data.relatedPrograms,
|
||||
};
|
||||
fieldSelectors.isCourseRunActive = data => (
|
||||
fieldSelectors.isCourseRunStarted(data) && !fieldSelectors.isCourseRunFinished(data)
|
||||
);
|
||||
|
||||
export const programs = StrictDict({
|
||||
estimatedNumberOfWeeks: data => data.estimatedNumberOfWeeks,
|
||||
numberOfCourses: data => data.numberOfCourses,
|
||||
programType: data => data.programType,
|
||||
programTypeUrl: data => data.programTypeUrl,
|
||||
provider: data => data.provider,
|
||||
title: data => data.title,
|
||||
});
|
||||
|
||||
export const fieldKeys = keyStore(fieldSelectors);
|
||||
|
||||
export const cardSelectors = Object.keys(fieldSelectors).reduce(
|
||||
(obj, key) => ({ ...obj, [key]: mkCardSelector(fieldSelectors[key]) }),
|
||||
{},
|
||||
);
|
||||
|
||||
export const cardSelector = (sel, courseNumber) => (state) => sel(state, courseNumber);
|
||||
|
||||
export default StrictDict({
|
||||
cardSelector,
|
||||
...cardSelectors,
|
||||
programs,
|
||||
});
|
||||
@@ -4,16 +4,21 @@ import { StrictDict } from 'utils';
|
||||
|
||||
import * as app from './app';
|
||||
import * as requests from './requests';
|
||||
import * as cardData from './cardData';
|
||||
|
||||
export { default as thunkActions } from './thunkActions';
|
||||
|
||||
const modules = {
|
||||
app,
|
||||
requests,
|
||||
cardData,
|
||||
};
|
||||
|
||||
const moduleProps = (propName) => Object.keys(modules).reduce(
|
||||
(obj, moduleKey) => ({ ...obj, [moduleKey]: modules[moduleKey][propName] }),
|
||||
(obj, moduleKey) => {
|
||||
const value = modules[moduleKey][propName];
|
||||
return value ? { ...obj, [moduleKey]: value } : obj;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { StrictDict } from 'utils';
|
||||
import { StrictDict, keyStore } from 'utils';
|
||||
|
||||
export const shapes = StrictDict({
|
||||
course: PropTypes.shape({
|
||||
@@ -21,10 +21,6 @@ export const shapes = StrictDict({
|
||||
isStarted: PropTypes.bool,
|
||||
minPassingGrade: PropTypes.number,
|
||||
}),
|
||||
credit: PropTypes.shape({
|
||||
isPurchased: PropTypes.bool,
|
||||
requestStatus: PropTypes.string,
|
||||
}),
|
||||
certificates: PropTypes.shape({
|
||||
availableDate: PropTypes.string,
|
||||
downloadUrls: PropTypes.shape({
|
||||
@@ -43,7 +39,12 @@ export const shapes = StrictDict({
|
||||
isVerified: PropTypes.bool,
|
||||
}),
|
||||
entitlement: PropTypes.shape({
|
||||
isEntitlement: PropTypes.bool,
|
||||
isEntitlement: PropTypes.bool.isRequired,
|
||||
availableSessions: PropTypes.shape({
|
||||
startDate: PropTypes.string,
|
||||
endDate: PropTypes.string,
|
||||
courseNumber: PropTypes.string,
|
||||
}),
|
||||
canChange: PropTypes.bool,
|
||||
isFulfilled: PropTypes.bool,
|
||||
isRefundable: PropTypes.bool,
|
||||
@@ -63,7 +64,7 @@ export const shapes = StrictDict({
|
||||
programType: PropTypes.string,
|
||||
programTypeUrl: PropTypes.string,
|
||||
numberOfCourses: PropTypes.number,
|
||||
estimatedDuration: PropTypes.string,
|
||||
estimatedNumberOfWeeks: PropTypes.number,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -71,7 +72,6 @@ shapes.courseRunCardData = PropTypes.shape({
|
||||
course: shapes.course,
|
||||
provider: shapes.provider,
|
||||
courseRun: shapes.courseRun,
|
||||
credit: shapes.credit,
|
||||
certificates: shapes.certificates,
|
||||
enrollment: shapes.enrollment,
|
||||
entitlement: shapes.entitlement,
|
||||
@@ -79,4 +79,16 @@ shapes.courseRunCardData = PropTypes.shape({
|
||||
relatedPrograms: PropTypes.arrayOf(shapes.programCard),
|
||||
});
|
||||
|
||||
export const keys = StrictDict({
|
||||
cardData: keyStore(shapes.courseRunCardData),
|
||||
course: keyStore(shapes.course),
|
||||
provider: keyStore(shapes.provider),
|
||||
courseRun: keyStore(shapes.courseRun),
|
||||
certificates: keyStore(shapes.certificates),
|
||||
enrollment: keyStore(shapes.enrollment),
|
||||
entitlement: keyStore(shapes.entitlement),
|
||||
grades: keyStore(shapes.grades),
|
||||
program: keyStore(shapes.programCard),
|
||||
});
|
||||
|
||||
export default shapes;
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
const { cardData } = selectors;
|
||||
|
||||
export const getCardValue = (courseNumber) => (sel) => useSelector(cardData.cardSelector(sel, courseNumber));
|
||||
|
||||
export const nullMethod = () => ({});
|
||||
|
||||
export default {
|
||||
|
||||
Reference in New Issue
Block a user