richie-education 2.25.0-b2.dev46 → 2.25.0-b2.dev49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/components/TeacherDashboardCourseList/index.tsx +21 -22
- package/js/types/Joanie.ts +18 -3
- package/js/types/index.ts +1 -0
- package/js/utils/test/factories/joanie.ts +1 -0
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +45 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +16 -5
- package/js/widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment/index.ts +6 -9
- package/package.json +1 -1
|
@@ -53,6 +53,16 @@ const TeacherDashboardCourseList = ({
|
|
|
53
53
|
{titleTranslated && (
|
|
54
54
|
<h2 className="dashboard-course-list__title dashboard__page_title">{titleTranslated}</h2>
|
|
55
55
|
)}
|
|
56
|
+
{courseAndProductList.length > 0 ? (
|
|
57
|
+
<CourseGlimpseList
|
|
58
|
+
courses={getCourseGlimpseListProps(courseAndProductList, intl, organizationId)}
|
|
59
|
+
context={context}
|
|
60
|
+
className="dashboard__course-glimpse-list"
|
|
61
|
+
/>
|
|
62
|
+
) : (
|
|
63
|
+
<FormattedMessage {...messages.emptyList} />
|
|
64
|
+
)}
|
|
65
|
+
|
|
56
66
|
{isLoading && (
|
|
57
67
|
<Spinner aria-labelledby="loading-courses-data">
|
|
58
68
|
<span id="loading-courses-data">
|
|
@@ -60,28 +70,17 @@ const TeacherDashboardCourseList = ({
|
|
|
60
70
|
</span>
|
|
61
71
|
</Spinner>
|
|
62
72
|
)}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
disabled={isLoading}
|
|
75
|
-
ref={loadMoreButtonRef}
|
|
76
|
-
color="tertiary"
|
|
77
|
-
>
|
|
78
|
-
<FormattedMessage {...messages.loadMore} />
|
|
79
|
-
</Button>
|
|
80
|
-
)}
|
|
81
|
-
</>
|
|
82
|
-
) : (
|
|
83
|
-
<FormattedMessage {...messages.emptyList} />
|
|
84
|
-
))}
|
|
73
|
+
|
|
74
|
+
{hasMore && (
|
|
75
|
+
<Button
|
|
76
|
+
onClick={() => next()}
|
|
77
|
+
disabled={isLoading}
|
|
78
|
+
ref={loadMoreButtonRef}
|
|
79
|
+
color="tertiary"
|
|
80
|
+
>
|
|
81
|
+
<FormattedMessage {...messages.loadMore} />
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
85
84
|
</div>
|
|
86
85
|
);
|
|
87
86
|
};
|
package/js/types/Joanie.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CourseState } from 'types';
|
|
1
|
+
import type { CourseState, OpenEdXEnrollment } from 'types';
|
|
2
2
|
import type { Maybe, Nullable } from 'types/utils';
|
|
3
3
|
import { Resource, ResourcesQuery } from 'hooks/useResources';
|
|
4
4
|
import { OrderResourcesQuery } from 'hooks/useOrders';
|
|
@@ -190,8 +190,23 @@ export interface Enrollment {
|
|
|
190
190
|
created_on: string;
|
|
191
191
|
orders: OrderEnrollment[];
|
|
192
192
|
product_relations: CourseProductRelation[];
|
|
193
|
-
certificate_id
|
|
194
|
-
}
|
|
193
|
+
certificate_id: Nullable<string>;
|
|
194
|
+
}
|
|
195
|
+
export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj is Enrollment => {
|
|
196
|
+
if (!obj || typeof obj !== 'object') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return (
|
|
200
|
+
'is_active' in obj &&
|
|
201
|
+
'state' in obj &&
|
|
202
|
+
'course_run' in obj &&
|
|
203
|
+
'was_created_by_order' in obj &&
|
|
204
|
+
'created_on' in obj &&
|
|
205
|
+
'orders' in obj &&
|
|
206
|
+
'product_relations' in obj &&
|
|
207
|
+
'certificate_id' in obj
|
|
208
|
+
);
|
|
209
|
+
};
|
|
195
210
|
|
|
196
211
|
export interface EnrollmentLight {
|
|
197
212
|
id: string;
|
package/js/types/index.ts
CHANGED
|
@@ -69,4 +69,5 @@ export interface OpenEdXEnrollment {
|
|
|
69
69
|
* Use an unknown type to make sure we do not depend on any LMS-specific fields
|
|
70
70
|
* on enrollment objects, just use HTTP response codes.
|
|
71
71
|
*/
|
|
72
|
+
// TODO(rlecellier): rename into UnknownEnrollment
|
|
72
73
|
export type Enrollment = unknown;
|
package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx
CHANGED
|
@@ -3,7 +3,9 @@ import fetchMock from 'fetch-mock';
|
|
|
3
3
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
5
|
|
|
6
|
+
import { faker } from '@faker-js/faker';
|
|
6
7
|
import { Deferred } from 'utils/test/deferred';
|
|
8
|
+
import { EnrollmentFactory as JoanieEnrollment } from 'utils/test/factories/joanie';
|
|
7
9
|
import {
|
|
8
10
|
CourseRunFactory,
|
|
9
11
|
RichieContextFactory as mockRichieContextFactory,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
import { SessionProvider } from 'contexts/SessionContext';
|
|
13
15
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
14
16
|
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
17
|
+
import { Priority } from 'types';
|
|
15
18
|
import CourseRunEnrollment from './index';
|
|
16
19
|
|
|
17
20
|
jest.mock('utils/errors/handle');
|
|
@@ -96,10 +99,11 @@ describe('<CourseRunEnrollment /> with joanie backend ', () => {
|
|
|
96
99
|
it('shows an error message when enrollment get request failed', async () => {
|
|
97
100
|
const user = UserFactory().one();
|
|
98
101
|
const courseRun = CourseRunFactory().one();
|
|
99
|
-
|
|
102
|
+
const joanieEnrollmentId = faker.string.uuid();
|
|
103
|
+
courseRun.resource_link = `https://joanie.endpoint/api/v1.0/course-runs/${joanieEnrollmentId}`;
|
|
100
104
|
|
|
101
105
|
fetchMock.get(
|
|
102
|
-
`${endpoint}/api/v1.0/enrollments/?
|
|
106
|
+
`${endpoint}/api/v1.0/enrollments/?course_run_id=${joanieEnrollmentId}`,
|
|
103
107
|
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
104
108
|
);
|
|
105
109
|
|
|
@@ -118,6 +122,45 @@ describe('<CourseRunEnrollment /> with joanie backend ', () => {
|
|
|
118
122
|
await screen.findByText('Enrollment fetching failed');
|
|
119
123
|
});
|
|
120
124
|
|
|
125
|
+
it('shows a link to the course if the user is already enrolled', async () => {
|
|
126
|
+
// Joanie session requests
|
|
127
|
+
let nbApiCalls = 3;
|
|
128
|
+
const user = UserFactory().one();
|
|
129
|
+
const joanieEnrollment = JoanieEnrollment({ is_active: true }).one();
|
|
130
|
+
const courseRun = CourseRunFactory().one();
|
|
131
|
+
courseRun.resource_link = `https://joanie.endpoint/api/v1.0/course-runs/${joanieEnrollment.course_run.id}/`;
|
|
132
|
+
courseRun.state.priority = Priority.ONGOING_OPEN;
|
|
133
|
+
|
|
134
|
+
fetchMock.get(
|
|
135
|
+
`${endpoint}/api/v1.0/enrollments/?course_run_id=${joanieEnrollment.course_run.id}`,
|
|
136
|
+
{
|
|
137
|
+
count: 1,
|
|
138
|
+
next: null,
|
|
139
|
+
previous: null,
|
|
140
|
+
results: [joanieEnrollment],
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
nbApiCalls += 1;
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
render(
|
|
147
|
+
<QueryClientProvider client={createTestQueryClient({ user })}>
|
|
148
|
+
<IntlProvider locale="en">
|
|
149
|
+
<SessionProvider>
|
|
150
|
+
<CourseRunEnrollment courseRun={courseRun} />
|
|
151
|
+
</SessionProvider>
|
|
152
|
+
</IntlProvider>
|
|
153
|
+
</QueryClientProvider>,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const $goToCourseButton = await screen.findByRole('link', { name: 'Go to course' });
|
|
158
|
+
expect($goToCourseButton).toBeInTheDocument();
|
|
159
|
+
expect($goToCourseButton).toHaveAttribute('href', joanieEnrollment.course_run.resource_link);
|
|
160
|
+
expect(screen.getByText('You are enrolled in this course run')).toBeInTheDocument();
|
|
161
|
+
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
162
|
+
});
|
|
163
|
+
|
|
121
164
|
it('shows an "Unenroll" text and allows the user to unenroll', async () => {
|
|
122
165
|
const user = UserFactory().one();
|
|
123
166
|
const courseRun = CourseRunFactory().one();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useReducer } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
|
|
2
2
|
import { defineMessages, FormattedMessage } from 'react-intl';
|
|
3
3
|
import c from 'classnames';
|
|
4
4
|
import { Button } from '@openfun/cunningham-react';
|
|
@@ -12,6 +12,7 @@ import { HttpError } from 'utils/errors/HttpError';
|
|
|
12
12
|
import useCourseEnrollment from 'widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment';
|
|
13
13
|
import { CourseRunUnenrollButton } from 'widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/CourseRunUnenrollmentButton';
|
|
14
14
|
import useDateRelative from 'hooks/useDateRelative';
|
|
15
|
+
import { isEnrollment as isJoanieEnrollment } from 'types/Joanie';
|
|
15
16
|
|
|
16
17
|
const messages = defineMessages({
|
|
17
18
|
enroll: {
|
|
@@ -181,9 +182,8 @@ const reducer = ({ step, context }: ReducerState, action: ReducerAction): Reduce
|
|
|
181
182
|
|
|
182
183
|
const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
|
|
183
184
|
const { user, login } = useSession();
|
|
184
|
-
const { enrollmentIsActive, setEnrollment, canUnenroll, states } =
|
|
185
|
-
props.courseRun.resource_link
|
|
186
|
-
);
|
|
185
|
+
const { enrollment, enrollmentIsActive, setEnrollment, canUnenroll, states } =
|
|
186
|
+
useCourseEnrollment(props.courseRun.resource_link);
|
|
187
187
|
const startDate = new Date(props.courseRun.start);
|
|
188
188
|
const isStarted = new Date() > startDate;
|
|
189
189
|
const relativeStartDate = useDateRelative(startDate);
|
|
@@ -226,6 +226,16 @@ const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
|
|
|
226
226
|
[courseRun, currentUser, dispatch, enrollmentIsActive],
|
|
227
227
|
);
|
|
228
228
|
|
|
229
|
+
const LmsCourseLink = useMemo(() => {
|
|
230
|
+
if (states.isLoading) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
if (isJoanieEnrollment(enrollment)) {
|
|
234
|
+
return enrollment.course_run.resource_link;
|
|
235
|
+
}
|
|
236
|
+
return courseRun.resource_link;
|
|
237
|
+
}, [courseRun, enrollment]);
|
|
238
|
+
|
|
229
239
|
useEffect(() => {
|
|
230
240
|
dispatch({
|
|
231
241
|
payload: { currentUser: user, isEnrolled: enrollmentIsActive },
|
|
@@ -303,7 +313,8 @@ const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
|
|
|
303
313
|
return isStarted ? (
|
|
304
314
|
<div>
|
|
305
315
|
<Button
|
|
306
|
-
href={
|
|
316
|
+
href={LmsCourseLink === null ? '#' : LmsCourseLink}
|
|
317
|
+
disabled={LmsCourseLink === null}
|
|
307
318
|
className="course-run-enrollment__cta"
|
|
308
319
|
fullWidth={true}
|
|
309
320
|
>
|
|
@@ -21,22 +21,18 @@ const useCourseEnrollment = (resourceLink: string) => {
|
|
|
21
21
|
const queryClient = useQueryClient();
|
|
22
22
|
const EnrollmentAPI = EnrollmentApiInterface(resourceLink);
|
|
23
23
|
|
|
24
|
-
const [{ data: enrollment, isError, isLoading }, queryKey] = useSessionQuery(
|
|
24
|
+
const [{ data: enrollment, isError, isLoading: isEnrollmentLoading }, queryKey] = useSessionQuery(
|
|
25
25
|
['enrollment', resourceLink],
|
|
26
26
|
async () => {
|
|
27
27
|
return EnrollmentAPI.get(resourceLink, user!);
|
|
28
28
|
},
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
const [{ data: isActive, refetch: refetchIsActive }] =
|
|
32
|
-
[...queryKey, 'is_active'],
|
|
33
|
-
async () => EnrollmentAPI.isEnrolled(enrollment),
|
|
34
|
-
{
|
|
31
|
+
const [{ data: isActive, refetch: refetchIsActive, isLoading: isActiveLoading }] =
|
|
32
|
+
useSessionQuery([...queryKey, 'is_active'], async () => EnrollmentAPI.isEnrolled(enrollment), {
|
|
35
33
|
// Enrollment is null if it has been fetched
|
|
36
|
-
enabled: !!user && enrollment !== undefined && !
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
|
|
34
|
+
enabled: !!user && enrollment !== undefined && !isEnrollmentLoading,
|
|
35
|
+
});
|
|
40
36
|
const { mutateAsync } = useSessionMutation({
|
|
41
37
|
mutationFn: (activeEnrollment: boolean = true) =>
|
|
42
38
|
EnrollmentAPI.set(resourceLink, user!, enrollment, activeEnrollment),
|
|
@@ -61,6 +57,7 @@ const useCourseEnrollment = (resourceLink: string) => {
|
|
|
61
57
|
errors: {
|
|
62
58
|
get: isError,
|
|
63
59
|
},
|
|
60
|
+
isLoading: isEnrollmentLoading || isActiveLoading,
|
|
64
61
|
},
|
|
65
62
|
};
|
|
66
63
|
};
|