richie-education 2.25.0-b2.dev41 → 2.25.0-b2.dev43

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.
@@ -6,6 +6,7 @@ type BadgeProps = PropsWithChildren<{
6
6
  }>;
7
7
  const Badge = ({ children, color }: BadgeProps) => (
8
8
  <div
9
+ data-testid="badge"
9
10
  className={classNames('category-badge', {
10
11
  [`category-badge--${color}`]: Boolean(color),
11
12
  })}
@@ -46,7 +46,6 @@ describe('widgets/Search/components/CourseGlimpse', () => {
46
46
  srcset: 'some srcset',
47
47
  },
48
48
  },
49
- nb_seller_organizations: 1,
50
49
  state: {
51
50
  call_to_action: 'enroll now',
52
51
  datetime: '2019-03-14T10:35:47.823Z',
@@ -28,7 +28,6 @@ export interface CourseGlimpseCourse {
28
28
  srcset?: string;
29
29
  }>;
30
30
  };
31
- nb_seller_organizations: number;
32
31
  icon?: Nullable<{
33
32
  title: string;
34
33
  src: string;
@@ -68,11 +67,6 @@ const messages = defineMessages({
68
67
  description: 'Category label text for screen reader users',
69
68
  id: 'components.CourseGlimpse.categoryLabel',
70
69
  },
71
- organizationsTitle: {
72
- defaultMessage: 'Produced by {nbOrganizations} partners',
73
- description: 'Organizations title for multiple organizations',
74
- id: 'components.CourseGlimpse.organizationsTitle',
75
- },
76
70
  });
77
71
 
78
72
  const CourseGlimpseBase = ({ context, course }: CourseGlimpseProps & CommonDataProps) => {
@@ -114,7 +108,7 @@ const CourseGlimpseBase = ({ context, course }: CourseGlimpseProps & CommonDataP
114
108
  <span className="course-glimpse__title-text">{course.title}</span>
115
109
  </CourseLink>
116
110
  </h3>
117
- {course.nb_seller_organizations === 1 && course.organization.image ? (
111
+ {course.organization.image ? (
118
112
  <div className="course-glimpse__organization-logo">
119
113
  {/* alt forced to empty string because the organization name is rendered after */}
120
114
  <img
@@ -131,16 +125,7 @@ const CourseGlimpseBase = ({ context, course }: CourseGlimpseProps & CommonDataP
131
125
  title={intl.formatMessage(messages.organizationIconAlt)}
132
126
  size="small"
133
127
  />
134
- <span className="title">
135
- {course.nb_seller_organizations === 1 ? (
136
- course.organization.title
137
- ) : (
138
- <FormattedMessage
139
- {...messages.organizationsTitle}
140
- values={{ nbOrganizations: course.nb_seller_organizations }}
141
- />
142
- )}
143
- </span>
128
+ <span className="title">{course.organization.title}</span>
144
129
  </div>
145
130
  <div className="course-glimpse__metadata course-glimpse__metadata--code">
146
131
  <Icon
@@ -38,7 +38,6 @@ const getCourseGlimpsePropsFromCourseProductRelation = (
38
38
  title: courseProductRelation.organizations[0].title,
39
39
  image: courseProductRelation.organizations[0].logo || null,
40
40
  },
41
- nb_seller_organizations: courseProductRelation.organizations.length,
42
41
  product_id: courseProductRelation.product.id,
43
42
  course_route: courseRoute,
44
43
  state: courseProductRelation.product.state,
@@ -55,7 +54,6 @@ const getCourseGlimpsePropsFromRichieCourse = (course: RichieCourse): CourseGlim
55
54
  title: course.organization_highlighted,
56
55
  image: course.organization_highlighted_cover_image,
57
56
  },
58
- nb_seller_organizations: course.organizations.length,
59
57
  icon: course.icon,
60
58
  state: course.state,
61
59
  duration: course.duration,
@@ -93,7 +91,6 @@ const getCourseGlimpsePropsFromJoanieCourse = (
93
91
  title: course.organizations[0].title,
94
92
  image: course.organizations[0].logo || null,
95
93
  },
96
- nb_seller_organizations: course.organizations.length,
97
94
  state: course.state,
98
95
  nb_course_runs: course.course_run_ids.length,
99
96
  };
@@ -0,0 +1,244 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { PropsWithChildren } from 'react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { QueryClientProvider } from '@tanstack/react-query';
5
+ import { IntlProvider } from 'react-intl';
6
+ import fetchMock from 'fetch-mock';
7
+ import { faker } from '@faker-js/faker';
8
+ import queryString from 'query-string';
9
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
10
+ import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
11
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
12
+ import { PER_PAGE } from 'settings';
13
+ import { ContractResourceQuery, ContractState } from 'types/Joanie';
14
+ import { ContractFactory } from 'utils/test/factories/joanie';
15
+ import { ContractActions } from 'utils/AbilitiesHelper/types';
16
+ import { MenuLink } from '../..';
17
+ import ContractNavLink from '.';
18
+
19
+ jest.mock('utils/context', () => ({
20
+ __esModule: true,
21
+ default: mockRichieContextFactory({
22
+ authentication: { backend: 'fonzie', endpoint: 'https://demo.endpoint' },
23
+ joanie_backend: { endpoint: 'https://joanie.endpoint' },
24
+ }).one(),
25
+ }));
26
+
27
+ describe('<ContractNavLink />', () => {
28
+ const Wrapper = ({ children }: PropsWithChildren) => (
29
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
30
+ <IntlProvider locale="en">
31
+ <JoanieSessionProvider>
32
+ <MemoryRouter>{children}</MemoryRouter>
33
+ </JoanieSessionProvider>
34
+ </IntlProvider>
35
+ </QueryClientProvider>
36
+ );
37
+
38
+ beforeEach(() => {
39
+ // JoanieSessionProvider queries
40
+ fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
41
+ fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
42
+ fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
43
+ });
44
+
45
+ afterEach(() => {
46
+ fetchMock.restore();
47
+ });
48
+
49
+ it('should render a ContractNavLink with route and label when neither organizationId and courseProductRelationId are given', () => {
50
+ const link: MenuLink = {
51
+ to: '/dummy/url/',
52
+ label: 'My contract navigation link',
53
+ };
54
+
55
+ render(
56
+ <Wrapper>
57
+ <ContractNavLink link={link} />
58
+ </Wrapper>,
59
+ );
60
+
61
+ expect(screen.getByRole('link', { name: 'My contract navigation link' })).toBeInTheDocument();
62
+ expect(screen.queryByTestId('badge')).not.toBeInTheDocument();
63
+ });
64
+
65
+ describe('without sign ability', () => {
66
+ const contractAbilities = { [ContractActions.SIGN]: false };
67
+ it.each([
68
+ {
69
+ organizationId: faker.string.uuid(),
70
+ courseProductRelationId: undefined,
71
+ },
72
+ {
73
+ organizationId: faker.string.uuid(),
74
+ courseProductRelationId: faker.string.uuid(),
75
+ },
76
+ {
77
+ organizationId: undefined,
78
+ courseProductRelationId: faker.string.uuid(),
79
+ },
80
+ {
81
+ organizationId: undefined,
82
+ courseProductRelationId: undefined,
83
+ },
84
+ ])(
85
+ 'should never render Badge for organizationId: $organizationId and courseProductId: $courseProductRelationId',
86
+ async ({ organizationId, courseProductRelationId }) => {
87
+ let contractQueryParams: ContractResourceQuery = {
88
+ signature_state: ContractState.LEARNER_SIGNED,
89
+ page: 1,
90
+ page_size: PER_PAGE.teacherContractList,
91
+ };
92
+ if (courseProductRelationId) {
93
+ contractQueryParams = {
94
+ course_product_relation_id: courseProductRelationId,
95
+ ...contractQueryParams,
96
+ };
97
+ }
98
+
99
+ fetchMock.get(
100
+ `https://joanie.endpoint/api/v1.0/organizations/${organizationId}/contracts/?${queryString.stringify(
101
+ contractQueryParams,
102
+ { sort: false },
103
+ )}`,
104
+ {
105
+ count: 1,
106
+ next: null,
107
+ previous: null,
108
+ results: [ContractFactory({ abilities: contractAbilities }).one()],
109
+ },
110
+ );
111
+ render(
112
+ <Wrapper>
113
+ <ContractNavLink
114
+ link={{
115
+ to: '/dummy/url/',
116
+ label: 'My contract navigation link',
117
+ }}
118
+ organizationId={organizationId}
119
+ courseProductRelationId={courseProductRelationId}
120
+ />
121
+ </Wrapper>,
122
+ );
123
+
124
+ expect(
125
+ screen.getByRole('link', { name: 'My contract navigation link' }),
126
+ ).toBeInTheDocument();
127
+ expect(screen.queryByTestId('badge')).not.toBeInTheDocument();
128
+ },
129
+ );
130
+ });
131
+
132
+ describe('with sign ability', () => {
133
+ const contractAbilities = { [ContractActions.SIGN]: true };
134
+ it.each([
135
+ // with 1 contracts to sign
136
+ {
137
+ organizationId: faker.string.uuid(),
138
+ courseProductRelationId: undefined,
139
+ nbContractsToSign: 1,
140
+ expectedBadgeCount: 1,
141
+ },
142
+ {
143
+ organizationId: faker.string.uuid(),
144
+ courseProductRelationId: faker.string.uuid(),
145
+ nbContractsToSign: 1,
146
+ expectedBadgeCount: 1,
147
+ },
148
+ {
149
+ organizationId: undefined,
150
+ courseProductRelationId: faker.string.uuid(),
151
+ nbContractsToSign: 1,
152
+ expectedBadgeCount: undefined,
153
+ },
154
+ {
155
+ organizationId: undefined,
156
+ courseProductRelationId: undefined,
157
+ nbContractsToSign: 1,
158
+ expectedBadgeCount: undefined,
159
+ },
160
+
161
+ // with 0 contracts to sign
162
+ {
163
+ organizationId: faker.string.uuid(),
164
+ courseProductRelationId: undefined,
165
+ nbContractsToSign: 0,
166
+ expectedBadgeCount: undefined,
167
+ },
168
+ {
169
+ organizationId: faker.string.uuid(),
170
+ courseProductRelationId: faker.string.uuid(),
171
+ nbContractsToSign: 0,
172
+ expectedBadgeCount: undefined,
173
+ },
174
+ {
175
+ organizationId: undefined,
176
+ courseProductRelationId: faker.string.uuid(),
177
+ nbContractsToSign: 0,
178
+ expectedBadgeCount: undefined,
179
+ },
180
+ {
181
+ organizationId: undefined,
182
+ courseProductRelationId: undefined,
183
+ nbContractsToSign: 0,
184
+ expectedBadgeCount: undefined,
185
+ },
186
+ ])(
187
+ 'should render Badge (count: $expectedBadgeCount) for nb contracts to sign: $nbContractsToSign, organizationId: $organizationId and courseProductId: $courseProductRelationId',
188
+ async ({
189
+ nbContractsToSign,
190
+ organizationId,
191
+ courseProductRelationId,
192
+ expectedBadgeCount,
193
+ }) => {
194
+ let contractQueryParams: ContractResourceQuery = {
195
+ signature_state: ContractState.LEARNER_SIGNED,
196
+ page: 1,
197
+ page_size: PER_PAGE.teacherContractList,
198
+ };
199
+ if (courseProductRelationId) {
200
+ contractQueryParams = {
201
+ course_product_relation_id: courseProductRelationId,
202
+ ...contractQueryParams,
203
+ };
204
+ }
205
+
206
+ fetchMock.get(
207
+ `https://joanie.endpoint/api/v1.0/organizations/${organizationId}/contracts/?${queryString.stringify(
208
+ contractQueryParams,
209
+ { sort: false },
210
+ )}`,
211
+ {
212
+ count: nbContractsToSign,
213
+ next: null,
214
+ previous: null,
215
+ results: [ContractFactory({ abilities: contractAbilities }).one()],
216
+ },
217
+ );
218
+ render(
219
+ <Wrapper>
220
+ <ContractNavLink
221
+ link={{
222
+ to: '/dummy/url/',
223
+ label: 'My contract navigation link',
224
+ }}
225
+ organizationId={organizationId}
226
+ courseProductRelationId={courseProductRelationId}
227
+ />
228
+ </Wrapper>,
229
+ );
230
+
231
+ expect(
232
+ screen.getByRole('link', { name: 'My contract navigation link' }),
233
+ ).toBeInTheDocument();
234
+ if (expectedBadgeCount === undefined) {
235
+ expect(screen.queryByTestId('badge')).not.toBeInTheDocument();
236
+ } else {
237
+ const $badge = await screen.findByTestId('badge');
238
+ expect($badge).toBeInTheDocument();
239
+ expect($badge).toHaveTextContent(`${expectedBadgeCount}`);
240
+ }
241
+ },
242
+ );
243
+ });
244
+ });
@@ -0,0 +1,47 @@
1
+ import { createSearchParams } from 'react-router-dom';
2
+ import { useMemo } from 'react';
3
+ import { MenuLink } from 'widgets/Dashboard/components/DashboardSidebar';
4
+ import { ContractState, CourseProductRelation, Organization } from 'types/Joanie';
5
+ import useTeacherPendingContractsCount from 'hooks/useTeacherPendingContractsCount';
6
+ import { ContractActions } from 'utils/AbilitiesHelper/types';
7
+ import useContractAbilities from 'hooks/useContractAbilities';
8
+ import MenuNavLink from '../MenuNavLink';
9
+
10
+ interface ContractNavLinkProps {
11
+ link: MenuLink;
12
+ organizationId?: Organization['id'];
13
+ courseProductRelationId?: CourseProductRelation['id'];
14
+ }
15
+
16
+ const ContractNavLink = ({
17
+ link,
18
+ organizationId,
19
+ courseProductRelationId,
20
+ }: ContractNavLinkProps) => {
21
+ const { contracts: pendingContracts, pendingContractCount } = useTeacherPendingContractsCount({
22
+ organizationId,
23
+ courseProductRelationId,
24
+ });
25
+ const contractAbilities = useContractAbilities(pendingContracts);
26
+ const canSignContracts = contractAbilities.can(ContractActions.SIGN);
27
+ const hasContractsToSign = useMemo(
28
+ () => canSignContracts && pendingContractCount > 0,
29
+ [canSignContracts, pendingContractCount],
30
+ );
31
+ const searchParams = useMemo(() => {
32
+ if (hasContractsToSign) {
33
+ return createSearchParams({ signature_state: ContractState.LEARNER_SIGNED });
34
+ }
35
+
36
+ return createSearchParams({ signature_state: ContractState.SIGNED });
37
+ }, [hasContractsToSign]);
38
+
39
+ return (
40
+ <MenuNavLink
41
+ link={{ ...link, to: `${link.to}?${searchParams.toString()}` }}
42
+ badgeCount={hasContractsToSign ? pendingContractCount : undefined}
43
+ />
44
+ );
45
+ };
46
+
47
+ export default ContractNavLink;
@@ -0,0 +1,40 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { PropsWithChildren } from 'react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import { MenuLink } from '../..';
5
+ import MenuNavLink from '.';
6
+
7
+ describe('<MenuNavLink />', () => {
8
+ const Wrapper = ({ children }: PropsWithChildren) => <MemoryRouter>{children} </MemoryRouter>;
9
+ it('should render a MenuNavLink with route and label', () => {
10
+ const link: MenuLink = {
11
+ to: '/dummy/url/',
12
+ label: 'My navigation link',
13
+ };
14
+
15
+ render(
16
+ <Wrapper>
17
+ <MenuNavLink link={link} />
18
+ </Wrapper>,
19
+ );
20
+
21
+ expect(screen.getByRole('link', { name: 'My navigation link' })).toBeInTheDocument();
22
+ expect(screen.queryByTestId('badge')).not.toBeInTheDocument();
23
+ });
24
+
25
+ it('should render a MenuNavLink with a badge', () => {
26
+ const link: MenuLink = {
27
+ to: '/dummy/url/',
28
+ label: 'My navigation link',
29
+ };
30
+
31
+ render(
32
+ <Wrapper>
33
+ <MenuNavLink link={link} badgeCount={999} />
34
+ </Wrapper>,
35
+ );
36
+
37
+ expect(screen.getByRole('link', { name: 'My navigation link' })).toBeInTheDocument();
38
+ expect(screen.getByText('999')).toBeInTheDocument();
39
+ });
40
+ });
@@ -0,0 +1,28 @@
1
+ import classNames from 'classnames';
2
+ import { NavLink, useLocation } from 'react-router-dom';
3
+ import Badge from 'components/Badge';
4
+ import { MenuLink } from '../..';
5
+ import { isMenuLinkActive } from '../../utils';
6
+
7
+ interface MenuNavLinkProps {
8
+ link: MenuLink;
9
+ badgeCount?: number;
10
+ }
11
+
12
+ const MenuNavLink = ({ link, badgeCount }: MenuNavLinkProps) => {
13
+ const location = useLocation();
14
+ return (
15
+ <li
16
+ className={classNames('dashboard-sidebar__container__nav__item', {
17
+ active: isMenuLinkActive(link.to, location),
18
+ })}
19
+ >
20
+ <NavLink to={link.to} end>
21
+ {link.label}
22
+ </NavLink>
23
+ {badgeCount && <Badge color="primary">{badgeCount}</Badge>}
24
+ </li>
25
+ );
26
+ };
27
+
28
+ export default MenuNavLink;
@@ -2,7 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
2
2
  import { createMemoryRouter, RouterProvider } from 'react-router-dom';
3
3
  import { UserFactory } from 'utils/test/factories/richie';
4
4
  import { StorybookHelper } from 'utils/StorybookHelper';
5
- import Badge from 'components/Badge';
5
+ import MenuNavLink from './components/MenuNavLink';
6
6
  import { DashboardSidebar } from '.';
7
7
 
8
8
  export default {
@@ -19,7 +19,12 @@ export default {
19
19
  {
20
20
  to: '/test/again',
21
21
  label: 'An other menu link',
22
- badge: <Badge color="primary">999</Badge>,
22
+ component: (
23
+ <MenuNavLink
24
+ link={{ to: '/test/again', label: 'An other menu link' }}
25
+ badgeCount={999}
26
+ />
27
+ ),
23
28
  },
24
29
  ]}
25
30
  header="Dashboard story header"
@@ -1,14 +1,13 @@
1
- import { matchPath, NavLink, resolvePath, useLocation } from 'react-router-dom';
2
- import { PropsWithChildren, ReactNode, useCallback } from 'react';
3
- import classNames from 'classnames';
1
+ import { Fragment, PropsWithChildren, ReactNode } from 'react';
4
2
  import { useSession } from 'contexts/SessionContext';
5
3
  import { DashboardAvatar } from 'widgets/Dashboard/components/DashboardAvatar';
6
4
  import NavigationSelect from './components/NavigationSelect';
5
+ import MenuNavLink from './components/MenuNavLink';
7
6
 
8
7
  export interface MenuLink {
9
8
  to: string;
10
9
  label: string;
11
- badge?: ReactNode;
10
+ component?: ReactNode;
12
11
  }
13
12
 
14
13
  export interface DashboardSidebarProps extends PropsWithChildren {
@@ -28,15 +27,7 @@ export const DashboardSidebar = ({
28
27
  avatar,
29
28
  }: DashboardSidebarProps) => {
30
29
  const { user } = useSession();
31
- const location = useLocation();
32
30
  const classes = ['dashboard-sidebar'];
33
- const isActive = useCallback(
34
- (to: string) => {
35
- const path = resolvePath(to).pathname;
36
- return !!matchPath({ path, end: true }, location.pathname);
37
- },
38
- [location],
39
- );
40
31
 
41
32
  return (
42
33
  <aside className={classes.join(' ')} data-testid="dashboard__sidebar">
@@ -54,19 +45,13 @@ export const DashboardSidebar = ({
54
45
  </div>
55
46
 
56
47
  <ul className="dashboard-sidebar__container__nav">
57
- {menuLinks.map((link) => (
58
- <li
59
- key={link.to}
60
- className={classNames('dashboard-sidebar__container__nav__item', {
61
- active: isActive(link.to),
62
- })}
63
- >
64
- <NavLink to={link.to} end>
65
- {link.label}
66
- </NavLink>
67
- {link.badge}
68
- </li>
69
- ))}
48
+ {menuLinks.map((link) =>
49
+ link.component ? (
50
+ <Fragment key={link.to}>{link.component}</Fragment>
51
+ ) : (
52
+ <MenuNavLink link={link} key={link.to} />
53
+ ),
54
+ )}
70
55
  </ul>
71
56
  </div>
72
57
  {children}
@@ -0,0 +1,6 @@
1
+ import { Location, matchPath, resolvePath } from 'react-router-dom';
2
+
3
+ export const isMenuLinkActive = (to: string, location: Location) => {
4
+ const path = resolvePath(to).pathname;
5
+ return !!matchPath({ path, end: true }, location.pathname);
6
+ };
@@ -1,7 +1,7 @@
1
1
  import { faker } from '@faker-js/faker';
2
2
  import { CunninghamProvider } from '@openfun/cunningham-react';
3
3
  import { QueryClientProvider } from '@tanstack/react-query';
4
- import { render, screen } from '@testing-library/react';
4
+ import { render, screen, waitFor } from '@testing-library/react';
5
5
  import fetchMock from 'fetch-mock';
6
6
  import { IntlProvider } from 'react-intl';
7
7
  import { MemoryRouter, Route, Routes } from 'react-router-dom';
@@ -111,7 +111,9 @@ describe('<TeacherDashboardOrganizationSidebar />', () => {
111
111
 
112
112
  // It should display contract link with badge next to it displaying the number of contracts to sign
113
113
  const contractLink = screen.getByRole('link', { name: 'Contracts' });
114
- expect(contractLink!.nextSibling).toHaveTextContent(contractToSignCount.toString());
114
+ await waitFor(() => {
115
+ expect(contractLink!.nextSibling).toHaveTextContent(contractToSignCount.toString());
116
+ });
115
117
  expect(contractLink).toHaveAttribute(
116
118
  'href',
117
119
  '/teacher/organizations/' + organization.id + '/contracts?signature_state=half_signed',
@@ -1,20 +1,15 @@
1
- import { useMemo } from 'react';
2
1
  import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
- import { createSearchParams, useParams } from 'react-router-dom';
4
- import Badge from 'components/Badge';
2
+ import { useParams } from 'react-router-dom';
5
3
  import { Spinner } from 'components/Spinner';
6
4
  import { useOrganization } from 'hooks/useOrganizations';
7
- import { ContractState } from 'types/Joanie';
8
5
  import { DashboardSidebar, MenuLink } from 'widgets/Dashboard/components/DashboardSidebar';
9
6
  import {
10
7
  getDashboardRouteLabel,
11
8
  getDashboardRoutePath,
12
9
  } from 'widgets/Dashboard/utils/dashboardRoutes';
13
10
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherRouteMessages';
14
- import useTeacherPendingContractsCount from 'hooks/useTeacherPendingContractsCount';
15
- import useContractAbilities from 'hooks/useContractAbilities';
16
- import { ContractActions } from 'utils/AbilitiesHelper/types';
17
11
  import { DashboardAvatar, DashboardAvatarVariantEnum } from '../DashboardAvatar';
12
+ import ContractNavLink from '../DashboardSidebar/components/ContractNavLink';
18
13
 
19
14
  const messages = defineMessages({
20
15
  subHeader: {
@@ -42,13 +37,6 @@ export const TeacherDashboardOrganizationSidebar = () => {
42
37
  states: { fetching },
43
38
  } = useOrganization(organizationId);
44
39
 
45
- const { contracts: pendingContracts, pendingContractCount } = useTeacherPendingContractsCount({
46
- organizationId,
47
- courseProductRelationId,
48
- });
49
- const contractAbilities = useContractAbilities(pendingContracts);
50
- const canSignContracts = contractAbilities.can(ContractActions.SIGN);
51
-
52
40
  const getMenuLinkFromPath = (basePath: TeacherDashboardPaths) => {
53
41
  const path = getRoutePath(basePath, { organizationId });
54
42
 
@@ -57,30 +45,23 @@ export const TeacherDashboardOrganizationSidebar = () => {
57
45
  label: getRouteLabel(basePath),
58
46
  };
59
47
 
60
- // For the contracts link, we want to display the number of contracts if needed and set
61
- // the correct filter depending on the user's abilities
62
48
  if (basePath === TeacherDashboardPaths.ORGANIZATION_CONTRACTS) {
63
- if (canSignContracts && pendingContractCount > 0) {
64
- const searchParams = createSearchParams({ signature_state: ContractState.LEARNER_SIGNED });
65
- menuLink.badge = <Badge color="primary">{pendingContractCount}</Badge>;
66
- menuLink.to = `${path}?${searchParams.toString()}`;
67
- } else {
68
- const searchParams = createSearchParams({ signature_state: ContractState.SIGNED });
69
- menuLink.to = `${path}?${searchParams.toString()}`;
70
- }
49
+ menuLink.component = (
50
+ <ContractNavLink
51
+ link={menuLink}
52
+ organizationId={organizationId}
53
+ courseProductRelationId={courseProductRelationId}
54
+ />
55
+ );
71
56
  }
72
57
 
73
58
  return menuLink;
74
59
  };
75
60
 
76
- const links = useMemo(
77
- () =>
78
- [
79
- TeacherDashboardPaths.ORGANIZATION_COURSES,
80
- TeacherDashboardPaths.ORGANIZATION_CONTRACTS,
81
- ].map(getMenuLinkFromPath),
82
- [pendingContractCount, canSignContracts],
83
- );
61
+ const links = [
62
+ TeacherDashboardPaths.ORGANIZATION_COURSES,
63
+ TeacherDashboardPaths.ORGANIZATION_CONTRACTS,
64
+ ].map(getMenuLinkFromPath);
84
65
 
85
66
  if (fetching) {
86
67
  return (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev41",
3
+ "version": "2.25.0-b2.dev43",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {