Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dismiss alert modal and cancelled assignments #900

Closed
wants to merge 9 commits into from
Closed
29 changes: 28 additions & 1 deletion src/components/dashboard/data/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
import { ASSIGNMENT_TYPES } from '../../enterprise-user-subsidy/enterprise-offers/data/constants';
import {
LEARNER_ACKNOWLEDGED_ASSIGNMENT_CANCELLATION_ALERT,
LEARNER_ACKNOWLEDGED_ASSIGNMENT_EXPIRATION_ALERT,
} from '../main-content/course-enrollments/data/constants';

export function getIsActiveExpiredAssignment() {
// if item has never been set, user has not dismissed any expired assignments
const lastExpiredAlertDismissedTime = global.localStorage.getItem(LEARNER_ACKNOWLEDGED_ASSIGNMENT_EXPIRATION_ALERT);
if (lastExpiredAlertDismissedTime === null) {
return true;
}
const currentDate = new Date();
return (currentDate > new Date(lastExpiredAlertDismissedTime));
katrinan029 marked this conversation as resolved.
Show resolved Hide resolved
}

export function getIsActiveCancelledAssignment(assignments) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: getHasActiveCancelledAssignments as we're working with plural assignments here?

const lastCancelledAlertDismissedTime = global.localStorage.getItem(
LEARNER_ACKNOWLEDGED_ASSIGNMENT_CANCELLATION_ALERT,
);
const activeCancelledAssignments = assignments.filter((assignment) => (
assignment?.actions.some((action) => (
action.actionType === ASSIGNMENT_TYPES.CANCELLED
katrinan029 marked this conversation as resolved.
Show resolved Hide resolved
&& new Date(action.completedAt) > new Date(lastCancelledAlertDismissedTime)
))
));
return activeCancelledAssignments.length > 0;
}

/**
* Takes the flattened array from redeemableLearnerCreditPolicies and returns the options of
Expand All @@ -8,7 +35,7 @@ import { ASSIGNMENT_TYPES } from '../../enterprise-user-subsidy/enterprise-offer
*/
export default function getActiveAssignments(assignments = []) {
const activeAssignments = assignments.filter((assignment) => [
ASSIGNMENT_TYPES.CANCELLED, ASSIGNMENT_TYPES.ALLOCATED,
ASSIGNMENT_TYPES.ALLOCATED,
].includes(assignment.state));
Comment on lines 47 to 50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamstankiewicz any concerns with changing this definition of getActiveAssignments()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iloveagent57 @katrinan029 Yes, by removing ASSIGNMENT_TYPES.CANCELLED here, the CourseEnrollments components that check against "cancelled" assignment states would no longer be provided any canceled assignments to process (i.e., I don't believe the util functions (e.g., getIsActiveCancelledAssignment) would be executed? 🤔 For example, trying to run this locally, I couldn't get the canceled alert to appear unless canceled assignments were considered here.

I might recommend modifying getActiveAssignments to split out arrays for each assignment lifecycle state that's relevant, which would enable more granular control over which types of assignments you're working with in CourseEnrollments, i.e.:

// Renamed `getActiveAssignments` to `getAssignmentsByState`

export function getAssignmentsByState(assignments = []) {
  const allAssignments = [];
  const allocatedAssignments = [];
  const canceledAssignments = [];
  const acceptedAssignments = [];

  assignments.forEach((assignment) => {
    allAssignments.push(assignment);
    if (assignment.state === ASSIGNMENT_TYPES.ALLOCATED) {
      allocatedAssignments.push(assignment);
    }
    if (assignment.state === ASSIGNMENT_TYPES.CANCELLED) {
      canceledAssignments.push(assignment);
    }
    if (assignment.state === ASSIGNMENT_TYPES.ACCEPTED) {
      acceptedAssignments.push(assignment);
    }
  });

  const hasAssignments = allAssignments.length > 0;
  const hasAllocatedAssignments = allocatedAssignments.length > 0;
  const hasCanceledAssignments = canceledAssignments.length > 0;
  const hasAcceptedAssignments = acceptedAssignments.length > 0;

  return {
    assignments,
    hasAssignments,
    allocatedAssignments,
    hasAllocatedAssignments,
    canceledAssignments,
    hasCanceledAssignments,
    acceptedAssignments,
    hasAcceptedAssignments,
};

Note: This function should really only be called within getRedeemablePoliciesData at this point (as it is on master) as that logic has already been executed further up in the component tree, without needing to run again to get similar results.

const hasActiveAssignments = activeAssignments.length > 0;
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Info } from '@edx/paragon/icons';
import { getContactEmail } from '../../../../utils/common';

const CourseAssignmentAlert = ({
showAlert,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call to make this alert component act as a controlled component instead of uncontrolled.

onClose,
variant,
}) => {
Expand All @@ -20,6 +21,7 @@ const CourseAssignmentAlert = ({
return (
<Alert
variant="danger"
show={showAlert}
icon={Info}
dismissible
actions={[
Expand All @@ -40,11 +42,13 @@ const CourseAssignmentAlert = ({
CourseAssignmentAlert.propTypes = {
onClose: PropTypes.func,
variant: PropTypes.string,
showAlert: PropTypes.bool,
};

CourseAssignmentAlert.defaultProps = {
onClose: null,
variant: null,
showAlert: null,
};

export default CourseAssignmentAlert;
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import {
} from './data/utils';
import { UserSubsidyContext } from '../../../enterprise-user-subsidy';
import { features } from '../../../../config';
import getActiveAssignments from '../../data/utils';
import getActiveAssignments, { getIsActiveCancelledAssignment, getIsActiveExpiredAssignment } from '../../data/utils';
import { ASSIGNMENT_TYPES } from '../../../enterprise-user-subsidy/enterprise-offers/data/constants';
import { LEARNER_ACKNOWLEDGED_ASSIGNMENT_CANCELLATION_ALERT, LEARNER_ACKNOWLEDGED_ASSIGNMENT_EXPIRATION_ALERT } from './data';

export const COURSE_SECTION_TITLES = {
current: 'My courses',
Expand Down Expand Up @@ -46,7 +47,10 @@ const CourseEnrollments = ({ children }) => {
} = useContext(UserSubsidyContext);

const [assignments, setAssignments] = useState([]);
const [showCancelledAssignmentsAlert, setShowCancelledAssignmentsAlert] = useState(false);
const [
showCancelledAssignmentsAlert,
setShowCancelledAssignmentsAlert,
] = useState(false);
const [showExpiredAssignmentsAlert, setShowExpiredAssignmentsAlert] = useState(false);

useEffect(() => {
Expand All @@ -55,12 +59,12 @@ const CourseEnrollments = ({ children }) => {
const assignmentsData = sortAssignmentsByAssignmentStatus(data);
setAssignments(assignmentsData);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] I might propose a bit of a further refactor to this useEffect to clean it up a bit:

useEffect(() => {
    if (!redeemableLearnerCreditPolicies) {
      return;
    }

    const {
      allocatedAssignments,
      canceledAssignments,
      hasCanceledAssignments,
    } = redeemableLearnerCreditPolicies.learnerContentAssignments;

    const sortedAllocatedAssignments = sortAssignmentsByAssignmentStatus(allocatedAssignments);
    const transformedAllocatedAssignments = getTransformedAllocatedAssignments(
      sortedAllocatedAssignments,
      slug,
    );
    setAssignments(transformedAllocatedAssignments);

    const hasActiveCancelledAssignments = (
      hasCanceledAssignments && getHasActiveCancelledAssignments(canceledAssignments)
    );
    setShowCancelledAssignmentsAlert(hasActiveCancelledAssignments);

    const hasActiveExpiredAssignments = getHasActiveExpiredAssignment(allocatedAssignments);
    setShowExpiredAssignmentsAlert(hasActiveExpiredAssignments);
}, [redeemableLearnerCreditPolicies, slug]);

By doing so, we could rely on the state variable assignments instead of assignedCourses when passing them as the course runs to the assigned CourseSection component (i.e., the call to getTransformedAllocatedAssignments was moved within the useEffect).


const hasCancelledAssignments = assignmentsData?.some(
assignment => assignment.state === ASSIGNMENT_TYPES.CANCELLED,
);
const hasExpiredAssignments = assignmentsData?.some(assignment => isAssignmentExpired(assignment));
const hasActiveCancelledAssignments = assignmentsData?.some((assignment) => (
assignment.state === ASSIGNMENT_TYPES.CANCELLED)) && getIsActiveCancelledAssignment(assignmentsData);
iloveagent57 marked this conversation as resolved.
Show resolved Hide resolved
setShowCancelledAssignmentsAlert(hasActiveCancelledAssignments);

setShowCancelledAssignmentsAlert(hasCancelledAssignments);
const hasExpiredAssignments = assignmentsData?.some(assignment => isAssignmentExpired(assignment))
&& getIsActiveExpiredAssignment();
setShowExpiredAssignmentsAlert(hasExpiredAssignments);
}, [redeemableLearnerCreditPolicies]);
const { activeAssignments, hasActiveAssignments } = getActiveAssignments(assignments);
Expand Down Expand Up @@ -109,15 +113,24 @@ const CourseEnrollments = ({ children }) => {
</CourseEnrollmentsAlert>
);
}
const handleOnCloseCancelAlert = () => {
setShowCancelledAssignmentsAlert(false);
global.localStorage.setItem(LEARNER_ACKNOWLEDGED_ASSIGNMENT_CANCELLATION_ALERT, new Date());
};

const handleOnCloseExpiredAlert = () => {
setShowCancelledAssignmentsAlert(false);
global.localStorage.setItem(LEARNER_ACKNOWLEDGED_ASSIGNMENT_EXPIRATION_ALERT, new Date());
};

const hasCourseEnrollments = Object.values(courseEnrollmentsByStatus).flat().length > 0;
return (
<>
{features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && showCancelledAssignmentsAlert && (
<CourseAssignmentAlert variant="cancelled" onClose={() => setShowCancelledAssignmentsAlert(false)}> </CourseAssignmentAlert>
{features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && (
<CourseAssignmentAlert showAlert={showCancelledAssignmentsAlert} variant="cancelled" onClose={handleOnCloseCancelAlert}> </CourseAssignmentAlert>
)}
{features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && showExpiredAssignmentsAlert && (
<CourseAssignmentAlert variant="expired" onClose={() => setShowExpiredAssignmentsAlert(false)}> </CourseAssignmentAlert>
{features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && (
<CourseAssignmentAlert showAlert={showExpiredAssignmentsAlert} variant="expired" onClose={handleOnCloseExpiredAlert}> </CourseAssignmentAlert>
)}
{showMarkCourseCompleteSuccess && (
<CourseEnrollmentsAlert variant="success" onClose={() => setShowMarkCourseCompleteSuccess(false)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class CourseSection extends React.Component {
const { isOpen } = this.state;
const { courseRuns, title } = this.props;
const sectionTitle = isOpen ? title : `${title} `;
const coursesCount = this.getCoursesCount(isOpen, title, courseRuns.length);
const activeAssignedCourses = courseRuns.filter(course => !course.isCancelledAssignment);
const coursesCount = this.getCoursesCount(isOpen, title, activeAssignedCourses.length);
return (
<h3>
{sectionTitle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ export const COURSE_STATUSES = {
};

export const GETSMARTER_BASE_URL = 'https://www.getsmarter.com';

export const LEARNER_ACKNOWLEDGED_ASSIGNMENT_CANCELLATION_ALERT = 'learnerAcknowledgedCancellationAt';
export const LEARNER_ACKNOWLEDGED_ASSIGNMENT_EXPIRATION_ALERT = 'learnerAcknowledgedExpirationA';
katrinan029 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import '@testing-library/jest-dom/extend-expect';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { renderWithRouter } from '@edx/frontend-enterprise-utils';

import dayjs from 'dayjs';
import userEvent from '@testing-library/user-event';
import {
createCourseEnrollmentWithStatus,
} from './enrollment-testutils';
import CourseEnrollments, { COURSE_SECTION_TITLES } from '../CourseEnrollments';
import CourseEnrollments, {
COURSE_SECTION_TITLES,
} from '../CourseEnrollments';
import { MARK_MOVE_TO_IN_PROGRESS_DEFAULT_LABEL } from '../course-cards/move-to-in-progress-modal/MoveToInProgressModal';
import { MARK_SAVED_FOR_LATER_DEFAULT_LABEL } from '../course-cards/mark-complete-modal/MarkCompleteModal';
import { updateCourseCompleteStatusRequest } from '../course-cards/mark-complete-modal/data/service';
Expand All @@ -24,15 +27,27 @@ import * as hooks from '../data/hooks';
import { SubsidyRequestsContext } from '../../../../enterprise-subsidy-requests';
import { UserSubsidyContext } from '../../../../enterprise-user-subsidy';
import { sortAssignmentsByAssignmentStatus } from '../data/utils';
import getActiveAssignments, { getIsActiveCancelledAssignment } from '../../../data/utils';

jest.mock('@edx/frontend-platform/auth');
jest.mock('@edx/frontend-enterprise-utils');
getAuthenticatedUser.mockReturnValue({ username: 'test-username' });

jest.mock('../course-cards/mark-complete-modal/data/service');

jest.mock('../../../data/utils', () => ({
__esModule: true,
default: jest.fn(),
getIsActiveCancelledAssignment: jest.fn(),
}));

jest.mock('../data/service');
jest.mock('../data/hooks');
jest.mock('../../../../../config', () => ({
features: {
FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT: true,
},
}));

const enterpriseConfig = {
uuid: 'test-enterprise-uuid',
Expand All @@ -42,6 +57,10 @@ const inProgCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUS
const upcomingCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.upcoming });
const completedCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.completed });
const savedForLaterCourseRun = createCourseEnrollmentWithStatus({ status: COURSE_STATUSES.savedForLater });
const cancelledAssignedCourseRun = createCourseEnrollmentWithStatus({
status: COURSE_STATUSES.assigned,
isCancelledAssignment: true,
});

const transformedLicenseRequest = {
created: '2017-02-05T05:00:00Z',
Expand All @@ -52,17 +71,41 @@ const transformedLicenseRequest = {
notifications: [],
};

const assignmentData = {
contentKey: 'test-contentKey',
contentTitle: 'test-title',
contentMetadata: {
endDate: '2018-08-18T05:00:00Z',
startDate: '2017-02-05T05:00:00Z',
courseType: 'test-course-type',
enrollByDate: '2017-02-05T05:00:00Z',
partners: [{ name: 'test-partner' }],
},
state: 'cancelled',
// actions: [{
// actionType: 'cancelled',
// completedAt: '2023-12-14T18:10:05.128809Z',
// }],
};

hooks.useCourseEnrollments.mockReturnValue({
courseEnrollmentsByStatus: {
inProgress: [inProgCourseRun],
upcoming: [upcomingCourseRun],
completed: [completedCourseRun],
assigned: [cancelledAssignedCourseRun],
savedForLater: [savedForLaterCourseRun],
requested: [transformedLicenseRequest],
},
updateCourseEnrollmentStatus: jest.fn(),
});
const initialUserSubsidyState = {};
const initialUserSubsidyState = {
redeemableLearnerCreditPolicies: [
{
learnerContentAssignments: [assignmentData],
},
],
};
const renderEnrollmentsComponent = () => render(
<IntlProvider locale="en">
<AppContext.Provider value={{ enterpriseConfig }}>
Expand All @@ -85,7 +128,11 @@ jest.mock('../data/utils', () => ({
describe('Course enrollments', () => {
beforeEach(() => {
updateCourseCompleteStatusRequest.mockImplementation(() => ({ data: {} }));
sortAssignmentsByAssignmentStatus.mockReturnValue([]);
sortAssignmentsByAssignmentStatus.mockReturnValue([assignmentData]);
getActiveAssignments.mockReturnValue({
activeAssignments: [],
hasActiveAssignments: true,
});
});

afterEach(() => {
Expand All @@ -100,6 +147,29 @@ describe('Course enrollments', () => {
expect(screen.getAllByText(inProgCourseRun.title).length).toBeGreaterThanOrEqual(1);
});

it('does not render cancelled assignment and renders cancelled alert', async () => {
getIsActiveCancelledAssignment.mockReturnValue(true);
renderWithRouter(renderEnrollmentsComponent());
expect(screen.queryByText('Your learning administrator canceled this assignment.')).toBeFalsy();
expect(screen.getByText('Course assignment cancelled')).toBeInTheDocument();
expect(screen.queryByText('test-title')).toBeFalsy();
const dismissButton = screen.getByText('Dismiss');
userEvent.click(dismissButton);
await waitFor(() => expect(screen.queryByText('Course assignment cancelled')).toBeFalsy());
});

it('if there are active cancelled assignments, cancelled alert is rendered', () => {
getIsActiveCancelledAssignment.mockReturnValue(true);
renderEnrollmentsComponent();
expect(screen.queryByText('Course assignment cancelled')).toBeTruthy();
});

it('if there are no active cancelled assignments, cancelled alert is hidden', () => {
getIsActiveCancelledAssignment.mockReturnValue(false);
renderEnrollmentsComponent();
expect(screen.queryByText('Course assignment cancelled')).toBeFalsy();
});

it('generates course status update on move to in progress action', async () => {
const { getByText } = renderEnrollmentsComponent();
await act(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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' }) => {
const createCourseEnrollmentWithStatus = ({ status = COURSE_STATUSES.inProgress, mode = 'verified', isCancelledAssignment = false }) => {
const randomNumber = Math.random();
return ({
courseRunId: `$course-v1:edX+DemoX+Demo_Course-${randomNumber}`,
Expand All @@ -18,6 +18,7 @@ const createCourseEnrollmentWithStatus = ({ status = COURSE_STATUSES.inProgress,
hasEmailsEnabled: true,
isRevoked: false,
mode,
isCancelledAssignment,
});
};

Expand Down