richie-education 2.25.0-b2.dev41 → 2.25.0-b2.dev42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/components/Badge/index.tsx +1 -0
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +244 -0
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +47 -0
- package/js/widgets/Dashboard/components/DashboardSidebar/components/MenuNavLink/index.spec.tsx +40 -0
- package/js/widgets/Dashboard/components/DashboardSidebar/components/MenuNavLink/index.tsx +28 -0
- package/js/widgets/Dashboard/components/DashboardSidebar/index.stories.tsx +7 -2
- package/js/widgets/Dashboard/components/DashboardSidebar/index.tsx +10 -25
- package/js/widgets/Dashboard/components/DashboardSidebar/utils.ts +6 -0
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.spec.tsx +4 -2
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +13 -32
- package/package.json +1 -1
package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx
ADDED
|
@@ -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;
|
package/js/widgets/Dashboard/components/DashboardSidebar/components/MenuNavLink/index.spec.tsx
ADDED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
key={link.to}
|
|
60
|
-
|
|
61
|
-
|
|
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}
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 (
|