diff --git a/.env.development b/.env.development index 9644fd9003..210792ef1e 100644 --- a/.env.development +++ b/.env.development @@ -49,6 +49,7 @@ LEARNING_TYPE_FACET='true' LEARNER_SUPPORT_URL='https://support.edx.org/hc/en-us' LEARNER_SUPPORT_SPEND_ENROLLMENT_LIMITS_URL='http://edx.org' LEARNER_SUPPORT_ABOUT_DEACTIVATION_URL='http://edx.org' +LEARNER_SUPPORT_PACED_COURSE_MODE_URL='http://edx.org' FEATURE_ENABLE_PATHWAYS='true' FEATURE_ENABLE_COURSE_REVIEW='' FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS='true' diff --git a/.env.development-stage b/.env.development-stage index 8f5de26998..a06a81a81f 100644 --- a/.env.development-stage +++ b/.env.development-stage @@ -49,6 +49,7 @@ LEARNING_TYPE_FACET='true' LEARNER_SUPPORT_URL='https://support.edx.org/hc/en-us' LEARNER_SUPPORT_SPEND_ENROLLMENT_LIMITS_URL='http://edx.org' LEARNER_SUPPORT_ABOUT_DEACTIVATION_URL='http://edx.org' +LEARNER_SUPPORT_PACED_COURSE_MODE_URL='http://edx.org' FEATURE_ENABLE_PATHWAYS='true' FEATURE_ENABLE_COURSE_REVIEW='' FEATURE_ENROLL_WITH_ENTERPRISE_OFFERS='true' diff --git a/src/components/dashboard/main-content/course-enrollments/CourseSection.jsx b/src/components/dashboard/main-content/course-enrollments/CourseSection.jsx index 235cd2f23a..7580596b81 100644 --- a/src/components/dashboard/main-content/course-enrollments/CourseSection.jsx +++ b/src/components/dashboard/main-content/course-enrollments/CourseSection.jsx @@ -5,12 +5,12 @@ import { Bubble, Collapsible, Skeleton } from '@openedx/paragon'; import { v4 as uuidv4 } from 'uuid'; import { - InProgressCourseCard, - UpcomingCourseCard, + AssignedCourseCard, CompletedCourseCard, - SavedForLaterCourseCard, + InProgressCourseCard, RequestedCourseCard, - AssignedCourseCard, + SavedForLaterCourseCard, + UpcomingCourseCard, } from './course-cards'; import { COURSE_STATUSES } from '../../../../constants'; @@ -140,8 +140,10 @@ const CourseSection = ({ onClose={() => handleCollapsibleToggle(false)} defaultOpen > - {getFormattedOptionalSubtitle()} - {renderCourseCards()} +
+ {getFormattedOptionalSubtitle()} + {renderCourseCards()} +
); diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/AssignedCourseCard.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/AssignedCourseCard.jsx index 10ef8fd49c..52dc46fe70 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/AssignedCourseCard.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/AssignedCourseCard.jsx @@ -45,6 +45,7 @@ const AssignedCourseCard = (props) => { hasViewCertificateLink={false} canUnenroll={false} externalCourseLink={false} + isCourseAssigned {...props} /> ); diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx index ad26ffdc12..42b079e03e 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/BaseCourseCard.jsx @@ -1,32 +1,43 @@ -import { useContext, useState } from 'react'; +import { useState } from 'react'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; + import { - Badge, Col, Dropdown, Icon, IconButton, IconButtonWithTooltip, Row, Skeleton, Hyperlink, Stack, + Badge, + Col, + Dropdown, + Hyperlink, + Icon, + IconButton, + IconButtonWithTooltip, + Row, + Skeleton, + Stack, } from '@openedx/paragon'; import { - Info, - InfoOutline, - MoreVert, - Warning, + Info, InfoOutline, MoreVert, Warning, } from '@openedx/paragon/icons'; import { v4 as uuidv4 } from 'uuid'; import classNames from 'classnames'; -import { FormattedMessage, defineMessages, useIntl } from '@edx/frontend-platform/i18n'; -import { AppContext } from '@edx/frontend-platform/react'; +import { defineMessages, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; +import { getConfig } from '@edx/frontend-platform'; import dayjs from '../../../../../utils/dayjs'; import { EmailSettingsModal } from './email-settings'; import { UnenrollModal } from './unenroll'; import { COURSE_PACING, COURSE_STATUSES } from '../../../../../constants'; -import { ENROLL_BY_DATE_WARNING_THRESHOLD_DAYS, EXECUTIVE_EDUCATION_COURSE_MODES, useEnterpriseCustomer } from '../../../../app/data'; -import { isTodayWithinDateThreshold } from '../../../../../utils/common'; +import { + ENROLL_BY_DATE_WARNING_THRESHOLD_DAYS, + EXECUTIVE_EDUCATION_COURSE_MODES, + useEnterpriseCustomer, +} from '../../../../app/data'; +import { isCourseEnded, isTodayWithinDateThreshold } from '../../../../../utils/common'; const messages = defineMessages({ statusBadgeLabelInProgress: { id: 'enterprise.learner_portal.dashboard.enrollments.course.status_badge_label.in_progress', - defaultMessage: 'In Progress', + defaultMessage: 'In progress', description: 'The label for the status badge for courses that are in-progress', }, statusBadgeLabelUpcoming: { @@ -74,6 +85,16 @@ const messages = defineMessages({ defaultMessage: 'Enrollment deadline approaching', description: 'Tooltip content for enrollment deadline approaching', }, + pacingWas: { + id: 'enterprise.learner_portal.dashboard.enrollments.course.misc_text.pacing_was', + defaultMessage: 'This course was {pacing}-paced', + description: 'The label for the course miscellaneous past tense text for course mode pacing', + }, + pacingIs: { + id: 'enterprise.learner_portal.dashboard.enrollments.course.misc_text.pacing_is', + defaultMessage: 'This course is {pacing}-paced', + description: 'The label for the course miscellaneous current tense text for course mode pacing', + }, }); const BADGE_PROPS_BY_COURSE_STATUS = { @@ -114,19 +135,18 @@ const BaseCourseCard = ({ microMastersTitle, orgName, children, + courseUpgradePrice, buttons, linkToCourse, externalCourseLink, - linkToCertificate, miscText, isCourseAssigned, isCanceledAssignment, isExpiredAssignment, isLoading, - hasViewCertificateLink, }) => { const intl = useIntl(); - const { config, authenticatedUser } = useContext(AppContext); + const { LEARNER_SUPPORT_PACED_COURSE_MODE_URL } = getConfig(); const { data: enterpriseCustomer } = useEnterpriseCustomer(); const [hasEmailsEnabled, setHasEmailsEnabled] = useState(defaultHasEmailsEnabled); const [emailSettingsModal, setEmailSettingsModal] = useState({ @@ -138,8 +158,21 @@ const BaseCourseCard = ({ options: {}, }); + const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); + const CourseTitleComponent = externalCourseLink ? Hyperlink : Link; + const getCoursePaceHyperlink = (chunks) => ( + + {chunks} + + ); + const handleUnenrollButtonClick = () => { setUnenrollModal((prevState) => ({ ...prevState, @@ -170,7 +203,6 @@ const BaseCourseCard = ({ const getDropdownMenuItems = () => { const firstMenuItems = []; const lastMenuItems = []; - const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); if (hasEmailsEnabled !== null && !isExecutiveEducation2UCourse) { firstMenuItems.push({ key: 'email-settings', @@ -214,14 +246,14 @@ const BaseCourseCard = ({ }; const getCourseMiscText = () => { - const isCourseEnded = dayjs(endDate).isAfter(); - let message = ''; - if (pacing) { - message += 'This course '; - message += isCourseEnded ? 'was ' : 'is '; - message += `${pacing}-paced. `; + if (!pacing || !COURSE_PACING[pacing.toUpperCase()]) { + return null; } - return message; + const courseHasEnded = isCourseEnded(endDate); + if (courseHasEnded) { + return messages.pacingWas; + } + return messages.pacingIs; }; const resetModals = () => { @@ -275,7 +307,6 @@ const BaseCourseCard = ({ }; const renderSettingsDropdown = (menuItems) => { - const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); const execEdClass = isExecutiveEducation2UCourse ? 'text-light-100' : undefined; if (!menuItems?.length) { @@ -283,7 +314,7 @@ const BaseCourseCard = ({ } return ( -
+
{menuItems.map(menuItem => ( @@ -328,9 +360,9 @@ const BaseCourseCard = ({ return null; } return ( - +
- +
); }; @@ -339,25 +371,24 @@ const BaseCourseCard = ({ return null; } return ( -

+

{microMastersTitle} -

+
); }; - const renderOrganizationName = () => { + const renderOrgNameAndCourseType = () => { if (!orgName) { return null; } - const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); const courseTypeLabel = isExecutiveEducation2UCourse ? 'Executive Education' : 'Course'; const tooltipText = isExecutiveEducation2UCourse ? 'Executive Education courses are instructor-led, cohort-based, and follow a set schedule.' : 'Courses are on-demand, self-paced, and include asynchronous online discussion.'; return ( - -

+ +

{orgName} • {courseTypeLabel}

); @@ -442,7 +474,7 @@ const BaseCourseCard = ({ } return ( -
+
{dateFields.map((dateField, index) => { const isLastDateField = index === dateFields.length - 1; return ( @@ -456,15 +488,9 @@ const BaseCourseCard = ({ ); }; - const renderChildren = () => { - if (children) { - return ( - - - {children} - - - ); + const renderCourseUpgradePrice = () => { + if (courseUpgradePrice) { + return courseUpgradePrice; } return null; }; @@ -475,26 +501,13 @@ const BaseCourseCard = ({ } return ( - + {buttons} ); }; - const renderViewCertificateText = () => { - if (!linkToCertificate) { - return null; - } - return ( - - View your certificate on - {' '} - your profile → - - ); - }; - const renderMiscText = () => { const courseMiscText = getCourseMiscText(); if (miscText != null) { @@ -503,93 +516,109 @@ const BaseCourseCard = ({ if (!courseMiscText) { return null; } + return ( - - {courseMiscText} - +
+ {intl.formatMessage(courseMiscText, { + a: getCoursePaceHyperlink, + pacing, + })} +
); }; const renderBadge = () => { - const badgeProps = (isCourseAssigned) ? BADGE_PROPS_BY_COURSE_STATUS.assigned : BADGE_PROPS_BY_COURSE_STATUS[type]; + const badgeProps = isCourseAssigned ? BADGE_PROPS_BY_COURSE_STATUS.assigned : BADGE_PROPS_BY_COURSE_STATUS[type]; if (!badgeProps) { return null; } - return ; + return ; }; const renderAssignmentAlert = () => { const alertText = isCanceledAssignment ? 'Your learning administrator canceled this assignment' : 'Deadline to enroll in this course has passed'; - const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); return ( -
- - {alertText} -
+ + + {alertText} + ); }; const dropdownMenuItems = getDropdownMenuItems(); - const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); return (
- {isLoading ? ( - <> -
Loading...
- - - ) - : ( +
+ {isLoading ? ( <> -
-
- {renderMicroMastersTitle()} - -

- - {title} - -

- {renderBadge()} -
- {renderOrganizationName()} -
- {renderSettingsDropdown(dropdownMenuItems)} -
- {renderCourseInfoOutline()} - {renderButtons()} - {renderChildren()} - - - {renderMiscText()} - {renderAdditionalInfoOutline()} - {hasViewCertificateLink && renderViewCertificateText()} - - - {(isCanceledAssignment || isExpiredAssignment) && ( - - {renderAssignmentAlert()} - - )} - {renderEmailSettingsModal()} - {renderUnenrollModal()} +
Loading...
+ - )} + ) + : ( + +
+
+ +

+ {renderMicroMastersTitle()} + + {title} + +

+ {renderBadge()} +
+ {renderOrgNameAndCourseType()} +
+ {renderSettingsDropdown(dropdownMenuItems)} +
+ {renderCourseInfoOutline()} + {renderCourseUpgradePrice()} + {renderButtons()} + {children} + {!isCourseAssigned && ( + + + {renderMiscText()} + {renderAdditionalInfoOutline()} + + + )} + {(isCanceledAssignment || isExpiredAssignment) && ( +
+ {renderAssignmentAlert()} +
+ )} +
+ )} + {renderEmailSettingsModal()} + {renderUnenrollModal()} +
); }; @@ -601,8 +630,8 @@ BaseCourseCard.propTypes = { externalCourseLink: PropTypes.bool, courseRunId: PropTypes.string.isRequired, mode: PropTypes.string.isRequired, - hasViewCertificateLink: PropTypes.bool, buttons: PropTypes.element, + courseUpgradePrice: PropTypes.element, children: PropTypes.node, startDate: PropTypes.string, endDate: PropTypes.string, @@ -636,8 +665,8 @@ BaseCourseCard.defaultProps = { orgName: null, pacing: null, buttons: null, + courseUpgradePrice: null, linkToCertificate: null, - hasViewCertificateLink: true, dropdownMenuItems: null, isLoading: false, miscText: null, diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/CompletedCourseCard.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/CompletedCourseCard.jsx index a100695f35..8bd4464f56 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/CompletedCourseCard.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/CompletedCourseCard.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import { AppContext } from '@edx/frontend-platform/react'; import { getConfig } from '@edx/frontend-platform/config'; +import { Hyperlink } from '@openedx/paragon'; +import classNames from 'classnames'; import BaseCourseCard from './BaseCourseCard'; import ContinueLearningButton from './ContinueLearningButton'; @@ -22,7 +24,7 @@ const CompletedCourseCard = (props) => { resumeCourseRunUrl, } = props; const config = getConfig(); - + const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); const renderButtons = () => { if (isCourseEnded(endDate)) { return null; @@ -39,30 +41,39 @@ const CompletedCourseCard = (props) => { /> ); }; - const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); + const renderCertificateInfo = () => ( props.linkToCertificate ? ( -
-
+
+
verified certificate preview
-

+

View your certificate on{' '} - + your profile → - -

+ +
) : ( !isExecutiveEducation2UCourse && ( -

+

To earn a certificate,{' '} - + retake this course → - -

+ +
) ) ); diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/ContinueLearningButton.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/ContinueLearningButton.jsx index a7998cb6cf..bd4a2d73a4 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/ContinueLearningButton.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/ContinueLearningButton.jsx @@ -5,6 +5,7 @@ import { Button, Hyperlink } from '@openedx/paragon'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; import dayjs from 'dayjs'; import { EXECUTIVE_EDUCATION_COURSE_MODES, useEnterpriseCustomer } from '../../../../app/data'; + /** * A 'Continue Learning' button with parameters. * @@ -40,7 +41,7 @@ const ContinueLearningButton = ({ const isCourseStarted = () => dayjs(startDate) <= dayjs(); const isExecutiveEducation2UCourse = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); const disabled = !isCourseStarted() ? 'disabled' : undefined; - const defaultVariant = isExecutiveEducation2UCourse ? 'inverse-primary' : 'outline-primary'; + const defaultVariant = isExecutiveEducation2UCourse ? 'inverse-outline-primary' : 'outline-primary'; const renderContent = () => { // resumeCourseRunUrl indicates that learner has made progress, available only if the learner has started learning. diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/InProgressCourseCard.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/InProgressCourseCard.jsx index cb15d665df..d612f2999e 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/InProgressCourseCard.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/InProgressCourseCard.jsx @@ -3,9 +3,8 @@ import PropTypes from 'prop-types'; import { useNavigate } from 'react-router-dom'; import { AppContext } from '@edx/frontend-platform/react'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; -import { FormattedMessage, defineMessages } from '@edx/frontend-platform/i18n'; +import { defineMessages, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Stack } from '@openedx/paragon'; - import dayjs from '../../../../../utils/dayjs'; import BaseCourseCard, { getScreenReaderText } from './BaseCourseCard'; import { MarkCompleteModal } from './mark-complete-modal'; @@ -14,7 +13,7 @@ import ContinueLearningButton from './ContinueLearningButton'; import Notification from './Notification'; import UpgradeCourseButton from './UpgradeCourseButton'; -import { LICENSE_SUBSIDY_TYPE, useEnterpriseCustomer } from '../../../../app/data'; +import { EXECUTIVE_EDUCATION_COURSE_MODES, LICENSE_SUBSIDY_TYPE, useEnterpriseCustomer } from '../../../../app/data'; import { useCourseUpgradeData, useUpdateCourseEnrollmentStatus } from '../data'; import { COURSE_STATUSES } from '../../../../../constants'; @@ -24,6 +23,26 @@ const messages = defineMessages({ defaultMessage: 'Save course for later for {courseTitle}', description: 'Text for the save course for later button in the course card dropdown menu', }, + upgradeCourseOriginalPrice: { + id: 'enterprise.learner_portal.dashboard.enrollments.course.upgrade_course_original_price', + defaultMessage: 'Original price:', + description: 'Text for the course info outline upgrade original price in the course card dropdown menu', + }, + upgradeCoursePriceStrikethrough: { + id: 'enterprise.learner_portal.dashboard.enrollments.course.upgrade_course_price_strikethrough', + defaultMessage: '{courseRunPrice} USD', + description: 'Text for the course info outline price strikethrough in the course card dropdown menu', + }, + upgradeCourseFree: { + id: 'enterprise.learner_portal.dashboard.enrollments.course.upgrade_course_free', + defaultMessage: 'FREE', + description: 'Text for the course info outline upgrade "FREE" text in the course card dropdown menu', + }, + upgradeCourseCoveredByOrganization: { + id: 'enterprise.learner_portal.dashboard.enrollments.course.upgrade_course_covered_by_organization', + defaultMessage: 'Covered by your organization', + description: 'Text for the course info outline upgrade covered by organization in the course card dropdown menu', + }, }); function useLinkToCourse({ @@ -52,15 +71,18 @@ export const InProgressCourseCard = ({ ...rest }) => { const navigate = useNavigate(); + const intl = useIntl(); const { subsidyForCourse, hasUpgradeAndConfirm, + courseRunPrice, } = useCourseUpgradeData({ courseRunKey: courseRunId, mode }); - const [isMarkCompleteModalOpen, setIsMarkCompleteModalOpen] = useState(false); const { courseCards } = useContext(AppContext); const { data: enterpriseCustomer } = useEnterpriseCustomer(); const updateCourseEnrollmentStatus = useUpdateCourseEnrollmentStatus({ enterpriseCustomer }); + const isExecutiveEducation = EXECUTIVE_EDUCATION_COURSE_MODES.includes(mode); + const coursewareOrUpgradeLink = useLinkToCourse({ linkToCourse, subsidyForCourse, @@ -68,8 +90,16 @@ export const InProgressCourseCard = ({ const renderButtons = () => ( + {hasUpgradeAndConfirm && ( + + )} - {hasUpgradeAndConfirm && ( - - )} ); + const renderCourseUpgradePrice = () => { + if (!hasUpgradeAndConfirm || enterpriseCustomer.hideCourseOriginalPrice || !courseRunPrice) { + return null; + } + return ( + +
+ + {intl.formatMessage(messages.upgradeCourseOriginalPrice)}{' '} + ${intl.formatMessage(messages.upgradeCoursePriceStrikethrough, { courseRunPrice })}{' '} + + {intl.formatMessage(messages.upgradeCourseFree)} + + +
+
{intl.formatMessage(messages.upgradeCourseCoveredByOrganization)}
+
+ ); + }; const filteredNotifications = notifications.filter((notification) => { const now = dayjs(); if (dayjs(notification.date).isBetween(now, dayjs(now).add('1', 'w'))) { @@ -164,7 +206,7 @@ export const InProgressCourseCard = ({ return null; } return ( -
+
    {renderNotifications()} diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/UpcomingCourseCard.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/UpcomingCourseCard.jsx index 795c99b64a..bdd15fa50a 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/UpcomingCourseCard.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/UpcomingCourseCard.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import dayjs from 'dayjs'; -import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n'; +import { FormattedDate, FormattedMessage } from '@edx/frontend-platform/i18n'; import { Button } from '@openedx/paragon'; import BaseCourseCard from './BaseCourseCard'; diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx index de79623424..d87a6858ae 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/UpgradeCourseButton.jsx @@ -1,11 +1,63 @@ import { useState } from 'react'; import PropTypes from 'prop-types'; -import { Button } from '@openedx/paragon'; +import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon'; import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils'; - -import EnrollModal from '../../../../course/EnrollModal'; +import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import { useCouponCodes, useEnterpriseCustomer } from '../../../../app/data'; import { useCourseUpgradeData } from '../data'; +import EnrollModal from '../../../../course/EnrollModal'; + +const messages = defineMessages({ + overlayTextCoveredByOrganization: { + id: 'enterprise.learner_portal.dashbboard.enrollments.course.upgrade.overlay.text.covered_by_organization', + defaultMessage: 'Covered by your organization', + description: 'The label for the course upgrade button overlay text', + }, + upgradeForFreeButton: { + id: 'enterprise.learner_portal.dashbboard.enrollments.course.upgrade.button.text', + defaultMessage: 'Upgrade{title} for free', + description: 'The label for the course upgrade button text', + }, +}); + +const upgradeButtonScreenReaderText = (chunks) => {chunks}; + +const OverlayTriggerWrapper = ({ courseRunKey, hasCourseRunPrice, children }) => { + const intl = useIntl(); + + const { data: enterpriseCustomer } = useEnterpriseCustomer(); + const { hideCourseOriginalPrice } = enterpriseCustomer; + + /* If the hideCourseOriginalPrice price flag is false OR there is a courseRunPrice, + we want to display the button without the overlay text since the + `renderCourseInfoOutline` component will display course price within + the InProgressCourseCard component */ + if (!hideCourseOriginalPrice || hasCourseRunPrice) { + return ( +
    + {children} +
    + ); + } + return ( + + {intl.formatMessage(messages.overlayTextCoveredByOrganization)} + + )} + > + {children} + + ); +}; + +OverlayTriggerWrapper.propTypes = { + courseRunKey: PropTypes.string.isRequired, + hasCourseRunPrice: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, +}; /** * Button for upgrading a course via coupon code (possibly offer later on). @@ -17,6 +69,7 @@ const UpgradeCourseButton = ({ courseRunKey, mode, }) => { + const intl = useIntl(); const [isModalOpen, setIsModalOpen] = useState(false); const { data: enterpriseCustomer } = useEnterpriseCustomer(); @@ -33,7 +86,6 @@ const UpgradeCourseButton = ({ 'edx.ui.enterprise.learner_portal.course.upgrade_button.clicked', ); }; - const handleEnroll = () => { sendEnterpriseTrackEvent( enterpriseCustomer.uuid, @@ -43,15 +95,23 @@ const UpgradeCourseButton = ({ return ( <> - + + + a[href]:not(.btn) { - color: $light-200; - text-decoration-color: $light-200; + .dashboard-course-card { + &.exec-ed-course-card { + p > a[href]:not(.btn) { + color: $light-200; + text-decoration-color: $light-200; - &:hover { - color: $light-500; + &:hover { + color: $light-500; + } } + background-color: $dark-200; } - background-color: $dark-200; - } - .notifications { - li:last-child { - .notification { - @extend .mb-0; + .notifications { + li:last-child { + .notification { + @extend .mb-0; + } } - } - - .notification { - background: $info-100; - } - } - @include media-breakpoint-down(xs) { - .btn-xs-block { - width: 100%; + .notification { + background: $info-100; + } } - } - .course-misc-text { - small { - display: block; + @include media-breakpoint-down(xs) { + .btn-xs-block { + width: 100%; + } } } - - .assignment-alert-row { - margin-bottom: -20px; - } - - .assignment-alert { - border-radius: 0 0 10px 10px; - width: 100%; - } } diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/tests/BaseCourseCard.test.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/tests/BaseCourseCard.test.jsx index cf3872d4f5..0500bf6707 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/tests/BaseCourseCard.test.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/tests/BaseCourseCard.test.jsx @@ -14,8 +14,12 @@ import { ToastsContext } from '../../../../../Toasts'; import { useEnterpriseCustomer } from '../../../../../app/data'; import { queryClient } from '../../../../../../utils/tests'; -import { authenticatedUserFactory, enterpriseCustomerFactory } from '../../../../../app/data/services/data/__factories__'; +import { + authenticatedUserFactory, + enterpriseCustomerFactory, +} from '../../../../../app/data/services/data/__factories__'; import { COURSE_STATUSES } from '../../data'; +import { isCourseEnded } from '../../../../../../utils/common'; jest.mock('@edx/frontend-enterprise-utils', () => ({ ...jest.requireActual('@edx/frontend-enterprise-utils'), @@ -125,36 +129,80 @@ describe('', () => { expect(screen.getByText('Loading...')).toBeInTheDocument(); }); - it('renders with different startDate values', () => { - const today = dayjs().toISOString(); - const yesterday = dayjs().subtract(1, 'day').toISOString(); - const tomorrow = dayjs().add(1, 'day').toISOString(); + it.each([{ + startDate: dayjs().toISOString(), + }, { + startDate: dayjs().subtract(1, 'day').toISOString(), + }, { + startDate: dayjs().add(1, 'day').toISOString(), + }])('renders with different startDate values', ({ startDate }) => { + const formattedStartDate = dayjs(startDate).format('MMMM Do, YYYY'); + const isCourseStarted = dayjs(startDate) <= dayjs(); - [today, yesterday, tomorrow].forEach((startDate) => { - const formattedStartDate = dayjs(startDate).format('MMMM Do, YYYY'); - const isCourseStarted = dayjs(startDate) <= dayjs(); + renderWithRouter( + , + ); + if (!isCourseStarted) { + expect(screen.getByText(`Starts ${formattedStartDate}`)).toBeInTheDocument(); + } else { + expect(screen.queryByText(`Starts ${formattedStartDate}`)).not.toBeInTheDocument(); + } + }); - renderWithRouter( - , - ); - if (!isCourseStarted) { - expect(screen.getByText(`Starts ${formattedStartDate}`)).toBeInTheDocument(); - } else { - expect(screen.queryByText(`Starts ${formattedStartDate}`)).not.toBeInTheDocument(); - } - }); + it.each([{ + startDate: dayjs().subtract(10, 'days').toISOString(), + endDate: dayjs().add(10, 'days').toISOString(), + pacing: 'self', + }, { + startDate: dayjs().subtract(25, 'day').toISOString(), + endDate: dayjs().subtract(1, 'days').toISOString(), + pacing: 'self', + }, { + startDate: dayjs().subtract(10, 'days').toISOString(), + endDate: dayjs().add(10, 'days').toISOString(), + pacing: 'instructor', + }, { + startDate: dayjs().subtract(25, 'day').toISOString(), + endDate: dayjs().subtract(1, 'days').toISOString(), + pacing: 'instructor', + }])('renders with different tense values for pacing (%s)', ({ pacing, startDate, endDate }) => { + const courseHasEnded = isCourseEnded(endDate); + renderWithRouter( + , + ); + expect(screen.getByTestId('course-pacing-help-link')).toBeInTheDocument(); + expect(screen.getByText(`${pacing}-paced`)).toBeInTheDocument(); + + if (courseHasEnded) { + expect(screen.getByText('This course was', { exact: false })).toBeInTheDocument(); + } else { + expect(screen.getByText('This course is', { exact: false })).toBeInTheDocument(); + } }); it.each([ diff --git a/src/components/dashboard/main-content/course-enrollments/data/hooks.js b/src/components/dashboard/main-content/course-enrollments/data/hooks.js index adb7bbaf0d..1b5c0b84cf 100644 --- a/src/components/dashboard/main-content/course-enrollments/data/hooks.js +++ b/src/components/dashboard/main-content/course-enrollments/data/hooks.js @@ -23,10 +23,10 @@ import { ASSIGNMENT_TYPES } from '../../../../enterprise-user-subsidy/enterprise import { COUPON_CODE_SUBSIDY_TYPE, COURSE_MODES_MAP, - LEARNER_CREDIT_SUBSIDY_TYPE, - LICENSE_SUBSIDY_TYPE, getSubsidyToApplyForCourse, groupCourseEnrollmentsByStatus, + LEARNER_CREDIT_SUBSIDY_TYPE, + LICENSE_SUBSIDY_TYPE, queryEnterpriseCourseEnrollments, queryRedeemablePolicies, transformCourseEnrollment, diff --git a/src/components/pathway-progress/PathwayNode.jsx b/src/components/pathway-progress/PathwayNode.jsx index 9615d9f10c..973a44f8bc 100644 --- a/src/components/pathway-progress/PathwayNode.jsx +++ b/src/components/pathway-progress/PathwayNode.jsx @@ -41,12 +41,12 @@ const PathwayNode = ({ node }) => {
-

{node.title}

  +

{node.title}

{' '} {node.status === IN_PROGRESS && ( diff --git a/src/components/pathway-progress/tests/PathwayNode.test.jsx b/src/components/pathway-progress/tests/PathwayNode.test.jsx index e9afba075e..865401a91c 100644 --- a/src/components/pathway-progress/tests/PathwayNode.test.jsx +++ b/src/components/pathway-progress/tests/PathwayNode.test.jsx @@ -1,8 +1,6 @@ import React from 'react'; import { AppContext } from '@edx/frontend-platform/react'; -import { - screen, render, -} from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { IntlProvider } from '@edx/frontend-platform/i18n'; @@ -43,7 +41,7 @@ describe('', () => { ); expect(screen.getByText(pathwayNodeExtractedData.title)).toBeInTheDocument(); - expect(screen.getByText('In Progress')).toBeInTheDocument(); + expect(screen.getByText('In progress')).toBeInTheDocument(); expect(screen.getByText('Resume Course')).toBeInTheDocument(); const cardImageNode = getByAltText(pathwayNodeExtractedData.title); expect(cardImageNode).toHaveAttribute('src', pathwayNodeExtractedData.cardImage); diff --git a/src/index.jsx b/src/index.jsx index 1320a9d320..fdc0968b6c 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -47,6 +47,7 @@ initialize({ LEARNER_SUPPORT_URL: process.env.LEARNER_SUPPORT_URL || null, LEARNER_SUPPORT_SPEND_ENROLLMENT_LIMITS_URL: process.env.LEARNER_SUPPORT_SPEND_ENROLLMENT_LIMITS_URL || null, LEARNER_SUPPORT_ABOUT_DEACTIVATION_URL: process.env.LEARNER_SUPPORT_ABOUT_DEACTIVATION_URL || null, + LEARNER_SUPPORT_PACED_COURSE_MODE_URL: process.env.LEARNER_SUPPORT_PACED_COURSE_MODE_URL || null, GETSMARTER_STUDENT_TC_URL: process.env.GETSMARTER_STUDENT_TC_URL || null, GETSMARTER_PRIVACY_POLICY_URL: process.env.GETSMARTER_PRIVACY_POLICY_URL || null, GETSMARTER_LEARNER_DASHBOARD_URL: process.env.GETSMARTER_LEARNER_DASHBOARD_URL || null, diff --git a/src/styles/index.scss b/src/styles/index.scss index 3539502f5a..fea8242b4a 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -48,7 +48,13 @@ --pgn-color-info-500: #{$info-500}; } +// TODO: Class override should be a contribution to Paragon if not already .pgn__card-logo-cap { - object-fit: scale-down !important; + object-fit: scale-down !important; object-position: center center !important; } + +// Custom CSS utilities +.text-underline { + text-decoration: underline; +} diff --git a/src/utils/common.js b/src/utils/common.js index 068299294f..494a065cfd 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -4,7 +4,7 @@ import { getConfig } from '@edx/frontend-platform/config'; import { logError } from '@edx/frontend-platform/logging'; import dayjs from './dayjs'; -export const isCourseEnded = endDate => dayjs(endDate) < dayjs(); +export const isCourseEnded = endDate => dayjs(endDate).isBefore(dayjs()); export const createArrayFromValue = (value) => { const values = [];