Skip to content

Commit 8c9280d

Browse files
authored
feat: adds learner access section (#1467)
* feat: adds learner access section
1 parent 8d86eed commit 8c9280d

30 files changed

+630
-91
lines changed

src/components/BulkEnrollmentPage/BulkEnrollmentContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {
1+
import React, {
22
createContext, useState, useReducer, useMemo,
33
} from 'react';
44
import PropTypes from 'prop-types';

src/components/BulkEnrollmentPage/stepper/AddCoursesStep.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { screen, waitFor } from '@testing-library/react';
22
import '@testing-library/jest-dom/extend-expect';
3-
import { useMemo } from 'react';
3+
import React, { useMemo } from 'react';
44
import { IntlProvider } from '@edx/frontend-platform/i18n';
55
import { Provider } from 'react-redux';
66
import configureMockStore from 'redux-mock-store';

src/components/BulkEnrollmentPage/stepper/ReviewStepCourseList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useContext, useEffect, useMemo } from 'react';
1+
import React, { useContext, useEffect, useMemo } from 'react';
22
import PropTypes from 'prop-types';
33
import { InstantSearch, Configure, connectStateResults } from 'react-instantsearch-dom';
44
import { camelCaseObject } from '@edx/frontend-platform';

src/components/ContentHighlights/ContentHighlightsContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import { createContext } from 'use-context-selector';
33
import { SearchClient } from 'algoliasearch/lite';
44

src/components/ContentHighlights/HighlightStepper/HighlightStepperSelectContentSearch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import PropTypes from 'prop-types';
33
import { useContextSelector } from 'use-context-selector';
44
import { Configure, connectStateResults, InstantSearch } from 'react-instantsearch-dom';

src/components/ContentHighlights/tests/ContentHighlights.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import { screen } from '@testing-library/react';
23
import '@testing-library/jest-dom/extend-expect';
34
import { Provider } from 'react-redux';

src/components/EnterpriseApp/EnterpriseAppContextProvider.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import {
23
render,
34
waitFor,
Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,46 @@
11
import PropTypes from 'prop-types';
22
import { Skeleton } from '@openedx/paragon';
33
import EnrollmentCard from './EnrollmentCard';
4-
import useEnterpriseCourseEnrollments from '../data/hooks/useEnterpriseCourseEnrollments';
54

6-
const CourseEnrollments = ({ userEmail, lmsUserId, enterpriseUuid }) => {
7-
const { isLoading, data } = useEnterpriseCourseEnrollments({ userEmail, lmsUserId, enterpriseUuid });
8-
const enrollments = data?.data.enrollments;
5+
const CourseEnrollments = ({ enrollments, isLoading }) => (
6+
<div className="ml-5">
7+
{isLoading && !enrollments ? (
8+
<Skeleton
9+
width={400}
10+
height={200}
11+
/>
12+
) : (
13+
<>
14+
<h3>Enrollments</h3>
15+
{enrollments?.completed?.map((enrollment) => (
16+
<EnrollmentCard enrollment={enrollment} />
17+
))}
18+
{enrollments?.inProgress?.map((enrollment) => (
19+
<EnrollmentCard enrollment={enrollment} />
20+
))}
21+
{enrollments?.upcoming?.map((enrollment) => (
22+
<EnrollmentCard enrollment={enrollment} />
23+
))}
24+
</>
25+
)}
26+
</div>
27+
);
28+
29+
const enrollmentShape = PropTypes.shape({
30+
courseKey: PropTypes.string,
31+
courseType: PropTypes.string,
32+
courseRunStatus: PropTypes.string,
33+
displayName: PropTypes.string,
34+
orgName: PropTypes.string,
35+
}).isRequired;
936

10-
return (
11-
<div className="ml-5">
12-
{isLoading && !enrollments ? (
13-
<Skeleton
14-
width={400}
15-
height={200}
16-
/>
17-
) : (
18-
<>
19-
<h3>Enrollments</h3>
20-
{enrollments?.completed?.map((enrollment) => (
21-
<EnrollmentCard enrollment={enrollment} />
22-
))}
23-
{enrollments?.inProgress?.map((enrollment) => (
24-
<EnrollmentCard enrollment={enrollment} />
25-
))}
26-
{enrollments?.upcoming?.map((enrollment) => (
27-
<EnrollmentCard enrollment={enrollment} />
28-
))}
29-
</>
30-
)}
31-
</div>
32-
);
33-
};
3437
CourseEnrollments.propTypes = {
35-
userEmail: PropTypes.string.isRequired,
36-
lmsUserId: PropTypes.string.isRequired,
37-
enterpriseUuid: PropTypes.string.isRequired,
38+
enrollments: PropTypes.shape({
39+
completed: PropTypes.arrayOf(enrollmentShape),
40+
inProgress: PropTypes.arrayOf(enrollmentShape),
41+
upcoming: PropTypes.arrayOf(enrollmentShape),
42+
}).isRequired,
43+
isLoading: PropTypes.bool.isRequired,
3844
};
3945

4046
export default CourseEnrollments;
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useParams } from 'react-router-dom';
4+
import {
5+
Hyperlink, Icon, Stack, Skeleton,
6+
} from '@openedx/paragon';
7+
import { NorthEast } from '@openedx/paragon/icons';
8+
import { useIntl } from '@edx/frontend-platform/i18n';
9+
import { subscriptionPageUrl, learnerCreditPageUrl } from '../utils';
10+
11+
type SubsidyLinkProps = {
12+
subscription: Subscription;
13+
};
14+
15+
const SubsidyLink = ({ subscription }: SubsidyLinkProps) => {
16+
const { enterpriseSlug } = useParams() as { enterpriseSlug: string };
17+
const { subscriptionPlan: { planType, title, uuid } } = subscription;
18+
const subscriptionUrl = subscriptionPageUrl({ enterpriseSlug, uuid });
19+
20+
return (
21+
<div className="pl-3">
22+
<div className="d-flex align-items-center">
23+
<Hyperlink
24+
className="font-weight-bold pb-2 text-truncate d-flex align-items-center"
25+
style={{ maxWidth: '90%' }}
26+
destination={subscriptionUrl}
27+
target="_blank"
28+
showLaunchIcon={false}
29+
>
30+
<span className="text-truncate">{title}</span>
31+
<Icon
32+
id="SampleIcon"
33+
size="xs"
34+
src={NorthEast}
35+
screenReaderText="Visit subscription page"
36+
className="ml-1 mb-1"
37+
/>
38+
</Hyperlink>
39+
</div>
40+
<p className="small pb-2">{planType}</p>
41+
</div>
42+
);
43+
};
44+
45+
SubsidyLink.propTypes = {
46+
subscription: PropTypes.shape({
47+
uuid: PropTypes.string.isRequired,
48+
subscriptionPlan: PropTypes.shape({
49+
planType: PropTypes.string.isRequired,
50+
title: PropTypes.string.isRequired,
51+
uuid: PropTypes.string.isRequired,
52+
}).isRequired,
53+
}).isRequired,
54+
};
55+
56+
type LearnerCreditLinkProps = {
57+
plan: LearnerCreditPlan;
58+
};
59+
60+
const LearnerCreditLink = ({ plan }: LearnerCreditLinkProps) => {
61+
const { displayName, uuid } = plan;
62+
const { enterpriseSlug } = useParams() as { enterpriseSlug: string };
63+
const learnerCreditUrl = learnerCreditPageUrl({ enterpriseSlug, uuid });
64+
const intl = useIntl();
65+
66+
const policyTypeText = intl.formatMessage({
67+
id: 'adminPortal.peopleManagement.learnerDetailPage.policyType',
68+
defaultMessage: plan.policyType === 'AssignedLearnerCreditAccessPolicy' ? 'Assignment' : 'Browse & Enroll',
69+
description: 'Text indicating the type of learner credit policy',
70+
});
71+
72+
return (
73+
<div className="pl-3">
74+
<div className="d-flex align-items-center">
75+
<Hyperlink
76+
className="font-weight-bold pb-2 text-truncate d-flex align-items-center"
77+
style={{ maxWidth: '90%' }}
78+
destination={learnerCreditUrl}
79+
target="_blank"
80+
showLaunchIcon={false}
81+
>
82+
<span className="text-truncate">{displayName}</span>
83+
<Icon
84+
id="SampleIcon"
85+
size="xs"
86+
src={NorthEast}
87+
screenReaderText="Visit credit plan page"
88+
className="ml-1 mb-1"
89+
/>
90+
</Hyperlink>
91+
</div>
92+
<p className="small pb-2">{policyTypeText}</p>
93+
</div>
94+
);
95+
};
96+
97+
LearnerCreditLink.propTypes = {
98+
plan: PropTypes.shape({
99+
displayName: PropTypes.string.isRequired,
100+
active: PropTypes.bool.isRequired,
101+
policyType: PropTypes.string.isRequired,
102+
uuid: PropTypes.string.isRequired,
103+
}).isRequired,
104+
};
105+
106+
type LearnerAccessProps = {
107+
subscriptions: Subscription[];
108+
creditPlansData: LearnerCreditPlan[];
109+
isLoading: boolean;
110+
};
111+
112+
const LearnerAccess = ({ subscriptions, creditPlansData, isLoading }: LearnerAccessProps) => {
113+
const intl = useIntl();
114+
const accessHeader = intl.formatMessage({
115+
id: 'adminPortal.peopleManagement.learnerDetailPage.accessHeader',
116+
defaultMessage: 'Access',
117+
description: 'Header for learner access information',
118+
});
119+
return (
120+
<div>
121+
{isLoading ? (
122+
<Skeleton
123+
width={400}
124+
height={200}
125+
/>
126+
) : (
127+
<Stack gap={4}>
128+
<div className="pt-3">
129+
<h3 className="pb-3">{accessHeader}</h3>
130+
<div className="learner-detail-section">
131+
{subscriptions.length > 0 && (
132+
<div>
133+
<h5 className="pb-3 ml-3 mb-0">SUBSCRIPTION</h5>
134+
{subscriptions.map((subscription) => (
135+
<SubsidyLink key={subscription.uuid} subscription={subscription} />
136+
))}
137+
</div>
138+
)}
139+
140+
{creditPlansData?.length > 0 && (
141+
<div>
142+
<h5 className="pb-3 ml-3 mb-0">LEARNER CREDIT</h5>
143+
{creditPlansData.map((plan) => (
144+
<LearnerCreditLink key={plan.uuid} plan={plan} />
145+
))}
146+
</div>
147+
)}
148+
</div>
149+
</div>
150+
</Stack>
151+
)}
152+
</div>
153+
);
154+
};
155+
156+
LearnerAccess.propTypes = {
157+
subscriptions: PropTypes.arrayOf(PropTypes.shape({
158+
uuid: PropTypes.string.isRequired,
159+
subscriptionPlan: PropTypes.shape({
160+
planType: PropTypes.string.isRequired,
161+
title: PropTypes.string.isRequired,
162+
}).isRequired,
163+
})).isRequired,
164+
creditPlansData: PropTypes.arrayOf(PropTypes.shape({
165+
displayName: PropTypes.string.isRequired,
166+
active: PropTypes.bool.isRequired,
167+
policyType: PropTypes.string.isRequired,
168+
uuid: PropTypes.string.isRequired,
169+
})).isRequired,
170+
isLoading: PropTypes.bool.isRequired,
171+
};
172+
173+
export default LearnerAccess;

src/components/PeopleManagement/LearnerDetailPage/LearnerDetailGroupMemberships.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import React from 'react';
12
import { useParams } from 'react-router-dom';
23
import PropTypes from 'prop-types';
34
import {

src/components/PeopleManagement/LearnerDetailPage/LearnerDetailPage.jsx

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import {
99
import { Person } from '@openedx/paragon/icons';
1010

1111
import { ROUTE_NAMES } from '../../EnterpriseApp/data/constants';
12-
import { useEnterpriseGroupUuid } from '../data/hooks';
13-
import { useEnterpriseLearnerData } from './data/hooks';
12+
import {
13+
useEnterpriseGroupUuid,
14+
useLearnerProfileView,
15+
useLearnerCreditPlans,
16+
} from '../data/hooks';
17+
import useEnterpriseLearnerData from './data/hooks';
1418
import LearnerDetailGroupMemberships from './LearnerDetailGroupMemberships';
19+
import LearnerAccess from './LearnerAccess';
1520
import CourseEnrollments from './CourseEnrollments';
1621

1722
const LearnerDetailPage = ({ enterpriseUUID }) => {
@@ -20,6 +25,17 @@ const LearnerDetailPage = ({ enterpriseUUID }) => {
2025

2126
const { isLoading, learnerData } = useEnterpriseLearnerData(enterpriseUUID, learnerId);
2227

28+
const { isLoading: isLoadingProfile, data: profileData, error: profileError } = useLearnerProfileView({
29+
enterpriseUuid: enterpriseUUID,
30+
lmsUserId: learnerId,
31+
userEmail: learnerData?.email,
32+
});
33+
34+
const { isLoading: isLoadingCreditPlans, data: creditPlansData, error: creditPlansError } = useLearnerCreditPlans({
35+
enterpriseId: enterpriseUUID,
36+
lmsUserId: learnerId,
37+
});
38+
2339
const activeLabel = () => {
2440
if (!isLoading && !learnerData?.name) {
2541
return learnerData?.email;
@@ -49,6 +65,18 @@ const LearnerDetailPage = ({ enterpriseUUID }) => {
4965
}
5066
return baseLinks;
5167
}, [intl, enterpriseSlug, groupUuid, enterpriseGroup]);
68+
69+
const isLoadingAll = isLoading || isLoadingProfile || isLoadingCreditPlans;
70+
const hasError = profileError || creditPlansError;
71+
72+
if (hasError) {
73+
return (
74+
<div className="pt-3">
75+
<p className="text-danger">Error loading learner information</p>
76+
</div>
77+
);
78+
}
79+
5280
return (
5381
<div className="pt-4 pl-4 mb-3">
5482
<Breadcrumb
@@ -57,13 +85,11 @@ const LearnerDetailPage = ({ enterpriseUUID }) => {
5785
linkAs={Link}
5886
activeLabel={`${activeLabel()}`}
5987
/>
60-
{isLoading ? (
61-
<div className="col col-5">
62-
<Skeleton
63-
width={400}
64-
height={200}
65-
/>
66-
</div>
88+
{isLoadingAll ? (
89+
<Skeleton
90+
width={400}
91+
height={200}
92+
/>
6793
) : (
6894
<div className="row">
6995
<div className="col col-5">
@@ -75,16 +101,22 @@ const LearnerDetailPage = ({ enterpriseUUID }) => {
75101
<p className="mb-1 small">Joined on {learnerData?.joinedOrg}</p>
76102
</Card.Section>
77103
</Card>
104+
<LearnerAccess
105+
subscriptions={profileData?.data?.subscriptions}
106+
creditPlansData={creditPlansData}
107+
isLoading={isLoadingProfile}
108+
/>
109+
<LearnerDetailGroupMemberships enterpriseUuid={enterpriseUUID} lmsUserId={learnerId} />
78110
</div>
79111
<div className="col col-6">
80-
<CourseEnrollments userEmail={learnerData?.email} lmsUserId={learnerId} enterpriseUuid={enterpriseUUID} />
112+
<CourseEnrollments
113+
enrollments={profileData?.data?.enrollments}
114+
isLoading={isLoadingProfile}
115+
/>
81116
</div>
82117
</div>
83118
)}
84-
85-
<LearnerDetailGroupMemberships enterpriseUuid={enterpriseUUID} lmsUserId={learnerId} />
86119
</div>
87-
88120
);
89121
};
90122

0 commit comments

Comments
 (0)