richie-education 2.25.0-b2.dev103 → 2.25.0-b2.dev111
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/api/joanie.ts +13 -0
- package/js/hooks/useCourseOrders/index.ts +32 -0
- package/js/{pages/TeacherDashboardContractsLayout/hooks → hooks}/useDefaultOrganizationId/index.spec.tsx +6 -42
- package/js/hooks/useOrganizations/index.ts +4 -4
- package/js/hooks/useResources/index.tsx +2 -0
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +68 -83
- package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.spec.tsx +21 -62
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +1 -1
- package/js/pages/TeacherDashboardContractsLayout/styles.scss +0 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +62 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +123 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnersFiltersBar/index.spec.tsx +70 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnersFiltersBar/index.tsx +31 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +158 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +44 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +385 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +141 -0
- package/js/settings/index.ts +1 -7
- package/js/settings/settings.prod.ts +1 -0
- package/js/types/Joanie.ts +47 -0
- package/js/utils/OrderHelper/index.ts +5 -1
- package/js/utils/test/factories/cunningham.ts +13 -0
- package/js/utils/test/factories/joanie.ts +44 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +8 -2
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +1 -1
- package/js/widgets/Dashboard/components/FilterOrganization/index.tsx +27 -10
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +36 -76
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +6 -1
- package/js/widgets/Dashboard/utils/teacherRouteMessages.tsx +23 -0
- package/js/widgets/Dashboard/utils/teacherRoutes.tsx +31 -0
- package/package.json +1 -1
- package/scss/objects/_dashboard.scss +7 -0
- /package/js/{pages/TeacherDashboardContractsLayout/hooks → hooks}/useDefaultOrganizationId/index.tsx +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import fetchMock from 'fetch-mock';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import queryString from 'query-string';
|
|
5
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
6
|
+
import {
|
|
7
|
+
CourseProductRelationFactory,
|
|
8
|
+
NestedCourseOrderFactory,
|
|
9
|
+
OrganizationFactory,
|
|
10
|
+
} from 'utils/test/factories/joanie';
|
|
11
|
+
import { expectNoSpinner } from 'utils/test/expectSpinner';
|
|
12
|
+
import { PER_PAGE } from 'settings';
|
|
13
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
14
|
+
import { expectBannerError } from 'utils/test/expectBanner';
|
|
15
|
+
import { render } from 'utils/test/render';
|
|
16
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
17
|
+
import { TeacherDashboardCourseLearnersLayout } from '.';
|
|
18
|
+
|
|
19
|
+
jest.mock('utils/context', () => ({
|
|
20
|
+
__esModule: true,
|
|
21
|
+
default: mockRichieContextFactory({
|
|
22
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.endpoint.test' },
|
|
23
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
24
|
+
}).one(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('pages/TeacherDashboardCourseLearnersLayout', () => {
|
|
28
|
+
setupJoanieSession();
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
// CourseSidebar api calls
|
|
31
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/courses/', {});
|
|
32
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', []);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it.each([
|
|
36
|
+
{
|
|
37
|
+
expectedLabel: 'should not render organization filter without organizations',
|
|
38
|
+
nbOrganization: 0,
|
|
39
|
+
organizationFilterShouldBeDisplayed: false,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
expectedLabel: 'should not render organization filter with 1 organization',
|
|
43
|
+
nbOrganization: 1,
|
|
44
|
+
organizationFilterShouldBeDisplayed: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
expectedLabel: 'should render organization filter with 2 organization',
|
|
48
|
+
nbOrganization: 2,
|
|
49
|
+
organizationFilterShouldBeDisplayed: true,
|
|
50
|
+
},
|
|
51
|
+
])('$expectedLabel', async ({ nbOrganization, organizationFilterShouldBeDisplayed }) => {
|
|
52
|
+
const courseProductRelation = CourseProductRelationFactory().one();
|
|
53
|
+
const organizationList = OrganizationFactory().many(nbOrganization);
|
|
54
|
+
fetchMock.get(
|
|
55
|
+
`https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
|
|
56
|
+
organizationList,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Course sidebar query
|
|
60
|
+
fetchMock.get(
|
|
61
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
|
|
62
|
+
{},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// First request before finding default organizationId
|
|
66
|
+
const courseOrderListQueryParams = {
|
|
67
|
+
course_product_relation_id: courseProductRelation.id,
|
|
68
|
+
page: 1,
|
|
69
|
+
page_size: PER_PAGE.courseLearnerList,
|
|
70
|
+
};
|
|
71
|
+
fetchMock.get(
|
|
72
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify(courseOrderListQueryParams, { sort: false })}`,
|
|
73
|
+
[],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (organizationList.length > 0) {
|
|
77
|
+
// Course sidebar query
|
|
78
|
+
fetchMock.get(
|
|
79
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organizationList[0].id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
80
|
+
[],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Second request when default organizationId is fetched
|
|
84
|
+
fetchMock.get(
|
|
85
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify({ organization_id: organizationList[0].id, ...courseOrderListQueryParams }, { sort: false })}`,
|
|
86
|
+
[],
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
render(<TeacherDashboardCourseLearnersLayout />, {
|
|
90
|
+
routerOptions: {
|
|
91
|
+
path: '/:courseId/:courseProductRelationId',
|
|
92
|
+
initialEntries: [`/${courseProductRelation.course.id}/${courseProductRelation.id}`],
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await expectNoSpinner();
|
|
97
|
+
if (organizationFilterShouldBeDisplayed) {
|
|
98
|
+
expect(
|
|
99
|
+
await screen.findByRole('combobox', {
|
|
100
|
+
name: /Organization/,
|
|
101
|
+
hidden: true,
|
|
102
|
+
}),
|
|
103
|
+
).toBeInTheDocument();
|
|
104
|
+
} else {
|
|
105
|
+
expect(
|
|
106
|
+
screen.queryByRole('combobox', {
|
|
107
|
+
name: /Organization/,
|
|
108
|
+
hidden: true,
|
|
109
|
+
}),
|
|
110
|
+
).not.toBeInTheDocument();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should call onFiltersChange on organization filter change', async () => {
|
|
115
|
+
const courseProductRelation = CourseProductRelationFactory().one();
|
|
116
|
+
const defaultOrganization = OrganizationFactory().one();
|
|
117
|
+
const otherOrganization = OrganizationFactory().one();
|
|
118
|
+
const organizationList = [defaultOrganization, otherOrganization];
|
|
119
|
+
|
|
120
|
+
// Course sidebar queries
|
|
121
|
+
fetchMock.get(
|
|
122
|
+
`https://joanie.endpoint/api/v1.0/organizations/${defaultOrganization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
123
|
+
[],
|
|
124
|
+
);
|
|
125
|
+
fetchMock.get(
|
|
126
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
|
|
127
|
+
{},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
fetchMock.get(
|
|
131
|
+
`https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
|
|
132
|
+
organizationList,
|
|
133
|
+
);
|
|
134
|
+
// First request before finding default organizationId
|
|
135
|
+
const courseOrderListQueryParams = {
|
|
136
|
+
course_product_relation_id: courseProductRelation.id,
|
|
137
|
+
page: 1,
|
|
138
|
+
page_size: PER_PAGE.courseLearnerList,
|
|
139
|
+
};
|
|
140
|
+
fetchMock.get(
|
|
141
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify(courseOrderListQueryParams, { sort: false })}`,
|
|
142
|
+
[],
|
|
143
|
+
);
|
|
144
|
+
// Second request when default organizationId is fetched
|
|
145
|
+
fetchMock.get(
|
|
146
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify({ organization_id: defaultOrganization.id, ...courseOrderListQueryParams }, { sort: false })}`,
|
|
147
|
+
[],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
render(<TeacherDashboardCourseLearnersLayout />, {
|
|
151
|
+
routerOptions: {
|
|
152
|
+
path: '/:courseId/:courseProductRelationId',
|
|
153
|
+
initialEntries: [`/${courseProductRelation.course.id}/${courseProductRelation.id}`],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const organizationFilter: HTMLInputElement = await screen.findByRole('combobox', {
|
|
158
|
+
name: 'Organization',
|
|
159
|
+
hidden: true,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const user = userEvent.setup();
|
|
163
|
+
await user.click(organizationFilter);
|
|
164
|
+
const optionToSelect = screen.getByRole('option', { name: organizationList[1].title });
|
|
165
|
+
|
|
166
|
+
fetchMock.get(
|
|
167
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify({ organization_id: otherOrganization.id, ...courseOrderListQueryParams }, { sort: false })}`,
|
|
168
|
+
[],
|
|
169
|
+
);
|
|
170
|
+
await user.click(optionToSelect);
|
|
171
|
+
// onload default value is undefine and is onFiltersChange called once
|
|
172
|
+
expect(
|
|
173
|
+
fetchMock.called(
|
|
174
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify({ organization_id: otherOrganization.id, ...courseOrderListQueryParams }, { sort: false })}`,
|
|
175
|
+
),
|
|
176
|
+
).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should render a list of course learners for a course product relation', async () => {
|
|
180
|
+
const defaultOrganization = OrganizationFactory().one();
|
|
181
|
+
const otherOrganization = OrganizationFactory().one();
|
|
182
|
+
const organizationList = [defaultOrganization, otherOrganization];
|
|
183
|
+
const courseProductRelation = CourseProductRelationFactory().one();
|
|
184
|
+
const courseOrderList = NestedCourseOrderFactory().many(3);
|
|
185
|
+
// https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=d390d6a9-8437-4f92-8897-982e12f97259
|
|
186
|
+
// Course sidebar queries
|
|
187
|
+
fetchMock.get(
|
|
188
|
+
`https://joanie.endpoint/api/v1.0/organizations/${defaultOrganization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
189
|
+
[],
|
|
190
|
+
);
|
|
191
|
+
fetchMock.get(
|
|
192
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
|
|
193
|
+
{},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
fetchMock.get(
|
|
197
|
+
`https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
|
|
198
|
+
organizationList,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// First request before finding default organizationId
|
|
202
|
+
const courseOrderListQueryParams = {
|
|
203
|
+
course_product_relation_id: courseProductRelation.id,
|
|
204
|
+
page: 1,
|
|
205
|
+
page_size: PER_PAGE.courseLearnerList,
|
|
206
|
+
};
|
|
207
|
+
fetchMock.get(
|
|
208
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify(courseOrderListQueryParams, { sort: false })}`,
|
|
209
|
+
courseOrderList,
|
|
210
|
+
);
|
|
211
|
+
// Second request when default organizationId is fetched
|
|
212
|
+
fetchMock.get(
|
|
213
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify({ organization_id: defaultOrganization.id, ...courseOrderListQueryParams }, { sort: false })}`,
|
|
214
|
+
courseOrderList,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
render(<TeacherDashboardCourseLearnersLayout />, {
|
|
218
|
+
routerOptions: {
|
|
219
|
+
path: '/:courseId/:courseProductRelationId',
|
|
220
|
+
initialEntries: [`/${courseProductRelation.course.id}/${courseProductRelation.id}`],
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await expectNoSpinner();
|
|
225
|
+
|
|
226
|
+
// Organization filter should have been rendered
|
|
227
|
+
const organizationFilter: HTMLInputElement = await screen.findByRole('combobox', {
|
|
228
|
+
name: 'Organization',
|
|
229
|
+
});
|
|
230
|
+
expect(organizationFilter).toHaveAttribute('value', '');
|
|
231
|
+
|
|
232
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
233
|
+
// Table body should have been rendered with 3 rows (one per courseOrder)
|
|
234
|
+
// Table content is tested in CourseLearnerDataGrid
|
|
235
|
+
courseOrderList.forEach((courseOrder) => {
|
|
236
|
+
expect(screen.getByTestId(courseOrder.id)).toBeInTheDocument();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should render a list of course learners for an organization', async () => {
|
|
241
|
+
const organization = OrganizationFactory().one();
|
|
242
|
+
const courseProductRelation = CourseProductRelationFactory().one();
|
|
243
|
+
const courseOrderList = NestedCourseOrderFactory().many(3);
|
|
244
|
+
|
|
245
|
+
// Course sidebar queries
|
|
246
|
+
fetchMock.get(
|
|
247
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
248
|
+
[],
|
|
249
|
+
);
|
|
250
|
+
fetchMock.get(
|
|
251
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/${courseProductRelation.id}/`,
|
|
252
|
+
{},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// before default organization's fetched, we query all organization to decide if we should display organization filter or not.
|
|
256
|
+
fetchMock.get(
|
|
257
|
+
`https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
|
|
258
|
+
[organization],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const courseOrderListQueryParams = {
|
|
262
|
+
organization_id: organization.id,
|
|
263
|
+
course_product_relation_id: courseProductRelation.id,
|
|
264
|
+
page: 1,
|
|
265
|
+
page_size: PER_PAGE.courseLearnerList,
|
|
266
|
+
};
|
|
267
|
+
fetchMock.get(
|
|
268
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify(courseOrderListQueryParams, { sort: false })}`,
|
|
269
|
+
courseOrderList,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
render(<TeacherDashboardCourseLearnersLayout />, {
|
|
273
|
+
routerOptions: {
|
|
274
|
+
path: '/:organizationId/:courseId/:courseProductRelationId',
|
|
275
|
+
initialEntries: [
|
|
276
|
+
`/${organization.id}/${courseProductRelation.course.id}/${courseProductRelation.id}`,
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await expectNoSpinner();
|
|
282
|
+
|
|
283
|
+
// Organization filter should not have been rendered
|
|
284
|
+
const organizationFilter = screen.queryByRole('combobox', { name: 'Organization' });
|
|
285
|
+
expect(organizationFilter).not.toBeInTheDocument();
|
|
286
|
+
|
|
287
|
+
expect(screen.getByRole('table')).toBeInTheDocument();
|
|
288
|
+
// Table body should have been rendered with 3 rows (one per courseOrder)
|
|
289
|
+
// Table content is tested in CourseLearnerDataGrid
|
|
290
|
+
courseOrderList.forEach((courseOrder) => {
|
|
291
|
+
expect(screen.getByTestId(courseOrder.id)).toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should render an empty table if there are no course learners', async () => {
|
|
296
|
+
const organization = OrganizationFactory().one();
|
|
297
|
+
const courseProductRelation = CourseProductRelationFactory().one();
|
|
298
|
+
|
|
299
|
+
// Course sidebar queries
|
|
300
|
+
fetchMock.get(
|
|
301
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
302
|
+
[],
|
|
303
|
+
);
|
|
304
|
+
fetchMock.get(
|
|
305
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/${courseProductRelation.id}/`,
|
|
306
|
+
{},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// before default organization's fetched, we query all organization to decide if we should display organization filter or not.
|
|
310
|
+
fetchMock.get(
|
|
311
|
+
`https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
|
|
312
|
+
[organization],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const courseOrderListQueryParams = {
|
|
316
|
+
organization_id: organization.id,
|
|
317
|
+
course_product_relation_id: courseProductRelation.id,
|
|
318
|
+
page: 1,
|
|
319
|
+
page_size: PER_PAGE.courseLearnerList,
|
|
320
|
+
};
|
|
321
|
+
fetchMock.get(
|
|
322
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify(courseOrderListQueryParams, { sort: false })}`,
|
|
323
|
+
[],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
render(<TeacherDashboardCourseLearnersLayout />, {
|
|
327
|
+
routerOptions: {
|
|
328
|
+
path: '/:organizationId/:courseId/:courseProductRelationId',
|
|
329
|
+
initialEntries: [
|
|
330
|
+
`/${organization.id}/${courseProductRelation.course.id}/${courseProductRelation.id}`,
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await expectNoSpinner();
|
|
336
|
+
|
|
337
|
+
// A message should have been rendered to inform the user that there are no contracts
|
|
338
|
+
screen.getByRole('img', { name: /illustration of an empty table/i });
|
|
339
|
+
screen.getByText(/this table is empty/i);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should render an error banner if an error occured during course learners fetching', async () => {
|
|
343
|
+
const organization = OrganizationFactory().one();
|
|
344
|
+
const courseProductRelation = CourseProductRelationFactory().one();
|
|
345
|
+
|
|
346
|
+
// Course sidebar queries
|
|
347
|
+
fetchMock.get(
|
|
348
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=${PER_PAGE.teacherContractList}`,
|
|
349
|
+
[],
|
|
350
|
+
);
|
|
351
|
+
fetchMock.get(
|
|
352
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/${courseProductRelation.id}/`,
|
|
353
|
+
{},
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// before default organization's fetched, we query all organization to decide if we should display organization filter or not.
|
|
357
|
+
fetchMock.get(
|
|
358
|
+
`https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
|
|
359
|
+
[organization],
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const courseOrderListQueryParams = {
|
|
363
|
+
organization_id: organization.id,
|
|
364
|
+
course_product_relation_id: courseProductRelation.id,
|
|
365
|
+
page: 1,
|
|
366
|
+
page_size: PER_PAGE.courseLearnerList,
|
|
367
|
+
};
|
|
368
|
+
fetchMock.get(
|
|
369
|
+
`https://joanie.endpoint/api/v1.0/courses/${courseProductRelation.course.id}/orders/?${queryString.stringify(courseOrderListQueryParams, { sort: false })}`,
|
|
370
|
+
new Response('', { status: HttpStatusCode.NOT_FOUND }),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
render(<TeacherDashboardCourseLearnersLayout />, {
|
|
374
|
+
routerOptions: {
|
|
375
|
+
path: '/:organizationId/:courseId/:courseProductRelationId',
|
|
376
|
+
initialEntries: [
|
|
377
|
+
`/${organization.id}/${courseProductRelation.course.id}/${courseProductRelation.id}`,
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await expectNoSpinner();
|
|
383
|
+
await expectBannerError('An error occurred while fetching orders. Please retry later.');
|
|
384
|
+
});
|
|
385
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { FormattedMessage, defineMessages } from 'react-intl';
|
|
2
|
+
|
|
3
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
4
|
+
import { SortModel, usePagination } from '@openfun/cunningham-react';
|
|
5
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import { TeacherDashboardCourseSidebar } from 'widgets/Dashboard/components/TeacherDashboardCourseSidebar';
|
|
7
|
+
import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
|
|
8
|
+
import { useCourseOrders } from 'hooks/useCourseOrders';
|
|
9
|
+
import { PER_PAGE } from 'settings';
|
|
10
|
+
import Banner, { BannerType } from 'components/Banner';
|
|
11
|
+
import { CourseOrderResourceQuery, Organization } from 'types/Joanie';
|
|
12
|
+
import { useOrganizations } from 'hooks/useOrganizations';
|
|
13
|
+
import { Spinner } from 'components/Spinner';
|
|
14
|
+
import CourseLearnerDataGrid from './components/CourseLearnerDataGrid';
|
|
15
|
+
import useCourseLearnersFilters from './hooks/useCourseLearnersFilters';
|
|
16
|
+
import CourseLearnersFiltersBar from './components/CourseLearnersFiltersBar';
|
|
17
|
+
|
|
18
|
+
const messages = defineMessages({
|
|
19
|
+
pageTitle: {
|
|
20
|
+
defaultMessage: 'Learners',
|
|
21
|
+
description: "Use for the page title of the course's contracts page",
|
|
22
|
+
id: 'pages.TeacherDashboardCourseLearnersLayout.pageTitle',
|
|
23
|
+
},
|
|
24
|
+
totalLearnerText: {
|
|
25
|
+
defaultMessage:
|
|
26
|
+
'{nbLearners} {nbLearners, plural, one {learner is enrolled} other {learners are enrolled}} for this training',
|
|
27
|
+
description: 'Text to indicate the total number of learner on a training',
|
|
28
|
+
id: 'pages.TeacherDashboardCourseLearnersLayout.totalLearnerText',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const TeacherDashboardCourseLearnersLayout = () => {
|
|
33
|
+
const { organizationId: routeOrganizationId } = useParams<{
|
|
34
|
+
organizationId?: Organization['id'];
|
|
35
|
+
}>();
|
|
36
|
+
const [searchParams] = useSearchParams();
|
|
37
|
+
const page = searchParams.get('page') ?? '1';
|
|
38
|
+
const pagination = usePagination({
|
|
39
|
+
defaultPage: page ? parseInt(page, 10) : 1,
|
|
40
|
+
pageSize: PER_PAGE.teacherContractList,
|
|
41
|
+
});
|
|
42
|
+
const [sortModel, setSortModel] = useState<SortModel>([
|
|
43
|
+
{
|
|
44
|
+
field: 'created_on',
|
|
45
|
+
sort: 'desc',
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// our list is always ordered by created_on asc or desc
|
|
50
|
+
// here we remove the neutral position that remove the "sortable" arrow icon.
|
|
51
|
+
const handleSetSortModel = (model: SortModel) => {
|
|
52
|
+
setSortModel(
|
|
53
|
+
model.length === 0
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
field: 'created_on',
|
|
57
|
+
sort: 'asc',
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: model,
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const { filters, setFilters } = useCourseLearnersFilters();
|
|
65
|
+
const {
|
|
66
|
+
items: organizations,
|
|
67
|
+
states: { isFetched: isOrganizationFetched },
|
|
68
|
+
} = useOrganizations({ course_product_relation_id: filters.course_product_relation_id });
|
|
69
|
+
const {
|
|
70
|
+
items: courseOrders,
|
|
71
|
+
meta,
|
|
72
|
+
states: { fetching, isFetched, error },
|
|
73
|
+
} = useCourseOrders(
|
|
74
|
+
{
|
|
75
|
+
...filters,
|
|
76
|
+
page: pagination.page,
|
|
77
|
+
page_size: PER_PAGE.courseLearnerList,
|
|
78
|
+
},
|
|
79
|
+
{ enabled: isOrganizationFetched },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const showFilters = useMemo(() => {
|
|
83
|
+
return !routeOrganizationId && isOrganizationFetched && organizations.length > 1;
|
|
84
|
+
}, [isOrganizationFetched, organizations.length, routeOrganizationId]);
|
|
85
|
+
|
|
86
|
+
const onFiltersChange = (newFilters: Partial<CourseOrderResourceQuery>) => {
|
|
87
|
+
// Reset pagination
|
|
88
|
+
pagination.setPage(1);
|
|
89
|
+
setFilters((prevFilters) => ({ ...prevFilters, ...newFilters }));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (isFetched && meta?.pagination?.count) {
|
|
94
|
+
pagination.setPagesCount(Math.ceil(meta!.pagination!.count / PER_PAGE.teacherContractList));
|
|
95
|
+
}
|
|
96
|
+
}, [meta, isFetched]);
|
|
97
|
+
|
|
98
|
+
if (error) {
|
|
99
|
+
return <Banner message={error} type={BannerType.ERROR} rounded />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!isOrganizationFetched) {
|
|
103
|
+
return <Spinner aria-labelledby="loading-organizations" />;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<DashboardLayout sidebar={<TeacherDashboardCourseSidebar />}>
|
|
108
|
+
<div className="dashboard__page_title_container">
|
|
109
|
+
<h1 className="dashboard__page_title">
|
|
110
|
+
<FormattedMessage {...messages.pageTitle} />
|
|
111
|
+
</h1>
|
|
112
|
+
{!!meta?.pagination?.count && (
|
|
113
|
+
<div className="list__count-description">
|
|
114
|
+
<FormattedMessage
|
|
115
|
+
{...messages.totalLearnerText}
|
|
116
|
+
values={{ nbLearners: meta?.pagination.count }}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
<div className="teacher-training-learners-page">
|
|
122
|
+
{showFilters && (
|
|
123
|
+
<div className="dashboard__page__actions">
|
|
124
|
+
<CourseLearnersFiltersBar
|
|
125
|
+
defaultValues={filters}
|
|
126
|
+
organizationList={organizations}
|
|
127
|
+
onFiltersChange={onFiltersChange}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
<CourseLearnerDataGrid
|
|
132
|
+
courseOrders={courseOrders}
|
|
133
|
+
sortModel={sortModel}
|
|
134
|
+
setSortModel={handleSetSortModel}
|
|
135
|
+
pagination={pagination}
|
|
136
|
+
isLoading={fetching}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
</DashboardLayout>
|
|
140
|
+
);
|
|
141
|
+
};
|
package/js/settings/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import * as testSettings from './settings.test';
|
|
|
5
5
|
let settingsOverride = {};
|
|
6
6
|
if (process.env.NODE_ENV === 'development') {
|
|
7
7
|
try {
|
|
8
|
-
settingsOverride = require('./settings.
|
|
8
|
+
settingsOverride = require('./settings.dev.ts');
|
|
9
9
|
} catch {
|
|
10
10
|
// no local settings found, do nothing
|
|
11
11
|
}
|
|
@@ -13,12 +13,6 @@ if (process.env.NODE_ENV === 'development') {
|
|
|
13
13
|
settingsOverride = testSettings;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
try {
|
|
17
|
-
settingsOverride = require('./settings.local.ts');
|
|
18
|
-
} catch {
|
|
19
|
-
// no local settings found, do nothing
|
|
20
|
-
}
|
|
21
|
-
|
|
22
16
|
const settings = mergeWith({}, prodSettings, settingsOverride);
|
|
23
17
|
|
|
24
18
|
export const {
|
|
@@ -54,6 +54,7 @@ export const CONTRACT_DOWNLOAD_SETTINGS = {
|
|
|
54
54
|
const DEFAULT_PER_PAGE = 50;
|
|
55
55
|
export const PER_PAGE = {
|
|
56
56
|
teacherContractList: 25,
|
|
57
|
+
courseLearnerList: DEFAULT_PER_PAGE,
|
|
57
58
|
useUnionResources: DEFAULT_PER_PAGE,
|
|
58
59
|
useCourseProductUnion: DEFAULT_PER_PAGE,
|
|
59
60
|
useOrdersEnrollments: DEFAULT_PER_PAGE,
|
package/js/types/Joanie.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface PaginatedParameters {
|
|
|
18
18
|
offset: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export interface UserLight {
|
|
22
|
+
id: string;
|
|
23
|
+
username: string;
|
|
24
|
+
full_name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export interface Organization {
|
|
22
29
|
id: string;
|
|
23
30
|
code: string;
|
|
@@ -25,6 +32,10 @@ export interface Organization {
|
|
|
25
32
|
logo: Nullable<JoanieFile>;
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
export interface OrganizationResourceQuery extends ResourcesQuery {
|
|
36
|
+
course_product_relation_id?: CourseProductRelation['id'];
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export interface ContractDefinition {
|
|
29
40
|
id: string;
|
|
30
41
|
description: string;
|
|
@@ -47,6 +58,8 @@ export interface Contract {
|
|
|
47
58
|
order: NestedCertificateOrder | NestedCredentialOrder;
|
|
48
59
|
}
|
|
49
60
|
|
|
61
|
+
export type ContractLight = Pick<Contract, 'id' | 'organization_signed_on' | 'student_signed_on'>;
|
|
62
|
+
|
|
50
63
|
export interface CourseListItem extends Resource {
|
|
51
64
|
id: string;
|
|
52
65
|
title: string;
|
|
@@ -134,6 +147,12 @@ export interface CourseProduct extends Product {
|
|
|
134
147
|
order: Nullable<OrderLite>;
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
export interface DefinitionResourcesProduct {
|
|
151
|
+
id: Product['id'];
|
|
152
|
+
certificate_definition_id: Nullable<CertificateDefinition['id']>;
|
|
153
|
+
contract_definition_id: Nullable<ContractDefinition['id']>;
|
|
154
|
+
}
|
|
155
|
+
|
|
137
156
|
export interface CourseProductRelation {
|
|
138
157
|
id: string;
|
|
139
158
|
course: CourseLight;
|
|
@@ -301,6 +320,27 @@ export interface NestedCredentialOrder extends AbstractNestedOrder {
|
|
|
301
320
|
|
|
302
321
|
export type OrderEnrollment = Pick<Order, 'id' | 'state' | 'product_id' | 'certificate_id'>;
|
|
303
322
|
|
|
323
|
+
export interface NestedCourseOrder {
|
|
324
|
+
id: Order['id'];
|
|
325
|
+
created_on: Order['created_on'];
|
|
326
|
+
owner: UserLight;
|
|
327
|
+
course_id: Order['id'];
|
|
328
|
+
product_id: Order['id'];
|
|
329
|
+
state: Order['state'];
|
|
330
|
+
enrollment_id: Enrollment['id'];
|
|
331
|
+
organization: Organization;
|
|
332
|
+
certificate_id?: Order['certificate_id'];
|
|
333
|
+
product: DefinitionResourcesProduct;
|
|
334
|
+
contract: ContractLight;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface CourseOrderResourceQuery extends PaginatedResourceQuery {
|
|
338
|
+
course_id?: CourseListItem['id'];
|
|
339
|
+
course_product_relation_id?: CourseProductRelation['id'];
|
|
340
|
+
organization_id?: Organization['id'];
|
|
341
|
+
product_id?: Product['id'];
|
|
342
|
+
}
|
|
343
|
+
|
|
304
344
|
export interface OrderGroup {
|
|
305
345
|
id: string;
|
|
306
346
|
is_active: boolean;
|
|
@@ -557,6 +597,13 @@ export interface API {
|
|
|
557
597
|
products: {
|
|
558
598
|
get(filters?: CourseProductQueryFilters): Promise<Nullable<CourseProductRelation>>;
|
|
559
599
|
};
|
|
600
|
+
orders: {
|
|
601
|
+
get(
|
|
602
|
+
filters?: CourseOrderResourceQuery,
|
|
603
|
+
): CourseOrderResourceQuery extends { id: string }
|
|
604
|
+
? Promise<Nullable<NestedCourseOrder>>
|
|
605
|
+
: Promise<PaginatedResponse<NestedCourseOrder>>;
|
|
606
|
+
};
|
|
560
607
|
};
|
|
561
608
|
organizations: {
|
|
562
609
|
get<Filters extends ResourcesQuery = ResourcesQuery>(
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Order,
|
|
5
5
|
OrderState,
|
|
6
6
|
ContractDefinition,
|
|
7
|
+
NestedCourseOrder,
|
|
7
8
|
} from 'types/Joanie';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -22,7 +23,10 @@ export class OrderHelper {
|
|
|
22
23
|
/**
|
|
23
24
|
* tell us if a order need to be sign by it's owner (the learner user).
|
|
24
25
|
*/
|
|
25
|
-
static orderNeedsSignature(
|
|
26
|
+
static orderNeedsSignature(
|
|
27
|
+
order: Order | NestedCourseOrder,
|
|
28
|
+
contractDefinition?: ContractDefinition,
|
|
29
|
+
) {
|
|
26
30
|
return (
|
|
27
31
|
order?.state === OrderState.VALIDATED &&
|
|
28
32
|
contractDefinition &&
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { usePagination } from '@openfun/cunningham-react';
|
|
2
|
+
import { factory } from './factories';
|
|
3
|
+
|
|
4
|
+
export const PaginationFactory = factory((): ReturnType<typeof usePagination> => {
|
|
5
|
+
return {
|
|
6
|
+
page: 1,
|
|
7
|
+
setPage: jest.fn(),
|
|
8
|
+
onPageChange: jest.fn(),
|
|
9
|
+
pagesCount: 1,
|
|
10
|
+
setPagesCount: jest.fn(),
|
|
11
|
+
pageSize: 50,
|
|
12
|
+
};
|
|
13
|
+
});
|