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: modifies logic for setting cookies on dashboard page load #881

Merged
merged 11 commits into from
Dec 5, 2023
17 changes: 7 additions & 10 deletions src/components/dashboard/DashboardPage.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import React, {
useContext, useEffect, useMemo,
} from 'react';
import React, { useContext, useEffect, useMemo } from 'react';
import { Helmet } from 'react-helmet';
import { useHistory, useLocation } from 'react-router-dom';
import {
Container,
Tabs,
Tab,
} from '@edx/paragon';
import { Container, Tab, Tabs } from '@edx/paragon';
import { AppContext } from '@edx/frontend-platform/react';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import { ProgramListingPage } from '../program-progress';
Expand All @@ -18,17 +12,20 @@ import { useLearnerProgramsListData } from '../program-progress/data/hooks';
import { useInProgressPathwaysData } from '../pathway-progress/data/hooks';
import CoursesTabComponent from './main-content/CoursesTabComponent';
import { MyCareerTab } from '../my-career';
import EnterpriseLearnerFirstVisitRedirect from '../enterprise-redirects/EnterpriseLearnerFirstVisitRedirect';
import { UserSubsidyContext } from '../enterprise-user-subsidy';
import { IntegrationWarningModal } from '../integration-warning-modal';
import SubscriptionExpirationModal from './SubscriptionExpirationModal';
import EnterpriseLearnerFirstVisitRedirect from '../enterprise-redirects/EnterpriseLearnerFirstVisitRedirect';

const DashboardPage = () => {
const { state } = useLocation();
const history = useHistory();
const { enterpriseConfig, authenticatedUser } = useContext(AppContext);
const { username } = authenticatedUser;
const { subscriptionPlan, showExpirationNotifications } = useContext(UserSubsidyContext);
const {
subscriptionPlan,
showExpirationNotifications,
} = useContext(UserSubsidyContext);
// TODO: Create a context provider containing these 2 data fetch hooks to future proof when we need to use this data
const [learnerProgramsListData, programsFetchError] = useLearnerProgramsListData(enterpriseConfig.uuid);
const [pathwayProgressData, pathwayFetchError] = useInProgressPathwaysData(enterpriseConfig.uuid);
Expand Down
11 changes: 11 additions & 0 deletions src/components/dashboard/data/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ASSIGNMENT_TYPES } from '../../enterprise-user-subsidy/enterprise-offers/data/constants';

export default function getActiveAssignments(assignments) {
const filteredAssignments = assignments?.filter((assignment) => assignment?.state === ASSIGNMENT_TYPES.ALLOCATED
brobro10000 marked this conversation as resolved.
Show resolved Hide resolved
|| assignment?.state === ASSIGNMENT_TYPES.CANCELLED);
const isActiveAssignments = () => filteredAssignments.length > 0;
brobro10000 marked this conversation as resolved.
Show resolved Hide resolved
return {
filteredAssignments,
isActiveAssignments,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@
import CourseAssignmentAlert from './CourseAssignmentAlert';
import { CourseEnrollmentsContext } from './CourseEnrollmentsContextProvider';
import {
getTransformedAllocatedAssignments, sortedEnrollmentsByEnrollmentDate, sortAssignmentsByAssignmentStatus,
getTransformedAllocatedAssignments,
isAssignmentExpired,
sortAssignmentsByAssignmentStatus,
sortedEnrollmentsByEnrollmentDate,
} from './data/utils';
import { UserSubsidyContext } from '../../../enterprise-user-subsidy';
import { features } from '../../../../config';
import getActiveAssignments from '../../data/utils';
import { ASSIGNMENT_TYPES } from '../../../enterprise-user-subsidy/enterprise-offers/data/constants';

export const COURSE_SECTION_TITLES = {
current: 'My courses',
completed: 'Completed courses',
savedForLater: 'Saved for later',
assigned: 'Assigned Courses',
};
export const ASSIGNMENT_TYPES = {
accepted: 'accepted',
allocated: 'allocated',
cancelled: 'cancelled',
};

const CourseEnrollments = ({ children }) => {
const {
Expand All @@ -51,31 +50,30 @@
const [showExpiredAssignmentsAlert, setShowExpiredAssignmentsAlert] = useState(false);

useEffect(() => {
// TODO: Refactor to DRY up code for redeemableLearnerCreditPolicies
const data = redeemableLearnerCreditPolicies?.flatMap(item => item?.learnerContentAssignments || []);
const assignmentsData = sortAssignmentsByAssignmentStatus(data);
setAssignments(assignmentsData);

const hasCancelledAssignments = assignmentsData?.some(
assignment => assignment.state === ASSIGNMENT_TYPES.cancelled,
assignment => assignment.state === ASSIGNMENT_TYPES.CANCELLED,

Check warning on line 59 in src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx#L59

Added line #L59 was not covered by tests
);
const hasExpiredAssignments = assignmentsData?.some(assignment => isAssignmentExpired(assignment));

setShowCancelledAssignmentsAlert(hasCancelledAssignments);
setShowExpiredAssignmentsAlert(hasExpiredAssignments);
}, [redeemableLearnerCreditPolicies]);

const filteredAssignments = assignments?.filter((assignment) => assignment?.state === ASSIGNMENT_TYPES.allocated
|| assignment?.state === ASSIGNMENT_TYPES.cancelled);
const { filteredAssignments } = getActiveAssignments(assignments);
const assignedCourses = getTransformedAllocatedAssignments(filteredAssignments, slug);

const currentCourseEnrollments = useMemo(
() => {
Object.keys(courseEnrollmentsByStatus).forEach((status) => {
courseEnrollmentsByStatus[status] = courseEnrollmentsByStatus[status].map((course) => {
const isAssigned = assignments?.some(assignment => (assignment?.state === ASSIGNMENT_TYPES.accepted
const isAssigned = assignments?.some(assignment => (assignment?.state === ASSIGNMENT_TYPES.ACCEPTED
&& course.courseRunId.includes(assignment?.contentKey)));

Check warning on line 74 in src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx#L74

Added line #L74 was not covered by tests
if (isAssigned) {
return { ...course, isCourseAssigned: true };

Check warning on line 76 in src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx#L76

Added line #L76 was not covered by tests
}
return course;
});
Expand Down Expand Up @@ -118,10 +116,10 @@
return (
<>
{showCancelledAssignmentsAlert && (
<CourseAssignmentAlert variant="cancelled" onClose={() => setShowCancelledAssignmentsAlert(false)}> </CourseAssignmentAlert>

Check warning on line 119 in src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx#L119

Added line #L119 was not covered by tests
)}
{showExpiredAssignmentsAlert && (
<CourseAssignmentAlert variant="expired" onClose={() => setShowExpiredAssignmentsAlert(false)}> </CourseAssignmentAlert>

Check warning on line 122 in src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx#L122

Added line #L122 was not covered by tests
)}
{showMarkCourseCompleteSuccess && (
<CourseEnrollmentsAlert variant="success" onClose={() => setShowMarkCourseCompleteSuccess(false)}>
Expand All @@ -141,7 +139,7 @@
{(!hasCourseEnrollments && !hasCourseAssignments) && children}
<>
{features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && (
<CourseSection

Check warning on line 142 in src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx

View check run for this annotation

Codecov / codecov/patch

src/components/dashboard/main-content/course-enrollments/CourseEnrollments.jsx#L142

Added line #L142 was not covered by tests
title={COURSE_SECTION_TITLES.assigned}
courseRuns={assignedCourses}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
useState, useEffect, useCallback,
} from 'react';
import { useCallback, useEffect, useState } from 'react';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { logError } from '@edx/frontend-platform/logging';
import _camelCase from 'lodash.camelcase';
Expand All @@ -11,8 +9,8 @@ import { groupCourseEnrollmentsByStatus, transformCourseEnrollment } from './uti
import { COURSE_STATUSES } from './constants';
import CourseService from '../../../../course/data/service';
import {
createEnrollWithLicenseUrl,
createEnrollWithCouponCodeUrl,
createEnrollWithLicenseUrl,
findCouponCodeForCourse,
findHighestLevelSeatSku,
getSubsidyToApplyForCourse,
Expand Down
68 changes: 50 additions & 18 deletions src/components/dashboard/tests/DashboardPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,15 @@ import { breakpoints } from '@edx/paragon';
import Cookies from 'universal-cookie';

import userEvent from '@testing-library/user-event';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';
import { UserSubsidyContext } from '../../enterprise-user-subsidy';
import { CourseContextProvider } from '../../course/CourseContextProvider';
import {
SUBSCRIPTION_EXPIRED_MODAL_TITLE,
SUBSCRIPTION_EXPIRING_MODAL_TITLE,
} from '../SubscriptionExpirationModal';
import {
SEEN_SUBSCRIPTION_EXPIRATION_MODAL_COOKIE_PREFIX,
} from '../../../config/constants';
import { SUBSCRIPTION_EXPIRED_MODAL_TITLE, SUBSCRIPTION_EXPIRING_MODAL_TITLE } from '../SubscriptionExpirationModal';
import { SEEN_SUBSCRIPTION_EXPIRATION_MODAL_COOKIE_PREFIX } from '../../../config/constants';
import { features } from '../../../config';
import * as hooks from '../main-content/course-enrollments/data/hooks';

import {
renderWithRouter,
} from '../../../utils/tests';
import { renderWithRouter } from '../../../utils/tests';
import DashboardPage from '../DashboardPage';

import { LICENSE_ACTIVATION_MESSAGE } from '../data/constants';
Expand All @@ -40,6 +34,11 @@ const defaultCouponCodesState = {

const mockAuthenticatedUser = { username: 'myspace-tom', name: 'John Doe' };

jest.mock('@edx/frontend-enterprise-utils', () => ({
...jest.requireActual('@edx/frontend-enterprise-utils'),
sendEnterpriseTrackEvent: jest.fn(),
}));

jest.mock('../../../config', () => ({
features: {
FEATURE_ENABLE_PATHWAY_PROGRESS: jest.fn(),
Expand All @@ -52,6 +51,10 @@ jest.mock('../main-content/course-enrollments/data/utils', () => ({
sortAssignmentsByAssignmentStatus: jest.fn(),
}));

jest.mock('../../enterprise-redirects/EnterpriseLearnerFirstVisitRedirect', () => jest.fn(
() => (<div>enterprise-learner-first-visit-redirect</div>),
));

const defaultAppState = {
enterpriseConfig: {
name: 'BearsRUs',
Expand All @@ -68,6 +71,16 @@ const defaultAppState = {
const defaultUserSubsidyState = {
couponCodes: defaultCouponCodesState,
enterpriseOffers: [],
redeemableLearnerCreditPolicies: [{
learnerContentAssignments: {
state: 'allocated',
},
},
{
learnerContentAssignments: {
state: 'cancelled',
},
}],
};

const defaultCourseState = {
Expand Down Expand Up @@ -176,12 +189,13 @@ describe('<Dashboard />', () => {
});

beforeEach(() => {
jest.clearAllMocks();
sortAssignmentsByAssignmentStatus.mockReturnValue([]);
});

it('renders user first name if available', () => {
renderWithRouter(<DashboardWithContext />);
expect(screen.getByText('Welcome, John!'));
expect(screen.getByText('Welcome, John!')).toBeInTheDocument();
});

it('does not render user first name if not available', () => {
Expand All @@ -193,15 +207,15 @@ describe('<Dashboard />', () => {
},
};
renderWithRouter(<DashboardWithContext initialAppState={appState} />);
expect(screen.getByText('Welcome!'));
expect(screen.getByText('Welcome!')).toBeInTheDocument();
});

it('renders license activation alert on activation success', () => {
renderWithRouter(
<DashboardWithContext />,
{ route: '/?activationSuccess=true' },
);
expect(screen.getByText(LICENSE_ACTIVATION_MESSAGE));
expect(screen.getByText(LICENSE_ACTIVATION_MESSAGE)).toBeInTheDocument();
});

it('does not render license activation alert without activation success', () => {
Expand All @@ -218,7 +232,7 @@ describe('<Dashboard />', () => {
renderWithRouter(
<DashboardWithContext />,
);
expect(screen.getByTestId('sidebar'));
expect(screen.getByTestId('sidebar')).toBeInTheDocument();
});

it('renders subsidies summary on a small screen', () => {
Expand All @@ -233,14 +247,14 @@ describe('<Dashboard />', () => {
}}
/>,
);
expect(screen.getByTestId('subsidies-summary'));
expect(screen.getByTestId('subsidies-summary')).toBeInTheDocument();
});

it('renders "Find a course" when search is enabled for the customer', () => {
renderWithRouter(
<DashboardWithContext />,
);
expect(screen.getByText('Find a course'));
expect(screen.getByText('Find a course')).toBeInTheDocument();
});

it('renders Pathways when feature is enabled', () => {
Expand All @@ -258,15 +272,15 @@ describe('<Dashboard />', () => {
renderWithRouter(
<DashboardWithContext initialAppState={appState} />,
);
expect(screen.getByText('Pathways'));
expect(screen.getByText('Pathways')).toBeInTheDocument();
});

it('renders My Career when feature is enabled', () => {
features.FEATURE_ENABLE_MY_CAREER.mockImplementation(() => true);
renderWithRouter(
<DashboardWithContext />,
);
expect(screen.getByText('My Career'));
expect(screen.getByText('My Career')).toBeInTheDocument();
});

it('does not render "Find a course" when search is disabled for the customer', () => {
Expand Down Expand Up @@ -331,6 +345,24 @@ describe('<Dashboard />', () => {
expect(programsTab).toHaveAttribute('aria-selected', 'false');
});

it('should send track event when "my-career" tab selected', () => {
renderWithRouter(<DashboardWithContext />);

const myCareerTab = screen.getByText('My Career');
userEvent.click(myCareerTab);

expect(sendEnterpriseTrackEvent).toHaveBeenCalledTimes(1);
});

it('should render redirect component if no cookie and no courseAssignments exist', () => {
const noActiveCourseAssignmentUserSubsidyState = {
...defaultUserSubsidyState,
redeemableLearnerCreditPolicies: [],
};
renderWithRouter(<DashboardWithContext initialUserSubsidyState={noActiveCourseAssignmentUserSubsidyState} />);
expect(screen.queryByText('enterprise-learner-first-visit-redirect')).toBeTruthy();
});

describe('SubscriptionExpirationModal', () => {
it('should not render when > 60 days of access remain', () => {
renderWithRouter(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
import React, { useEffect } from 'react';
import React, { useContext, useEffect } from 'react';
import { Redirect, useParams } from 'react-router-dom';
import Cookies from 'universal-cookie';
import { UserSubsidyContext } from '../enterprise-user-subsidy';
import getActiveAssignments from '../dashboard/data/utils';

export const isFirstDashboardPageVisit = () => {
const cookies = new Cookies();

const hasUserVisitedDashboard = cookies.get('has-user-visited-learner-dashboard');
return !hasUserVisitedDashboard;
};

const EnterpriseLearnerFirstVisitRedirect = () => {
const { enterpriseSlug } = useParams();
const {
redeemableLearnerCreditPolicies,
} = useContext(UserSubsidyContext);

const cookies = new Cookies();

const isFirstVisit = () => {
const hasUserVisitedDashboard = cookies.get('has-user-visited-learner-dashboard');
return !hasUserVisitedDashboard;
// TODO: Refactor to DRY up code for redeemableLearnerCreditPolicies
const hasActiveContentAssignments = (learnerCreditPolicies) => {
const learnerContentAssignmentsArray = learnerCreditPolicies?.flatMap(
item => item?.learnerContentAssignments || [],
);
// filters out course assignments that are not considered active
return getActiveAssignments(learnerContentAssignmentsArray).isActiveAssignments();
};

useEffect(() => {
if (isFirstVisit()) {
const cookies = new Cookies();

if (isFirstDashboardPageVisit()) {
cookies.set('has-user-visited-learner-dashboard', true, { path: '/' });
}
});
}, []);

if (isFirstVisit()) {
if (!hasActiveContentAssignments(redeemableLearnerCreditPolicies) && isFirstDashboardPageVisit()) {
return <Redirect to={`/${enterpriseSlug}/search`} />;
}

Expand Down
Loading
Loading