chore: testability updates

This commit is contained in:
Ben Warzeski
2022-06-08 21:34:33 -04:00
parent c471f2b3e4
commit 656f529ae5
25 changed files with 376 additions and 222 deletions

View File

@@ -15,7 +15,7 @@ Banner.defaultProps = {
};
Banner.propTypes = {
variant: PropTypes.string,
icon: PropTypes.string,
icon: PropTypes.func,
children: PropTypes.node.isRequired,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = {};

View File

@@ -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;

View File

@@ -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]);

View File

@@ -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,
};

View File

@@ -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}

View File

@@ -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">

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};
};

View File

@@ -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>

View File

@@ -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 }),
},
});

View File

@@ -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,
});

View File

@@ -0,0 +1,2 @@
/* eslint-disable import/prefer-default-export */
export { default as selectors } from './selectors';

View 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,
});

View File

@@ -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;
},
{},
);

View File

@@ -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;

View File

@@ -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 {