Skip to content

Commit

Permalink
feat: modifies logic for setting cookies on dashboard page load (#881)
Browse files Browse the repository at this point in the history
* feat: modifies logic for setting cookies on dashboard page load

* chore: tests

* chore: cleanup

* fix: typo in test name

* chore: PR fixes with corresponding tests

* chore: pr cleanup

* chore: PR fixes

* chore: PR cleanup

* chore: more PR fixes

* fix: added failsafe default value for undefined 'assignments'

* chore: redundant logic removed
  • Loading branch information
brobro10000 authored Dec 5, 2023
1 parent 499191f commit d77b948
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 61 deletions.
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
18 changes: 18 additions & 0 deletions src/components/dashboard/data/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ASSIGNMENT_TYPES } from '../../enterprise-user-subsidy/enterprise-offers/data/constants';

/**
* Takes the flattened array from redeemableLearnerCreditPolicies and returns the options of
* the array of activeAssignments, or hasActiveAssignments which returns a boolean value
* @param assignments - flatMap'ed object from redeemableLearnerCreditPolicies for learnerContentAssignments
* @returns {{hasActiveAssignments: boolean, activeAssignments: Array}}
*/
export default function getActiveAssignments(assignments = []) {
const activeAssignments = assignments.filter((assignment) => [
ASSIGNMENT_TYPES.CANCELLED, ASSIGNMENT_TYPES.ALLOCATED,
].includes(assignment.state));
const hasActiveAssignments = activeAssignments.length > 0;
return {
activeAssignments,
hasActiveAssignments,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@ import CourseEnrollmentsAlert from './CourseEnrollmentsAlert';
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,28 +50,27 @@ const CourseEnrollments = ({ children }) => {
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,
);
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 assignedCourses = getTransformedAllocatedAssignments(filteredAssignments, slug);
const { activeAssigments, hasActiveAssignments } = getActiveAssignments(assignments);
const assignedCourses = getTransformedAllocatedAssignments(activeAssigments, 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)));
if (isAssigned) {
return { ...course, isCourseAssigned: true };
Expand Down Expand Up @@ -113,8 +111,6 @@ const CourseEnrollments = ({ children }) => {
}

const hasCourseEnrollments = Object.values(courseEnrollmentsByStatus).flat().length > 0;
const hasCourseAssignments = filteredAssignments?.length > 0;

return (
<>
{showCancelledAssignmentsAlert && (
Expand All @@ -138,7 +134,7 @@ const CourseEnrollments = ({ children }) => {
This allows the parent component to customize what
gets displayed if the user does not have any course enrollments.
*/}
{(!hasCourseEnrollments && !hasCourseAssignments) && children}
{(!hasCourseEnrollments && !hasActiveAssignments) && children}
<>
{features.FEATURE_ENABLE_TOP_DOWN_ASSIGNMENT && (
<CourseSection
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 and returns a boolean value
return getActiveAssignments(learnerContentAssignmentsArray).hasActiveAssignments;
};

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

0 comments on commit d77b948

Please sign in to comment.