richie-education 2.25.0-b2.dev69 → 2.25.0-b2.dev77

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 (21) hide show
  1. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +95 -129
  2. package/js/components/ContractFrame/OrganizationContractFrame.tsx +10 -2
  3. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +37 -6
  4. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +11 -2
  5. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +2 -0
  6. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +50 -9
  7. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +8 -2
  8. package/js/types/Joanie.ts +1 -0
  9. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/hooks/useCourseRunPeriodMessage.ts +76 -0
  10. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.spec.tsx → CourseEnrolling/index.spec.tsx} +44 -13
  11. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.stories.tsx → CourseEnrolling/index.stories.tsx} +2 -2
  12. package/js/widgets/Dashboard/components/DashboardItem/{DashboardItemCourseEnrolling.tsx → CourseEnrolling/index.tsx} +38 -57
  13. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +7 -19
  14. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  15. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +76 -24
  16. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +1 -1
  17. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +3 -1
  18. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +136 -30
  19. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +60 -12
  20. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +6 -2
  21. package/package.json +1 -1
@@ -4,7 +4,8 @@ import { render, screen } from '@testing-library/react';
4
4
  import { IntlProvider, createIntl } from 'react-intl';
5
5
  import { QueryClientProvider } from '@tanstack/react-query';
6
6
  import { CunninghamProvider } from '@openfun/cunningham-react';
7
- import { CourseListItem } from 'types/Joanie';
7
+ import { PropsWithChildren } from 'react';
8
+ import { CourseListItem, CourseProductRelation, Organization } from 'types/Joanie';
8
9
  import {
9
10
  RichieContextFactory as mockRichieContextFactory,
10
11
  UserFactory,
@@ -16,7 +17,11 @@ import {
16
17
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
17
18
  import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
18
19
 
19
- import { CourseFactory } from 'utils/test/factories/joanie';
20
+ import {
21
+ CourseFactory,
22
+ CourseProductRelationFactory,
23
+ OrganizationFactory,
24
+ } from 'utils/test/factories/joanie';
20
25
  import { expectNoSpinner } from 'utils/test/expectSpinner';
21
26
  import { TeacherDashboardCourseSidebar, messages } from '.';
22
27
 
@@ -37,26 +42,44 @@ jest.mock('utils/indirection/window', () => ({
37
42
  const intl = createIntl({ locale: 'en' });
38
43
 
39
44
  interface RenderTeacherDashboardCourseSidebarProps {
40
- courseId: string;
45
+ courseId: CourseListItem['id'];
46
+ organizationId?: Organization['id'];
47
+ courseProductRelationId?: CourseProductRelation['id'];
41
48
  }
42
- const renderTeacherDashboardCourseSidebar = ({
49
+
50
+ const Wrapper = ({
51
+ children,
43
52
  courseId,
44
- }: RenderTeacherDashboardCourseSidebarProps) =>
45
- render(
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 (
46
68
  <IntlProvider locale="en">
47
69
  <QueryClientProvider client={createTestQueryClient({ user: UserFactory().one() })}>
48
70
  <JoanieSessionProvider>
49
71
  <CunninghamProvider>
50
- <MemoryRouter initialEntries={[`/${courseId}`]}>
72
+ <MemoryRouter initialEntries={[initialEntry]}>
51
73
  <Routes>
52
- <Route path="/:courseId" element={<TeacherDashboardCourseSidebar />} />
74
+ <Route path={routePath} element={children} />
53
75
  </Routes>
54
76
  </MemoryRouter>
55
77
  </CunninghamProvider>
56
78
  </JoanieSessionProvider>
57
79
  </QueryClientProvider>
58
- </IntlProvider>,
80
+ </IntlProvider>
59
81
  );
82
+ };
60
83
 
61
84
  describe('<TeacherDashboardCourseSidebar/>', () => {
62
85
  let nbApiRequest: number;
@@ -77,7 +100,12 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
77
100
  fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/${course.id}/`, course);
78
101
  nbApiRequest += 1; // call to course
79
102
 
80
- renderTeacherDashboardCourseSidebar({ courseId: course.id });
103
+ render(
104
+ <Wrapper courseId={course.id}>
105
+ <TeacherDashboardCourseSidebar />
106
+ </Wrapper>,
107
+ );
108
+
81
109
  await expectNoSpinner('Loading course...');
82
110
  const link = screen.getByRole('link', {
83
111
  name: intl.formatMessage(messages.syllabusLinkLabel),
@@ -85,25 +113,103 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
85
113
  expect(link).toHaveAttribute('href', `/redirects/courses/${course.code}`);
86
114
  });
87
115
 
88
- it('should display menu items', async () => {
89
- const course: CourseListItem = CourseFactory().one();
90
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/${course.id}/`, course);
91
- nbApiRequest += 1; // call to course
116
+ it.each([
117
+ {
118
+ label: 'course',
119
+ course: CourseFactory().one(),
120
+ organization: undefined,
121
+ courseProductRelation: undefined,
122
+ expectedRoutes: [TeacherDashboardPaths.COURSE_GENERAL_INFORMATION],
123
+ },
124
+ {
125
+ label: 'training',
126
+ course: CourseFactory().one(),
127
+ organization: undefined,
128
+ courseProductRelation: CourseProductRelationFactory().one(),
129
+ expectedRoutes: [
130
+ TeacherDashboardPaths.COURSE_PRODUCT,
131
+ TeacherDashboardPaths.COURSE_CONTRACTS,
132
+ ],
133
+ },
134
+ {
135
+ label: "organization's course",
136
+ course: CourseFactory().one(),
137
+ organization: OrganizationFactory().one(),
138
+ courseProductRelation: undefined,
139
+ expectedRoutes: [TeacherDashboardPaths.ORGANIZATION_COURSE_GENERAL_INFORMATION],
140
+ },
141
+ {
142
+ label: "organization's training",
143
+ course: CourseFactory().one(),
144
+ organization: OrganizationFactory().one(),
145
+ courseProductRelation: CourseProductRelationFactory().one(),
146
+ expectedRoutes: [
147
+ TeacherDashboardPaths.ORGANIZATION_PRODUCT,
148
+ TeacherDashboardPaths.ORGANIZATION_PRODUCT_CONTRACTS,
149
+ ],
150
+ },
151
+ ])(
152
+ 'should display menu items for "$label" route',
153
+ async ({ course, organization, courseProductRelation, expectedRoutes }) => {
154
+ // mock api for organization's training
155
+ if (organization && courseProductRelation) {
156
+ // fetching training's contracts
157
+ nbApiRequest += 1;
158
+ fetchMock.get(
159
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=25`,
160
+ [],
161
+ );
162
+ // fetching organization's training
163
+ nbApiRequest += 1;
164
+ fetchMock.get(
165
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/${courseProductRelation.id}/`,
166
+ courseProductRelation,
167
+ );
168
+ } else if (organization) {
169
+ // fetching organization's course
170
+ nbApiRequest += 1;
171
+ fetchMock.get(
172
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/courses/${course.id}/`,
173
+ course,
174
+ );
175
+ } else if (courseProductRelation) {
176
+ // fetching training
177
+ nbApiRequest += 1;
178
+ fetchMock.get(
179
+ `https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
180
+ courseProductRelation,
181
+ );
182
+ } else {
183
+ // mock api for course
184
+ nbApiRequest += 1;
185
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/${course.id}/`, course);
186
+ }
92
187
 
93
- renderTeacherDashboardCourseSidebar({ courseId: course.id });
94
- await expectNoSpinner('Loading course...');
95
- expect(
96
- screen.getByRole('link', {
97
- name: intl.formatMessage(
98
- TEACHER_DASHBOARD_ROUTE_LABELS[TeacherDashboardPaths.COURSE_GENERAL_INFORMATION],
99
- ),
100
- }),
101
- ).toBeInTheDocument();
102
-
103
- expect(screen.queryByTestId('organization-links')).not.toBeInTheDocument();
104
- // general informations
105
- // go to syllabus
106
- expect(screen.getAllByRole('link')).toHaveLength(2);
107
- expect(fetchMock.calls()).toHaveLength(nbApiRequest);
108
- });
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
+ );
197
+
198
+ await expectNoSpinner('Loading course...');
199
+ expectedRoutes.forEach((expectedRoute) => {
200
+ expect(
201
+ screen.getByRole('link', {
202
+ name: intl.formatMessage(TEACHER_DASHBOARD_ROUTE_LABELS[expectedRoute]),
203
+ }),
204
+ ).toBeInTheDocument();
205
+ });
206
+
207
+ expect(screen.queryByTestId('organization-links')).not.toBeInTheDocument();
208
+
209
+ let nbExpectedLinks = expectedRoutes.length;
210
+ nbExpectedLinks += 1; // link to syllabus
211
+ expect(screen.getAllByRole('link')).toHaveLength(nbExpectedLinks);
212
+ expect(fetchMock.calls()).toHaveLength(nbApiRequest);
213
+ },
214
+ );
109
215
  });
@@ -2,7 +2,7 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
2
2
  import { useParams } from 'react-router-dom';
3
3
  import { useMemo } from 'react';
4
4
  import { capitalize } from 'lodash-es';
5
- import { DashboardSidebar } from 'widgets/Dashboard/components/DashboardSidebar';
5
+ import { DashboardSidebar, MenuLink } from 'widgets/Dashboard/components/DashboardSidebar';
6
6
  import {
7
7
  getDashboardRouteLabel,
8
8
  getDashboardRoutePath,
@@ -11,6 +11,8 @@ import { useCourse } from 'hooks/useCourses';
11
11
  import { Spinner } from 'components/Spinner';
12
12
  import { Icon, IconTypeEnum } from 'components/Icon';
13
13
  import { useCourseProductRelation } from 'hooks/useCourseProductRelation';
14
+ import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherRouteMessages';
15
+ import ContractNavLink from '../DashboardSidebar/components/ContractNavLink';
14
16
  import { getMenuRoutes } from './utils';
15
17
 
16
18
  export const messages = defineMessages({
@@ -41,23 +43,35 @@ export const TeacherDashboardCourseSidebar = () => {
41
43
  const getRoutePath = getDashboardRoutePath(intl);
42
44
  const getRouteLabel = getDashboardRouteLabel(intl);
43
45
  const {
44
- courseId,
45
- courseProductRelationId = '',
46
- organizationId,
46
+ organizationId: routeOrganizationId,
47
+ courseId: routeCourseId,
48
+ courseProductRelationId: routeCourseProductRelationId = '',
47
49
  } = useParams<{
50
+ organizationId?: string;
48
51
  courseId: string;
49
52
  courseProductRelationId: string;
50
- organizationId?: string;
51
53
  }>();
52
54
 
53
55
  const {
54
56
  item: singleCourse,
55
57
  states: { fetching: courseFetching },
56
- } = useCourse(courseId, { organization_id: organizationId });
58
+ } = useCourse(
59
+ routeCourseId,
60
+ { organization_id: routeOrganizationId },
61
+ { enabled: !routeCourseProductRelationId },
62
+ );
63
+
57
64
  const {
58
65
  item: courseProductRelation,
59
66
  states: { fetching: courseProductRelationFetching },
60
- } = useCourseProductRelation(courseProductRelationId, { organization_id: organizationId });
67
+ } = useCourseProductRelation(
68
+ routeCourseProductRelationId,
69
+ {
70
+ organization_id: routeOrganizationId,
71
+ },
72
+ { enabled: !!routeCourseProductRelationId },
73
+ );
74
+
61
75
  const fetching = useMemo(
62
76
  () => courseFetching || courseProductRelationFetching,
63
77
  [courseFetching, courseProductRelationFetching],
@@ -71,14 +85,48 @@ export const TeacherDashboardCourseSidebar = () => {
71
85
  [courseProductRelation, singleCourse],
72
86
  );
73
87
 
74
- const menuLinks = getMenuRoutes({ courseProductRelationId, organizationId }).map((path) => ({
75
- to: getRoutePath(path, { courseId, courseProductRelationId, organizationId }),
76
- label: getRouteLabel(path),
77
- }));
88
+ const getMenuLinkFromPath = (basePath: TeacherDashboardPaths) => {
89
+ const path = getRoutePath(basePath, {
90
+ organizationId: routeOrganizationId,
91
+ courseId: routeCourseId,
92
+ courseProductRelationId: routeCourseProductRelationId,
93
+ });
94
+
95
+ const menuLink: MenuLink = {
96
+ to: path,
97
+ label: getRouteLabel(basePath),
98
+ };
99
+
100
+ if (
101
+ [
102
+ TeacherDashboardPaths.ORGANIZATION_PRODUCT_CONTRACTS,
103
+ TeacherDashboardPaths.COURSE_PRODUCT_CONTRACTS,
104
+ ].includes(basePath)
105
+ ) {
106
+ menuLink.component = (
107
+ <ContractNavLink
108
+ link={menuLink}
109
+ organizationId={routeOrganizationId}
110
+ courseProductRelationId={routeCourseProductRelationId}
111
+ />
112
+ );
113
+ }
114
+
115
+ return menuLink;
116
+ };
117
+
118
+ const menuLinkList = useMemo(
119
+ () =>
120
+ getMenuRoutes({
121
+ courseProductRelationId: routeCourseProductRelationId,
122
+ organizationId: routeOrganizationId,
123
+ }).map(getMenuLinkFromPath),
124
+ [routeOrganizationId, routeCourseProductRelationId],
125
+ );
78
126
 
79
127
  return (
80
128
  <DashboardSidebar
81
- menuLinks={menuLinks}
129
+ menuLinks={menuLinkList}
82
130
  header={
83
131
  course === undefined
84
132
  ? ''
@@ -4,16 +4,20 @@ interface GetMenuRoutesArgs {
4
4
  courseProductRelationId?: string;
5
5
  organizationId?: string;
6
6
  }
7
+
7
8
  export const getMenuRoutes = ({ courseProductRelationId, organizationId }: GetMenuRoutesArgs) => {
8
9
  if (organizationId) {
9
10
  if (courseProductRelationId) {
10
- return [TeacherDashboardPaths.ORGANIZATION_PRODUCT];
11
+ return [
12
+ TeacherDashboardPaths.ORGANIZATION_PRODUCT,
13
+ TeacherDashboardPaths.ORGANIZATION_PRODUCT_CONTRACTS,
14
+ ];
11
15
  }
12
16
  return [TeacherDashboardPaths.ORGANIZATION_COURSE_GENERAL_INFORMATION];
13
17
  }
14
18
 
15
19
  if (courseProductRelationId) {
16
- return [TeacherDashboardPaths.COURSE_PRODUCT];
20
+ return [TeacherDashboardPaths.COURSE_PRODUCT, TeacherDashboardPaths.COURSE_PRODUCT_CONTRACTS];
17
21
  }
18
22
  return [TeacherDashboardPaths.COURSE_GENERAL_INFORMATION];
19
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev69",
3
+ "version": "2.25.0-b2.dev77",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {