+
) : (
!isExecutiveEducation2UCourse && (
-
+
)
)
);
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 = [];