diff --git a/src/components/dashboard/SubscriptionExpirationModal.jsx b/src/components/dashboard/SubscriptionExpirationModal.jsx index e2ee689140..4977f1bafc 100644 --- a/src/components/dashboard/SubscriptionExpirationModal.jsx +++ b/src/components/dashboard/SubscriptionExpirationModal.jsx @@ -1,5 +1,7 @@ import React, { useContext } from 'react'; -import { MailtoLink, Modal } from '@openedx/paragon'; +import { + ActionRow, AlertModal, Button, MailtoLink, useToggle, +} from '@openedx/paragon'; import { AppContext } from '@edx/frontend-platform/react'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -23,6 +25,7 @@ const SubscriptionExpirationModal = () => { } = useContext(AppContext); const intl = useIntl(); + const [isOpen, , close] = useToggle(true); const { data: enterpriseCustomer } = useEnterpriseCustomer(); const { data: subscriptions } = useSubscriptions(); const { subscriptionPlan, subscriptionLicense } = subscriptions; @@ -33,17 +36,6 @@ const SubscriptionExpirationModal = () => { isCurrent, } = subscriptionPlan; - const renderTitle = () => { - if (isCurrent) { - return ( - {SUBSCRIPTION_EXPIRING_MODAL_TITLE} - ); - } - return ( - {SUBSCRIPTION_EXPIRED_MODAL_TITLE} - ); - }; - const renderContactText = () => { const contactText = 'contact your learning manager'; const email = getContactEmail(enterpriseCustomer); @@ -92,43 +84,10 @@ const SubscriptionExpirationModal = () => { ); }; - const renderBody = () => ( - <> -

- Your organization's access to your current subscription is expiring in - {timeUntilExpiration()} After it expires you will only have audit access to your courses. -

-

- If you are currently taking courses, plan your learning accordingly. You should also take - this time to {renderCertificateText()}. -

-

- If you think this is an error or need help, {renderContactText()}. -

- - Access expires on {i18nFormatTimestamp({ intl, timestamp: expirationDate })}. - - - ); - - const renderExpiredBody = () => ( - <> -

- Your organization's access to your subscription has expired. You will only have audit - access to the courses you were enrolled in with your subscription (courses from vouchers - will still be fully accessible). -

-

- You can also {renderCertificateText()}. -

-

- If you think this is an error or need help, {renderContactText()}. -

- - Access expired on {dayjs(expirationDate).format('MMM D, YYYY')}. - - - ); + const handleSubscriptionExpiredModalDismissal = () => { + close(); + global.localStorage.setItem(EXPIRED_SUBSCRIPTION_MODAL_LOCALSTORAGE_KEY(subscriptionLicense), 'true'); + }; const seenExpiredSubscriptionModal = !!global.localStorage.getItem( EXPIRED_SUBSCRIPTION_MODAL_LOCALSTORAGE_KEY(subscriptionLicense), @@ -139,18 +98,33 @@ const SubscriptionExpirationModal = () => { return null; } return ( - { - global.localStorage.setItem(EXPIRED_SUBSCRIPTION_MODAL_LOCALSTORAGE_KEY(subscriptionLicense), 'true'); - }} - open + + footerNode={( + + + + )} + hasCloseButton + > +

+ Your organization's access to your subscription has expired. You will only have audit + access to the courses you were enrolled in with your subscription (courses from vouchers + will still be fully accessible). +

+

+ You can also {renderCertificateText()}. +

+

+ If you think this is an error or need help, {renderContactText()}. +

+ + Access expired on {dayjs(expirationDate).format('MMM D, YYYY')}. + +
); } @@ -178,20 +152,40 @@ const SubscriptionExpirationModal = () => { return null; } + const handleSubscriptionExpiringModalDismissal = () => { + close(); + global.localStorage.setItem(expirationModalLocalStorageName, 'true'); + }; + return ( - { - global.localStorage.setItem(expirationModalLocalStorageName, 'true'); - }} - open + isOpen={isOpen} data-testid="expiration-modal" - /> + footerNode={( + + + + )} + hasCloseButton + > +

+ Your organization's access to your current subscription is expiring in + {timeUntilExpiration()} After it expires you will only have audit access to your courses. +

+

+ If you are currently taking courses, plan your learning accordingly. You should also take + this time to {renderCertificateText()}. +

+

+ If you think this is an error or need help, {renderContactText()}. +

+ + Access expires on {i18nFormatTimestamp({ intl, timestamp: expirationDate })}. + + ); }; diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/EmailSettingsModal.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/EmailSettingsModal.jsx index a86aed7926..0b23b63ef9 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/EmailSettingsModal.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/EmailSettingsModal.jsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { - Input, Modal, Alert, StatefulButton, + Form, Alert, StatefulButton, ActionRow, Button, StandardModal, } from '@openedx/paragon'; import { Error } from '@openedx/paragon/icons'; @@ -108,51 +108,47 @@ class EmailSettingsModal extends Component { const { error, hasEmailsEnabled, isSubmitting, } = this.state; - const { open, courseRunId } = this.props; + const { open } = this.props; return ( - - {error && ( - - An error occurred while saving your email settings. Please try again. - - )} -
- - {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
- - )} + isOpen={open} onClose={this.handleOnClose} - buttons={[ - , - ]} - open={open} - /> + hasCloseButton + isFullscreenOnMobile + footerNode={( + + + + + )} + > + {error && ( + + An error occurred while saving your email settings. Please try again. + + )} + + + Receive course emails such as reminders, schedule updates, and other critical announcements. + + + ); } } diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/tests/EmailSettingsModal.test.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/tests/EmailSettingsModal.test.jsx index 13822c9d47..459c56d25d 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/tests/EmailSettingsModal.test.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/email-settings/tests/EmailSettingsModal.test.jsx @@ -15,6 +15,7 @@ describe('', () => { {}} courseRunId="my+course+key" + open /> )); diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/MarkCompleteModal.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/MarkCompleteModal.jsx index e457576567..15bcd617b5 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/MarkCompleteModal.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/MarkCompleteModal.jsx @@ -1,6 +1,8 @@ import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { Modal, StatefulButton } from '@openedx/paragon'; +import { + ActionRow, Button, StandardModal, StatefulButton, +} from '@openedx/paragon'; import { camelCaseObject } from '@edx/frontend-platform'; import MarkCompleteModalContext from './MarkCompleteModalContext'; @@ -73,26 +75,30 @@ const MarkCompleteModal = ({ return ( - } - buttons={[ - , - ]} - open={isOpen && !confirmSuccessful} + isOpen={isOpen && !confirmSuccessful} onClose={handleModalOnClose} - closeText="Cancel" - /> + hasCloseButton + isFullscreenOnMobile + footerNode={( + + + + + )} + > + + ); }; diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/tests/MarkCompleteModal.test.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/tests/MarkCompleteModal.test.jsx index b01b930fbb..19df155ed5 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/tests/MarkCompleteModal.test.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/mark-complete-modal/tests/MarkCompleteModal.test.jsx @@ -85,7 +85,7 @@ describe('', () => { /> )); act(() => { - wrapper.find('.modal-footer button.btn-link').hostNodes().simulate('click'); + wrapper.find('[data-testid="mark-complete-modal-cancel-btn"]').hostNodes().simulate('click'); }); expect(mockOnClose).toBeCalledTimes(1); }); diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/move-to-in-progress-modal/MoveToInProgressModal.jsx b/src/components/dashboard/main-content/course-enrollments/course-cards/move-to-in-progress-modal/MoveToInProgressModal.jsx index e80fa79f76..f91cab4734 100644 --- a/src/components/dashboard/main-content/course-enrollments/course-cards/move-to-in-progress-modal/MoveToInProgressModal.jsx +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/move-to-in-progress-modal/MoveToInProgressModal.jsx @@ -1,6 +1,8 @@ import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { Modal, StatefulButton } from '@openedx/paragon'; +import { + ActionRow, Button, StandardModal, StatefulButton, +} from '@openedx/paragon'; import { camelCaseObject } from '@edx/frontend-platform'; import MoveToInProgressModalContext from './MoveToInProgressModalContext'; @@ -67,26 +69,30 @@ const MoveToInProgressModal = ({ return ( - } - buttons={[ - , - ]} - open={isOpen && !confirmSuccessful} + isOpen={isOpen && !confirmSuccessful} onClose={handleModalOnClose} - closeText="Cancel" - /> + hasCloseButton + isFullscreenOnMobile + footerNode={( + + + + + )} + > + + ); }; diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/styles/_EmailSettingsModal.scss b/src/components/dashboard/main-content/course-enrollments/course-cards/styles/_EmailSettingsModal.scss new file mode 100644 index 0000000000..6b6780fe43 --- /dev/null +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/styles/_EmailSettingsModal.scss @@ -0,0 +1,5 @@ +.email-checkbox { + .pgn__form-checkbox-input { + flex-shrink: 0 !important; + } +} diff --git a/src/components/dashboard/main-content/course-enrollments/course-cards/styles/index.scss b/src/components/dashboard/main-content/course-enrollments/course-cards/styles/index.scss new file mode 100644 index 0000000000..0d56f9921e --- /dev/null +++ b/src/components/dashboard/main-content/course-enrollments/course-cards/styles/index.scss @@ -0,0 +1,2 @@ +@import "./CourseCard"; +@import "./EmailSettingsModal"; 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 bcc5a64a30..8f94880b00 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 @@ -78,11 +78,8 @@ describe('', () => { }); it('handles email settings modal close/cancel', async () => { - userEvent.click(screen.getByTestId('modal-footer-btn', { name: 'Close' })); - await waitFor(() => { - const dialogElement = screen.getByTestId('modal'); - expect(dialogElement).not.toHaveClass('show'); - }); + userEvent.click(screen.getByTestId('email-setting-modal-close-btn', { name: 'Close' })); + expect(await screen.queryByRole('dialog')).not.toBeInTheDocument(); }); }); diff --git a/src/components/dashboard/main-content/course-enrollments/tests/CourseEnrollments.test.jsx b/src/components/dashboard/main-content/course-enrollments/tests/CourseEnrollments.test.jsx index e038b7dd97..88f27e3121 100644 --- a/src/components/dashboard/main-content/course-enrollments/tests/CourseEnrollments.test.jsx +++ b/src/components/dashboard/main-content/course-enrollments/tests/CourseEnrollments.test.jsx @@ -69,7 +69,13 @@ jest.mock('react-router-dom', () => ({ const inProgCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.inProgress }); const upcomingCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.upcoming }); const completedCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.completed }); -const savedForLaterCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.savedForLater }); +const savedForLaterCourseRun = createCourseEnrollmentWithStatus( + { + status: COURSE_STATUSES.savedForLater, + start: dayjs().subtract(1, 'day').toISOString(), + end: dayjs().add(1, 'day').toISOString(), + }, +); const cancelledAssignedCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.assigned, isCancelledAssignment: true, @@ -88,7 +94,7 @@ const assignmentData = { contentKey: 'test-contentKey', contentTitle: 'test-title', contentMetadata: { - endDate: '2018-08-18T05:00:00Z', + endDate: '2024-08-18T05:00:00Z', startDate: '2017-02-05T05:00:00Z', courseType: 'test-course-type', enrollByDate: '2017-02-05T05:00:00Z', @@ -107,10 +113,10 @@ jest.mock('../../../../app/data', () => ({ const mockAuthenticatedUser = authenticatedUserFactory(); const mockEnterpriseCustomer = enterpriseCustomerFactory(); -const CourseEnrollmentsWrapper = () => ( +const CourseEnrollmentsWrapper = ({ appContextProps = {} }) => ( - + @@ -347,6 +353,16 @@ describe('Course enrollments', () => { }, }); renderWithRouter(); + const { title } = savedForLaterCourseRun; + + // Open the dropdown menu for the course + userEvent.click(screen.getByLabelText(`course settings for ${title}`)); + + // Wait for the dropdown to be visible and use getByRole with name to find the correct menuitem + const moveToInProgressMenuItem = await screen.findByRole('menuitem', { name: /Move to In Progress/i }); + userEvent.click(moveToInProgressMenuItem); + + // Clicks the "Move course to In Progress" button, moving the course back to in progress status userEvent.click(screen.getByRole('button', { name: MARK_MOVE_TO_IN_PROGRESS_DEFAULT_LABEL })); // TODO This test only validates 'half way', we ideally want to update it to @@ -360,18 +376,41 @@ describe('Course enrollments', () => { }); it('generates course status update on move to saved for later action', async () => { + const appContext = { + courseCards: { + 'in-progress': { + settingsMenu: { + hasMarkComplete: true, + }, + }, + }, + }; useLocation.mockReturnValue({ state: { markedSavedForLaterSuccess: true, markedInProgressSuccess: false, }, }); - renderWithRouter(); + renderWithRouter(); + const { title } = inProgCourseRun; + + // Open the dropdown menu for the course + userEvent.click(screen.getByLabelText(`course settings for ${title}`)); + + // Wait for the dropdown to be visible and use getByRole with name to find the correct menuitem + const saveForLaterMenuItem = await screen.findByRole('menuitem', { name: /Save course for later/i }); + userEvent.click(saveForLaterMenuItem); + + // Clicks the "Save course for later" button, identified by its role userEvent.click(screen.getByRole('button', { name: MARK_SAVED_FOR_LATER_DEFAULT_LABEL })); + + // Verify the course status update request is made await waitFor(() => { expect(updateCourseCompleteStatusRequest).toHaveBeenCalledTimes(1); }); - expect(await screen.findByText('Your course was saved for later.')); + + // Ensure the success message is displayed + expect(await screen.findByText('Your course was saved for later.')).toBeInTheDocument(); }); it('renders in progress, upcoming, and requested course enrollments in the same section', async () => { diff --git a/src/components/dashboard/main-content/course-enrollments/tests/enrollment-testutils.js b/src/components/dashboard/main-content/course-enrollments/tests/enrollment-testutils.js index 6f05e4d577..a07d30f072 100644 --- a/src/components/dashboard/main-content/course-enrollments/tests/enrollment-testutils.js +++ b/src/components/dashboard/main-content/course-enrollments/tests/enrollment-testutils.js @@ -4,7 +4,15 @@ import { COURSE_STATUSES } from '../data/constants'; * Generate an enrollment with given status. * Can be used as a baseline to override and generate new courseRuns. */ -const createCourseEnrollmentWithStatus = ({ status = COURSE_STATUSES.inProgress, mode = 'verified', isCancelledAssignment = false }) => { +const createCourseEnrollmentWithStatus = ( + { + status = COURSE_STATUSES.inProgress, + mode = 'verified', + isCancelledAssignment = false, + start = '2017-02-05T05:00:00Z', + end = '2018-08-18T05:00:00Z', + }, +) => { const randomNumber = Math.random(); return ({ courseRunId: `$course-v1:edX+DemoX+Demo_Course-${randomNumber}`, @@ -13,8 +21,8 @@ const createCourseEnrollmentWithStatus = ({ status = COURSE_STATUSES.inProgress, title: `edX Demonstration Course-${randomNumber}`, notifications: [], created: '2017-02-05T05:00:00Z', - startDate: '2017-02-05T05:00:00Z', - endDate: '2018-08-18T05:00:00Z', + startDate: start, + endDate: end, hasEmailsEnabled: true, isRevoked: false, mode, diff --git a/src/components/dashboard/tests/DashboardPage.test.jsx b/src/components/dashboard/tests/DashboardPage.test.jsx index a5efaf5911..2feecb4c87 100644 --- a/src/components/dashboard/tests/DashboardPage.test.jsx +++ b/src/components/dashboard/tests/DashboardPage.test.jsx @@ -533,8 +533,8 @@ describe('', () => { expect(screen.queryByText(SUBSCRIPTION_EXPIRING_MODAL_TITLE)).toBeFalsy(); expect(screen.queryByText(SUBSCRIPTION_EXPIRED_MODAL_TITLE)).toBeTruthy(); - userEvent.click(screen.getByTestId('modal-footer-btn')); - await waitFor(() => expect(screen.queryByText(SUBSCRIPTION_EXPIRED_MODAL_TITLE)).toBeTruthy()); + userEvent.click(screen.getByTestId('subscription-expiration-button')); + await waitFor(() => expect(screen.queryByText(SUBSCRIPTION_EXPIRED_MODAL_TITLE)).toBeFalsy()); const expiredModalLocalStorageKey = !!global.localStorage.getItem( EXPIRED_SUBSCRIPTION_MODAL_LOCALSTORAGE_KEY(subscriptionLicense), ); @@ -619,7 +619,7 @@ describe('', () => { ); expect(screen.queryByText(SUBSCRIPTION_EXPIRING_MODAL_TITLE)).toBeTruthy(); expect(screen.queryByText(SUBSCRIPTION_EXPIRED_MODAL_TITLE)).toBeFalsy(); - userEvent.click(screen.getByTestId('modal-footer-btn')); + userEvent.click(screen.getByTestId('subscription-expiration-button')); const hasExpirationModal = !!global.localStorage.getItem(`${SEEN_SUBSCRIPTION_EXPIRATION_MODAL_COOKIE_PREFIX}${threshold}-${subscriptionPlanId}`); expect(hasExpirationModal).toEqual(true); }); diff --git a/src/components/integration-warning-modal/IntegrationWarningModal.jsx b/src/components/integration-warning-modal/IntegrationWarningModal.jsx index 9648765076..27c97b2bc2 100644 --- a/src/components/integration-warning-modal/IntegrationWarningModal.jsx +++ b/src/components/integration-warning-modal/IntegrationWarningModal.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import Cookies from 'universal-cookie'; -import { Modal, Button } from '@openedx/paragon'; +import { Button, AlertModal } from '@openedx/paragon'; import { getConfig } from '@edx/frontend-platform/config'; import { MODAL_BUTTON_TEXT, MODAL_TITLE } from './data/constants'; import ModalBody from './ModalBody'; @@ -27,23 +27,21 @@ const IntegrationWarningModal = ({ }; return ( - } - open={dismissed} - onClose={handleModalOnClose} + {MODAL_BUTTON_TEXT} - , - ]} - /> + + )} + > + + ); }; diff --git a/src/styles/index.scss b/src/styles/index.scss index fea8242b4a..b2986dfb2c 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -20,7 +20,7 @@ @import "../components/course/styles/ProgramSidebar"; @import "../components/course/styles/CourseSkills"; @import "../components/course/styles/CourseRecommendations"; -@import "../components/dashboard/main-content/course-enrollments/course-cards/styles/CourseCard"; +@import "../components/dashboard/main-content/course-enrollments/course-cards/styles"; @import "../components/dashboard/main-content/course-enrollments/styles/CourseSection"; @import "../components/dashboard/sidebar/styles/SidebarCard"; @import "../components/dashboard/styles/SubscriptionExpirationModal";