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.
Files changed (33) hide show
  1. package/js/api/joanie.ts +13 -0
  2. package/js/hooks/useCourseOrders/index.ts +32 -0
  3. package/js/{pages/TeacherDashboardContractsLayout/hooks → hooks}/useDefaultOrganizationId/index.spec.tsx +6 -42
  4. package/js/hooks/useOrganizations/index.ts +4 -4
  5. package/js/hooks/useResources/index.tsx +2 -0
  6. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +68 -83
  7. package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.spec.tsx +21 -62
  8. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +1 -1
  9. package/js/pages/TeacherDashboardContractsLayout/styles.scss +0 -4
  10. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +62 -0
  11. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +123 -0
  12. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnersFiltersBar/index.spec.tsx +70 -0
  13. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnersFiltersBar/index.tsx +31 -0
  14. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +158 -0
  15. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +44 -0
  16. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +385 -0
  17. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +141 -0
  18. package/js/settings/index.ts +1 -7
  19. package/js/settings/settings.prod.ts +1 -0
  20. package/js/types/Joanie.ts +47 -0
  21. package/js/utils/OrderHelper/index.ts +5 -1
  22. package/js/utils/test/factories/cunningham.ts +13 -0
  23. package/js/utils/test/factories/joanie.ts +44 -0
  24. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +8 -2
  25. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +1 -1
  26. package/js/widgets/Dashboard/components/FilterOrganization/index.tsx +27 -10
  27. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +36 -76
  28. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +6 -1
  29. package/js/widgets/Dashboard/utils/teacherRouteMessages.tsx +23 -0
  30. package/js/widgets/Dashboard/utils/teacherRoutes.tsx +31 -0
  31. package/package.json +1 -1
  32. package/scss/objects/_dashboard.scss +7 -0
  33. /package/js/{pages/TeacherDashboardContractsLayout/hooks → hooks}/useDefaultOrganizationId/index.tsx +0 -0
@@ -36,12 +36,24 @@ import {
36
36
  OrderGroup,
37
37
  CertificateProduct,
38
38
  CredentialProduct,
39
+ NestedCourseOrder,
40
+ UserLight,
41
+ ContractLight,
42
+ DefinitionResourcesProduct,
39
43
  } from 'types/Joanie';
40
44
  import { CourseStateFactory } from 'utils/test/factories/richie';
41
45
  import { FactoryHelper } from 'utils/test/factories/helper';
42
46
  import { JoanieUserApiAbilityActions, JoanieUserProfile } from 'types/User';
43
47
  import { factory } from './factories';
44
48
 
49
+ export const UserLightFactory = factory((): UserLight => {
50
+ return {
51
+ id: faker.string.uuid(),
52
+ username: faker.internet.userName(),
53
+ full_name: faker.person.fullName(),
54
+ email: faker.internet.email(),
55
+ };
56
+ });
45
57
  export const JoanieUserProfileFactory = factory((): JoanieUserProfile => {
46
58
  return {
47
59
  id: faker.string.uuid(),
@@ -131,6 +143,14 @@ export const ContractFactory = factory((): Contract => {
131
143
  };
132
144
  });
133
145
 
146
+ export const ContractLightFactory = factory((): ContractLight => {
147
+ return {
148
+ id: faker.string.uuid(),
149
+ student_signed_on: faker.date.past().toISOString(),
150
+ organization_signed_on: null,
151
+ };
152
+ });
153
+
134
154
  export const OrganizationFactory = factory((): Organization => {
135
155
  return {
136
156
  id: faker.string.uuid(),
@@ -203,6 +223,14 @@ export const CertificateCourseProductFactory = factory((): CourseProduct => {
203
223
  // factories.js need a feature that return a random factory from a list.
204
224
  export const ProductFactory = CredentialProductFactory;
205
225
 
226
+ export const DefinitionResourcesProductFactory = factory((): DefinitionResourcesProduct => {
227
+ return {
228
+ id: faker.string.uuid(),
229
+ certificate_definition_id: null,
230
+ contract_definition_id: null,
231
+ };
232
+ });
233
+
206
234
  export const CourseRunFactory = factory((): CourseRun => {
207
235
  return {
208
236
  course: CourseLightFactory().one(),
@@ -273,6 +301,22 @@ export const OrderGroupFullFactory = factory((): OrderGroup => {
273
301
  };
274
302
  });
275
303
 
304
+ export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
305
+ return {
306
+ id: faker.string.uuid(),
307
+ created_on: faker.date.past().toISOString(),
308
+ owner: UserLightFactory().one(),
309
+ course_id: faker.string.uuid(),
310
+ product_id: faker.string.uuid(),
311
+ state: OrderState.VALIDATED,
312
+ enrollment_id: faker.string.uuid(),
313
+ organization: OrganizationFactory().one(),
314
+ certificate_id: faker.string.uuid(),
315
+ product: DefinitionResourcesProductFactory().one(),
316
+ contract: ContractLightFactory().one(),
317
+ };
318
+ });
319
+
276
320
  export const CourseProductRelationFactory = factory((): CourseProductRelation => {
277
321
  return {
278
322
  id: faker.string.uuid(),
@@ -1,6 +1,12 @@
1
1
  import { FormattedMessage, defineMessages } from 'react-intl';
2
2
  import { useEffect } from 'react';
3
- import { CertificateOrder, CredentialOrder, OrderState, ContractDefinition } from 'types/Joanie';
3
+ import {
4
+ CertificateOrder,
5
+ CredentialOrder,
6
+ OrderState,
7
+ ContractDefinition,
8
+ NestedCourseOrder,
9
+ } from 'types/Joanie';
4
10
  import { StringHelper } from 'utils/StringHelper';
5
11
  import { handle } from 'utils/errors/handle';
6
12
  import { OrderHelper } from 'utils/OrderHelper';
@@ -52,7 +58,7 @@ export const messages = defineMessages({
52
58
  });
53
59
 
54
60
  interface OrderStateMessageProps {
55
- order: CredentialOrder | CertificateOrder;
61
+ order: CredentialOrder | CertificateOrder | NestedCourseOrder;
56
62
  contractDefinition?: ContractDefinition;
57
63
  }
58
64
 
@@ -5,7 +5,7 @@ import { ContractState, CourseProductRelation, Organization } from 'types/Joanie
5
5
  import useTeacherPendingContractsCount from 'hooks/useTeacherPendingContractsCount';
6
6
  import { ContractActions } from 'utils/AbilitiesHelper/types';
7
7
  import useContractAbilities from 'hooks/useContractAbilities';
8
- import useDefaultOrganizationId from 'pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId';
8
+ import useDefaultOrganizationId from 'hooks/useDefaultOrganizationId';
9
9
  import MenuNavLink from '../MenuNavLink';
10
10
 
11
11
  interface ContractNavLinkProps {
@@ -1,8 +1,9 @@
1
1
  import { defineMessages, useIntl } from 'react-intl';
2
2
  import { Select, SelectProps } from '@openfun/cunningham-react';
3
- import { useEffect } from 'react';
3
+ import { useEffect, useMemo } from 'react';
4
4
  import { useOrganizations } from 'hooks/useOrganizations';
5
5
  import { Spinner } from 'components/Spinner';
6
+ import { Organization } from 'types/Joanie';
6
7
 
7
8
  export const messages = defineMessages({
8
9
  organizationFilterLabel: {
@@ -10,20 +11,35 @@ export const messages = defineMessages({
10
11
  description: 'Use as organization filter label',
11
12
  id: 'components.ListFilterOrganization.organizationFilterLabel',
12
13
  },
14
+ allOrganizationOption: {
15
+ defaultMessage: 'All organizations',
16
+ description: 'Use as organization filter option label for "all organizations"',
17
+ id: 'components.ListFilterOrganization.allOrganizationOption',
18
+ },
13
19
  });
14
20
 
15
21
  interface FilterOrganizationProps {
16
22
  defaultValue?: string;
23
+ organizationList?: Organization[];
17
24
  onChange: ({ organization_id }: { organization_id?: string }) => void;
25
+ clearable?: boolean;
18
26
  }
19
27
 
20
- const FilterOrganization = ({ defaultValue, onChange }: FilterOrganizationProps) => {
28
+ const FilterOrganization = ({
29
+ defaultValue,
30
+ organizationList,
31
+ onChange,
32
+ clearable = false,
33
+ }: FilterOrganizationProps) => {
21
34
  const intl = useIntl();
22
35
  const {
23
- items: organizations,
36
+ items: fetchedOrganizationList,
24
37
  states: { isFetched },
25
- } = useOrganizations();
26
-
38
+ } = useOrganizations(undefined, { enabled: !organizationList });
39
+ const isReady = useMemo(() => {
40
+ return organizationList || isFetched;
41
+ }, [organizationList, isFetched]);
42
+ const organizations = organizationList || fetchedOrganizationList;
27
43
  const organizationOptions = organizations.map((organization) => ({
28
44
  label: organization.title,
29
45
  value: organization.id,
@@ -35,21 +51,22 @@ const FilterOrganization = ({ defaultValue, onChange }: FilterOrganizationProps)
35
51
  };
36
52
 
37
53
  useEffect(() => {
38
- if (isFetched && defaultValue === undefined) {
54
+ if (!clearable && isReady && defaultValue === undefined) {
39
55
  onChange({ organization_id: organizationOptions[0]?.value });
40
56
  }
41
57
  }, [defaultValue, isFetched]);
42
58
 
43
- if (!isFetched) return <Spinner />;
59
+ if (!isReady) {
60
+ return <Spinner />;
61
+ }
44
62
 
45
63
  return (
46
64
  <Select
47
65
  label={intl.formatMessage(messages.organizationFilterLabel)}
48
66
  options={organizationOptions}
49
- defaultValue={defaultValue || organizationOptions[0].value}
67
+ defaultValue={defaultValue}
50
68
  onChange={handleChange}
51
- disabled={!isFetched}
52
- clearable={false}
69
+ clearable={clearable}
53
70
  searchable={true}
54
71
  />
55
72
  );
@@ -1,21 +1,12 @@
1
1
  import fetchMock from 'fetch-mock';
2
- import { MemoryRouter, Route, Routes } from 'react-router-dom';
3
- import { render, screen } from '@testing-library/react';
4
- import { IntlProvider, createIntl } from 'react-intl';
5
- import { QueryClientProvider } from '@tanstack/react-query';
6
- import { CunninghamProvider } from '@openfun/cunningham-react';
7
- import { PropsWithChildren } from 'react';
8
- import { CourseListItem, CourseProductRelation, Organization } from 'types/Joanie';
9
- import {
10
- RichieContextFactory as mockRichieContextFactory,
11
- UserFactory,
12
- } from 'utils/test/factories/richie';
2
+ import { screen } from '@testing-library/react';
3
+ import { createIntl } from 'react-intl';
4
+ import { CourseListItem } from 'types/Joanie';
5
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
13
6
  import {
14
7
  TEACHER_DASHBOARD_ROUTE_LABELS,
15
8
  TeacherDashboardPaths,
16
9
  } from 'widgets/Dashboard/utils/teacherRouteMessages';
17
- import { createTestQueryClient } from 'utils/test/createTestQueryClient';
18
- import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
19
10
 
20
11
  import {
21
12
  CourseFactory,
@@ -23,6 +14,8 @@ import {
23
14
  OrganizationFactory,
24
15
  } from 'utils/test/factories/joanie';
25
16
  import { expectNoSpinner } from 'utils/test/expectSpinner';
17
+ import { render } from 'utils/test/render';
18
+ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
26
19
  import { TeacherDashboardCourseSidebar, messages } from '.';
27
20
 
28
21
  jest.mock('utils/context', () => ({
@@ -41,58 +34,11 @@ jest.mock('utils/indirection/window', () => ({
41
34
 
42
35
  const intl = createIntl({ locale: 'en' });
43
36
 
44
- interface RenderTeacherDashboardCourseSidebarProps {
45
- courseId: CourseListItem['id'];
46
- organizationId?: Organization['id'];
47
- courseProductRelationId?: CourseProductRelation['id'];
48
- }
49
-
50
- const Wrapper = ({
51
- children,
52
- courseId,
53
- organizationId,
54
- courseProductRelationId,
55
- }: PropsWithChildren<RenderTeacherDashboardCourseSidebarProps>) => {
56
- let routePath = '/:courseId';
57
- let initialEntry = `/${courseId}`;
58
-
59
- if (courseProductRelationId) {
60
- routePath += '/:courseProductRelationId';
61
- initialEntry += `/${courseProductRelationId}`;
62
- }
63
- if (organizationId) {
64
- routePath = '/:organizationId' + routePath;
65
- initialEntry = `/${organizationId}` + initialEntry;
66
- }
67
- return (
68
- <IntlProvider locale="en">
69
- <QueryClientProvider client={createTestQueryClient({ user: UserFactory().one() })}>
70
- <JoanieSessionProvider>
71
- <CunninghamProvider>
72
- <MemoryRouter initialEntries={[initialEntry]}>
73
- <Routes>
74
- <Route path={routePath} element={children} />
75
- </Routes>
76
- </MemoryRouter>
77
- </CunninghamProvider>
78
- </JoanieSessionProvider>
79
- </QueryClientProvider>
80
- </IntlProvider>
81
- );
82
- };
83
-
84
37
  describe('<TeacherDashboardCourseSidebar/>', () => {
38
+ const joanieSessionData = setupJoanieSession();
85
39
  let nbApiRequest: number;
86
40
  beforeEach(() => {
87
- // JoanieSessionProvider inital requests
88
- nbApiRequest = 3;
89
- fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
90
- fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
91
- fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
92
- });
93
- afterEach(() => {
94
- jest.clearAllMocks();
95
- fetchMock.restore();
41
+ nbApiRequest = joanieSessionData.nbSessionApiRequest;
96
42
  });
97
43
 
98
44
  it('should display syllabus link', async () => {
@@ -100,11 +46,12 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
100
46
  fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/${course.id}/`, course);
101
47
  nbApiRequest += 1; // call to course
102
48
 
103
- render(
104
- <Wrapper courseId={course.id}>
105
- <TeacherDashboardCourseSidebar />
106
- </Wrapper>,
107
- );
49
+ render(<TeacherDashboardCourseSidebar />, {
50
+ routerOptions: {
51
+ path: '/:courseId',
52
+ initialEntries: [`/${course.id}`],
53
+ },
54
+ });
108
55
 
109
56
  await expectNoSpinner('Loading course...');
110
57
  const link = screen.getByRole('link', {
@@ -129,6 +76,7 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
129
76
  expectedRoutes: [
130
77
  TeacherDashboardPaths.COURSE_PRODUCT,
131
78
  TeacherDashboardPaths.COURSE_CONTRACTS,
79
+ TeacherDashboardPaths.COURSE_PRODUCT_LEARNER_LIST,
132
80
  ],
133
81
  },
134
82
  {
@@ -146,6 +94,7 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
146
94
  expectedRoutes: [
147
95
  TeacherDashboardPaths.ORGANIZATION_PRODUCT,
148
96
  TeacherDashboardPaths.ORGANIZATION_PRODUCT_CONTRACTS,
97
+ TeacherDashboardPaths.ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST,
149
98
  ],
150
99
  },
151
100
  ])(
@@ -179,21 +128,32 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
179
128
  `https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
180
129
  courseProductRelation,
181
130
  );
131
+ nbApiRequest += 1;
132
+ fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', []);
182
133
  } else {
183
134
  // mock api for course
184
135
  nbApiRequest += 1;
185
136
  fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/${course.id}/`, course);
186
137
  }
187
138
 
188
- render(
189
- <Wrapper
190
- courseId={course.id}
191
- courseProductRelationId={courseProductRelation ? courseProductRelation.id : undefined}
192
- organizationId={organization ? organization.id : undefined}
193
- >
194
- <TeacherDashboardCourseSidebar />
195
- </Wrapper>,
196
- );
139
+ let routePath = '/:courseId';
140
+ let initialEntry = `/${course.id}`;
141
+
142
+ if (courseProductRelation) {
143
+ routePath += '/:courseProductRelationId';
144
+ initialEntry += `/${courseProductRelation.id}`;
145
+ }
146
+ if (organization) {
147
+ routePath = '/:organizationId' + routePath;
148
+ initialEntry = `/${organization.id}` + initialEntry;
149
+ }
150
+
151
+ render(<TeacherDashboardCourseSidebar />, {
152
+ routerOptions: {
153
+ path: routePath,
154
+ initialEntries: [initialEntry],
155
+ },
156
+ });
197
157
 
198
158
  await expectNoSpinner('Loading course...');
199
159
  expectedRoutes.forEach((expectedRoute) => {
@@ -11,13 +11,18 @@ export const getMenuRoutes = ({ courseProductRelationId, organizationId }: GetMe
11
11
  return [
12
12
  TeacherDashboardPaths.ORGANIZATION_PRODUCT,
13
13
  TeacherDashboardPaths.ORGANIZATION_PRODUCT_CONTRACTS,
14
+ TeacherDashboardPaths.ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST,
14
15
  ];
15
16
  }
16
17
  return [TeacherDashboardPaths.ORGANIZATION_COURSE_GENERAL_INFORMATION];
17
18
  }
18
19
 
19
20
  if (courseProductRelationId) {
20
- return [TeacherDashboardPaths.COURSE_PRODUCT, TeacherDashboardPaths.COURSE_PRODUCT_CONTRACTS];
21
+ return [
22
+ TeacherDashboardPaths.COURSE_PRODUCT,
23
+ TeacherDashboardPaths.COURSE_PRODUCT_CONTRACTS,
24
+ TeacherDashboardPaths.COURSE_PRODUCT_LEARNER_LIST,
25
+ ];
21
26
  }
22
27
  return [TeacherDashboardPaths.COURSE_GENERAL_INFORMATION];
23
28
  };
@@ -12,10 +12,12 @@ export enum TeacherDashboardPaths {
12
12
  ORGANIZATION_PRODUCT = '/teacher/organizations/{organizationId}/courses/{courseId}/products/{courseProductRelationId}',
13
13
  ORGANIZATION_COURSE_CONTRACTS = '/teacher/organizations/{organizationId}/courses/{courseId}/contracts',
14
14
  ORGANIZATION_PRODUCT_CONTRACTS = '/teacher/organizations/{organizationId}/courses/{courseId}/products/{courseProductRelationId}/contracts',
15
+ ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST = '/teacher/organizations/{organizationId}/courses/{courseId}/products/{courseProductRelationId}/learners',
15
16
  ORGANIZATION_COURSE_GENERAL_INFORMATION = '/teacher/organizations/{organizationId}/courses/{courseId}/information',
16
17
  COURSE = '/teacher/courses/{courseId}',
17
18
  COURSE_GENERAL_INFORMATION = '/teacher/courses/{courseId}/information',
18
19
  COURSE_PRODUCT = '/teacher/courses/{courseId}/products/{courseProductRelationId}',
20
+ COURSE_PRODUCT_LEARNER_LIST = '/teacher/courses/{courseId}/products/{courseProductRelationId}/learners',
19
21
  COURSE_CONTRACTS = '/teacher/courses/{courseId}/contracts',
20
22
  COURSE_PRODUCT_CONTRACTS = '/teacher/courses/{courseId}/products/{courseProductRelationId}/contracts',
21
23
  }
@@ -69,6 +71,12 @@ export const TEACHER_DASHBOARD_ROUTE_PATHS = defineMessages<TeacherDashboardPath
69
71
  defaultMessage:
70
72
  '/teacher/organizations/{organizationId}/courses/{courseId}/products/{courseProductRelationId}',
71
73
  },
74
+ [TeacherDashboardPaths.ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST]: {
75
+ id: 'components.TeacherDashboard.TeacherDashboardRoutes.organization.course.product.learnerList.path',
76
+ description: "The path to display the organization product's learner list view.",
77
+ defaultMessage:
78
+ '/teacher/organizations/{organizationId}/courses/{courseId}/products/{courseProductRelationId}/learners',
79
+ },
72
80
  [TeacherDashboardPaths.COURSE]: {
73
81
  id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.path',
74
82
  description: 'The path to display the course view.',
@@ -84,6 +92,11 @@ export const TEACHER_DASHBOARD_ROUTE_PATHS = defineMessages<TeacherDashboardPath
84
92
  description: 'The path to display the product view.',
85
93
  defaultMessage: '/teacher/courses/{courseId}/products/{courseProductRelationId}',
86
94
  },
95
+ [TeacherDashboardPaths.COURSE_PRODUCT_LEARNER_LIST]: {
96
+ id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.product.learnerList.path',
97
+ description: "The path to display the product's learner list view.",
98
+ defaultMessage: '/teacher/courses/{courseId}/products/{courseProductRelationId}/learners',
99
+ },
87
100
  [TeacherDashboardPaths.COURSE_CONTRACTS]: {
88
101
  id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.contracts.path',
89
102
  description: 'The path to display a course contracts view.',
@@ -143,6 +156,11 @@ export const TEACHER_DASHBOARD_ROUTE_LABELS = defineMessages<TeacherDashboardPat
143
156
  description: 'Label of the organization product view.',
144
157
  defaultMessage: 'General information',
145
158
  },
159
+ [TeacherDashboardPaths.ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST]: {
160
+ id: 'components.TeacherDashboard.TeacherDashboardRoutes.organization.course.product.learnerList.label',
161
+ description: "Label to display the organization product's learner list view.",
162
+ defaultMessage: 'Learners',
163
+ },
146
164
  [TeacherDashboardPaths.COURSE]: {
147
165
  id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.label',
148
166
  description: 'Label of the course root view.',
@@ -158,6 +176,11 @@ export const TEACHER_DASHBOARD_ROUTE_LABELS = defineMessages<TeacherDashboardPat
158
176
  description: 'Label of the product view.',
159
177
  defaultMessage: 'General information',
160
178
  },
179
+ [TeacherDashboardPaths.COURSE_PRODUCT_LEARNER_LIST]: {
180
+ id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.product.learnerList.label',
181
+ description: "Label to display the product's learner list view.",
182
+ defaultMessage: 'Learners',
183
+ },
161
184
  [TeacherDashboardPaths.COURSE_CONTRACTS]: {
162
185
  id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.contracts.label',
163
186
  description: 'Label of the course contracts view.',
@@ -14,6 +14,7 @@ import {
14
14
  } from 'widgets/Dashboard/utils/teacherRouteMessages';
15
15
  import { TeacherDashboardCourseLoader } from 'pages/TeacherDashboardCourseLoader';
16
16
  import { TeacherDashboardTrainingLoader } from 'pages/TeacherDashboardTraining';
17
+ import { TeacherDashboardCourseLearnersLayout } from 'pages/TeacherDashboardCourseLearnersLayout';
17
18
 
18
19
  export function getTeacherDashboardRoutes() {
19
20
  const intl = useIntl();
@@ -81,6 +82,19 @@ export function getTeacherDashboardRoutes() {
81
82
  ],
82
83
  },
83
84
  },
85
+ {
86
+ path: getRoutePath(TeacherDashboardPaths.COURSE_PRODUCT_LEARNER_LIST, {
87
+ courseId: ':courseId',
88
+ courseProductRelationId: ':courseProductRelationId',
89
+ }),
90
+ element: <TeacherDashboardCourseLearnersLayout />,
91
+ handle: {
92
+ crumbLabel:
93
+ TEACHER_DASHBOARD_ROUTE_LABELS[
94
+ TeacherDashboardPaths.COURSE_PRODUCT_LEARNER_LIST
95
+ ],
96
+ },
97
+ },
84
98
  ],
85
99
  },
86
100
  {
@@ -176,6 +190,23 @@ export function getTeacherDashboardRoutes() {
176
190
  ],
177
191
  },
178
192
  },
193
+ {
194
+ path: getRoutePath(
195
+ TeacherDashboardPaths.ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST,
196
+ {
197
+ organizationId: ':organizationId',
198
+ courseId: ':courseId',
199
+ courseProductRelationId: ':courseProductRelationId',
200
+ },
201
+ ),
202
+ element: <TeacherDashboardCourseLearnersLayout />,
203
+ handle: {
204
+ crumbLabel:
205
+ TEACHER_DASHBOARD_ROUTE_LABELS[
206
+ TeacherDashboardPaths.ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST
207
+ ],
208
+ },
209
+ },
179
210
  ],
180
211
  },
181
212
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev103",
3
+ "version": "2.25.0-b2.dev111",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -80,4 +80,11 @@
80
80
  }
81
81
  }
82
82
  }
83
+
84
+ .teacher-contract-page,
85
+ .teacher-training-learners-page {
86
+ background: #fff;
87
+ border-radius: 6px;
88
+ padding: 1rem;
89
+ }
83
90
  }