richie-education 2.25.0-b2.dev49 → 2.25.0-b2.dev62
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/.nvmrc +1 -1
- package/js/components/PaymentButton/hooks/useTerms.tsx +6 -17
- package/js/components/PaymentButton/index.spec.tsx +27 -39
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +9 -9
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +56 -31
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +4 -3
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId/index.spec.tsx +134 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId/index.tsx +28 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +6 -2
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +193 -0
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +44 -0
- package/js/types/commonDataProps.ts +3 -0
- package/js/utils/search/getSuggestionsSection/index.spec.ts +1 -1
- package/js/utils/test/factories/richie.ts +3 -0
- package/js/widgets/Dashboard/components/DashboardItem/DashboardItemCourseEnrolling.spec.tsx +111 -48
- package/js/widgets/Dashboard/components/DashboardItem/DashboardItemCourseEnrolling.tsx +6 -6
- package/package.json +2 -2
- package/scss/objects/_organization_glimpses.scss +2 -8
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters.tsx +0 -28
package/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20.11.0
|
|
@@ -3,7 +3,7 @@ import { useState } from 'react';
|
|
|
3
3
|
import { defineMessages, useIntl } from 'react-intl';
|
|
4
4
|
import { PaymentErrorMessageId } from 'components/PaymentButton/index';
|
|
5
5
|
import { Product } from 'types/Joanie';
|
|
6
|
-
import
|
|
6
|
+
import context from 'utils/context';
|
|
7
7
|
|
|
8
8
|
const messages = defineMessages({
|
|
9
9
|
termsMessage: {
|
|
@@ -33,7 +33,6 @@ export const useTerms = ({
|
|
|
33
33
|
error?: PaymentErrorMessageId;
|
|
34
34
|
}) => {
|
|
35
35
|
const intl = useIntl();
|
|
36
|
-
const api = useJoanieApi();
|
|
37
36
|
const [termsAccepted, setTermsAccepted] = useState(false);
|
|
38
37
|
const validateTerms = () => {
|
|
39
38
|
if (!product.contract_definition) {
|
|
@@ -44,18 +43,6 @@ export const useTerms = ({
|
|
|
44
43
|
}
|
|
45
44
|
};
|
|
46
45
|
|
|
47
|
-
const openContract = async (e: React.MouseEvent) => {
|
|
48
|
-
if (!product.contract_definition) {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
e.stopPropagation();
|
|
52
|
-
e.preventDefault();
|
|
53
|
-
const blob = await api.contractDefinitions.previewTemplate(product.contract_definition.id);
|
|
54
|
-
// eslint-disable-next-line compat/compat
|
|
55
|
-
const file = window.URL.createObjectURL(blob);
|
|
56
|
-
window.open(file);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
46
|
return {
|
|
60
47
|
termsAccepted,
|
|
61
48
|
validateTerms,
|
|
@@ -66,12 +53,14 @@ export const useTerms = ({
|
|
|
66
53
|
label={
|
|
67
54
|
<>
|
|
68
55
|
{intl.formatMessage(messages.termsMessage)}{' '}
|
|
69
|
-
<
|
|
70
|
-
|
|
56
|
+
<a
|
|
57
|
+
href={context.site_urls.terms_and_conditions ?? '#'}
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
71
60
|
title={intl.formatMessage(messages.termsMessageLinkTitle)}
|
|
72
61
|
>
|
|
73
62
|
{intl.formatMessage(messages.termsMessageLink)}
|
|
74
|
-
</
|
|
63
|
+
</a>
|
|
75
64
|
</>
|
|
76
65
|
}
|
|
77
66
|
onChange={(e) => setTermsAccepted(e.target.checked)}
|
|
@@ -54,6 +54,9 @@ jest.mock('utils/context', () => ({
|
|
|
54
54
|
joanie_backend: {
|
|
55
55
|
endpoint: 'https://joanie.test',
|
|
56
56
|
},
|
|
57
|
+
site_urls: {
|
|
58
|
+
terms_and_conditions: '/en/about/terms-and-conditions/',
|
|
59
|
+
},
|
|
57
60
|
}).one(),
|
|
58
61
|
}));
|
|
59
62
|
|
|
@@ -177,7 +180,9 @@ describe.each([
|
|
|
177
180
|
</Wrapper>,
|
|
178
181
|
);
|
|
179
182
|
|
|
180
|
-
const $terms = screen.getByLabelText(
|
|
183
|
+
const $terms = screen.getByLabelText(
|
|
184
|
+
'By checking this box, you accept the General Terms of Sale',
|
|
185
|
+
);
|
|
181
186
|
await act(async () => {
|
|
182
187
|
fireEvent.click($terms);
|
|
183
188
|
});
|
|
@@ -330,7 +335,9 @@ describe.each([
|
|
|
330
335
|
nbApiCalls += 1; // fetch order for useProductOrder
|
|
331
336
|
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
332
337
|
|
|
333
|
-
const $terms = screen.getByLabelText(
|
|
338
|
+
const $terms = screen.getByLabelText(
|
|
339
|
+
'By checking this box, you accept the General Terms of Sale',
|
|
340
|
+
);
|
|
334
341
|
await act(async () => {
|
|
335
342
|
fireEvent.click($terms);
|
|
336
343
|
});
|
|
@@ -455,7 +462,9 @@ describe.each([
|
|
|
455
462
|
expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
|
|
456
463
|
});
|
|
457
464
|
|
|
458
|
-
const $terms = screen.getByLabelText(
|
|
465
|
+
const $terms = screen.getByLabelText(
|
|
466
|
+
'By checking this box, you accept the General Terms of Sale',
|
|
467
|
+
);
|
|
459
468
|
await act(async () => {
|
|
460
469
|
fireEvent.click($terms);
|
|
461
470
|
});
|
|
@@ -590,7 +599,9 @@ describe.each([
|
|
|
590
599
|
`https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
|
|
591
600
|
);
|
|
592
601
|
|
|
593
|
-
const $terms = screen.getByLabelText(
|
|
602
|
+
const $terms = screen.getByLabelText(
|
|
603
|
+
'By checking this box, you accept the General Terms of Sale',
|
|
604
|
+
);
|
|
594
605
|
await act(async () => {
|
|
595
606
|
fireEvent.click($terms);
|
|
596
607
|
});
|
|
@@ -703,7 +714,9 @@ describe.each([
|
|
|
703
714
|
nbApiCalls += 1; // useProductOrder get order with filters
|
|
704
715
|
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
705
716
|
|
|
706
|
-
const $terms = screen.getByLabelText(
|
|
717
|
+
const $terms = screen.getByLabelText(
|
|
718
|
+
'By checking this box, you accept the General Terms of Sale',
|
|
719
|
+
);
|
|
707
720
|
await act(async () => {
|
|
708
721
|
fireEvent.click($terms);
|
|
709
722
|
});
|
|
@@ -802,17 +815,9 @@ describe.each([
|
|
|
802
815
|
});
|
|
803
816
|
|
|
804
817
|
it('should be able to preview the contract if product has a contract definition', async () => {
|
|
805
|
-
// eslint-disable-next-line compat/compat
|
|
806
|
-
URL.createObjectURL = jest.fn((blob) => blob) as any;
|
|
807
|
-
window.open = jest.fn();
|
|
808
|
-
|
|
809
818
|
const product: Joanie.Product = ProductFactory().one();
|
|
810
819
|
const billingAddress: Joanie.Address = AddressFactory().one();
|
|
811
820
|
|
|
812
|
-
const PREVIEW_URL = `https://joanie.test/api/v1.0/contract_definitions/${
|
|
813
|
-
product.contract_definition!.id
|
|
814
|
-
}/preview_template/`;
|
|
815
|
-
|
|
816
821
|
const fetchOrderQueryParams =
|
|
817
822
|
product.type === ProductType.CREDENTIAL
|
|
818
823
|
? {
|
|
@@ -826,12 +831,10 @@ describe.each([
|
|
|
826
831
|
state: ['pending', 'validated', 'submitted'],
|
|
827
832
|
};
|
|
828
833
|
|
|
829
|
-
fetchMock
|
|
830
|
-
.
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
)
|
|
834
|
-
.get(PREVIEW_URL, 'preview content');
|
|
834
|
+
fetchMock.get(
|
|
835
|
+
`https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
|
|
836
|
+
[],
|
|
837
|
+
);
|
|
835
838
|
|
|
836
839
|
render(
|
|
837
840
|
<Wrapper client={createTestQueryClient({ user: true })} product={product}>
|
|
@@ -839,25 +842,8 @@ describe.each([
|
|
|
839
842
|
</Wrapper>,
|
|
840
843
|
);
|
|
841
844
|
|
|
842
|
-
const $terms = screen.getByRole('
|
|
843
|
-
|
|
844
|
-
// eslint-disable-next-line compat/compat
|
|
845
|
-
expect(URL.createObjectURL).toHaveBeenCalledTimes(0);
|
|
846
|
-
expect(window.open).toHaveBeenCalledTimes(0);
|
|
847
|
-
expect(fetchMock.called(PREVIEW_URL)).toBe(false);
|
|
848
|
-
|
|
849
|
-
// console.log($terms);
|
|
850
|
-
await act(async () => {
|
|
851
|
-
fireEvent.click($terms);
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
expect(fetchMock.called(PREVIEW_URL)).toBe(true);
|
|
855
|
-
// eslint-disable-next-line compat/compat
|
|
856
|
-
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
|
|
857
|
-
// eslint-disable-next-line compat/compat
|
|
858
|
-
expect(URL.createObjectURL).toHaveBeenCalledWith('preview content');
|
|
859
|
-
expect(window.open).toHaveBeenCalledTimes(1);
|
|
860
|
-
expect(window.open).toHaveBeenCalledWith('preview content');
|
|
845
|
+
const $terms = screen.getByRole('link', { name: 'General Terms of Sale' });
|
|
846
|
+
expect($terms).toHaveAttribute('href', '/en/about/terms-and-conditions/');
|
|
861
847
|
});
|
|
862
848
|
|
|
863
849
|
it('should not show terms checkbox if the product does not have a contract definition', async () => {
|
|
@@ -959,7 +945,9 @@ describe.each([
|
|
|
959
945
|
name: `Pay ${formatPrice(product.price, product.price_currency)}`,
|
|
960
946
|
}) as HTMLButtonElement;
|
|
961
947
|
|
|
962
|
-
const $terms = screen.getByLabelText(
|
|
948
|
+
const $terms = screen.getByLabelText(
|
|
949
|
+
'By checking this box, you accept the General Terms of Sale',
|
|
950
|
+
);
|
|
963
951
|
await act(async () => {
|
|
964
952
|
fireEvent.click($terms);
|
|
965
953
|
});
|
|
@@ -12,7 +12,7 @@ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
|
12
12
|
import { ContractFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
13
13
|
import { expectNoSpinner } from 'utils/test/expectSpinner';
|
|
14
14
|
import { expectBannerError } from 'utils/test/expectBanner';
|
|
15
|
-
import {
|
|
15
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
16
16
|
import TeacherDashboardContracts from '.';
|
|
17
17
|
|
|
18
18
|
jest.mock('utils/context', () => ({
|
|
@@ -82,12 +82,12 @@ describe('pages/TeacherDashboardContracts', () => {
|
|
|
82
82
|
fetchMock.get(`https://joanie.test/api/v1.0/organizations/`, [organization]);
|
|
83
83
|
// TeacherDashboardContracts request a paginated list of contracts to display
|
|
84
84
|
fetchMock.get(
|
|
85
|
-
`https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?signature_state=signed&
|
|
85
|
+
`https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=2&signature_state=signed&page=1&page_size=25`,
|
|
86
86
|
{ results: contracts, count: 0, previous: null, next: null },
|
|
87
87
|
);
|
|
88
88
|
// useTeacherContractsToSign request all contract to sign, without pagination
|
|
89
89
|
fetchMock.get(
|
|
90
|
-
`https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?signature_state=half_signed
|
|
90
|
+
`https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=2&signature_state=half_signed`,
|
|
91
91
|
{ results: [], count: 0, previous: null, next: null },
|
|
92
92
|
);
|
|
93
93
|
|
|
@@ -244,6 +244,10 @@ describe('pages/TeacherDashboardContracts', () => {
|
|
|
244
244
|
abilities: { sign: true },
|
|
245
245
|
}).many(3);
|
|
246
246
|
|
|
247
|
+
fetchMock.get(
|
|
248
|
+
`https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
|
|
249
|
+
{ results: [], count: 0, previous: null, next: null },
|
|
250
|
+
);
|
|
247
251
|
fetchMock.get(
|
|
248
252
|
`https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=half_signed&page=1&page_size=25`,
|
|
249
253
|
{ results: contracts, count: 3, previous: null, next: null },
|
|
@@ -309,15 +313,11 @@ describe('pages/TeacherDashboardContracts', () => {
|
|
|
309
313
|
it('should render an error banner if an error occured during contracts fetching', async () => {
|
|
310
314
|
fetchMock.get(
|
|
311
315
|
`https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
|
|
312
|
-
|
|
313
|
-
throw new HttpError(404, 'Not found');
|
|
314
|
-
},
|
|
316
|
+
HttpStatusCode.NOT_FOUND,
|
|
315
317
|
);
|
|
316
318
|
fetchMock.get(
|
|
317
319
|
`https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=half_signed`,
|
|
318
|
-
|
|
319
|
-
throw new HttpError(404, 'Not found');
|
|
320
|
-
},
|
|
320
|
+
HttpStatusCode.NOT_FOUND,
|
|
321
321
|
);
|
|
322
322
|
|
|
323
323
|
render(
|
package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx
CHANGED
|
@@ -87,10 +87,7 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
|
|
|
87
87
|
|
|
88
88
|
render(
|
|
89
89
|
<Wrapper>
|
|
90
|
-
<ContractActionsBar
|
|
91
|
-
courseProductRelationId={faker.string.uuid()}
|
|
92
|
-
organizationId={faker.string.uuid()}
|
|
93
|
-
/>
|
|
90
|
+
<ContractActionsBar organizationId={faker.string.uuid()} />
|
|
94
91
|
</Wrapper>,
|
|
95
92
|
);
|
|
96
93
|
|
|
@@ -99,26 +96,47 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
|
|
|
99
96
|
expect(screen.getByRole('button', { name: /Request contracts archive/ })).toBeInTheDocument();
|
|
100
97
|
});
|
|
101
98
|
|
|
102
|
-
it(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
99
|
+
it.each([
|
|
100
|
+
{
|
|
101
|
+
label: "doesn't have contract to download",
|
|
102
|
+
hasContractToDownload: false,
|
|
103
|
+
courseProductRelationId: undefined,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
label: 'has contract to download and courseProductRelationId',
|
|
107
|
+
hasContractToDownload: true,
|
|
108
|
+
courseProductRelationId: faker.string.uuid(),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: "doesn't have contract to download and courseProductRelationId",
|
|
112
|
+
hasContractToDownload: false,
|
|
113
|
+
courseProductRelationId: faker.string.uuid(),
|
|
114
|
+
},
|
|
115
|
+
])(
|
|
116
|
+
"shouldn't only display sign button when $label",
|
|
117
|
+
({ hasContractToDownload, courseProductRelationId }) => {
|
|
118
|
+
mockHasContractToDownload = hasContractToDownload;
|
|
119
|
+
mockCanSignContracts = true;
|
|
120
|
+
mockContractsToSignCount = 1;
|
|
121
|
+
|
|
122
|
+
render(
|
|
123
|
+
<Wrapper>
|
|
124
|
+
<ContractActionsBar
|
|
125
|
+
courseProductRelationId={courseProductRelationId}
|
|
126
|
+
organizationId={faker.string.uuid()}
|
|
127
|
+
/>
|
|
128
|
+
</Wrapper>,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
|
|
132
|
+
expect(
|
|
133
|
+
screen.getByRole('button', { name: /Sign all pending contracts/ }),
|
|
134
|
+
).toBeInTheDocument();
|
|
135
|
+
expect(
|
|
136
|
+
screen.queryByRole('button', { name: /Request contracts archive/ }),
|
|
137
|
+
).not.toBeInTheDocument();
|
|
138
|
+
},
|
|
139
|
+
);
|
|
122
140
|
|
|
123
141
|
it("shouldn't only display download button", () => {
|
|
124
142
|
mockHasContractToDownload = true;
|
|
@@ -127,10 +145,7 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
|
|
|
127
145
|
|
|
128
146
|
render(
|
|
129
147
|
<Wrapper>
|
|
130
|
-
<ContractActionsBar
|
|
131
|
-
courseProductRelationId={faker.string.uuid()}
|
|
132
|
-
organizationId={faker.string.uuid()}
|
|
133
|
-
/>
|
|
148
|
+
<ContractActionsBar organizationId={faker.string.uuid()} />
|
|
134
149
|
</Wrapper>,
|
|
135
150
|
);
|
|
136
151
|
|
|
@@ -141,15 +156,25 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
|
|
|
141
156
|
).not.toBeInTheDocument();
|
|
142
157
|
});
|
|
143
158
|
|
|
144
|
-
it(
|
|
145
|
-
|
|
159
|
+
it.each([
|
|
160
|
+
{
|
|
161
|
+
label: 'only download is available but we have a courseProductRelationId',
|
|
162
|
+
hasContractToDownload: true,
|
|
163
|
+
courseProductRelationId: faker.string.uuid(),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
label: 'no actions are available',
|
|
167
|
+
hasContractToDownload: false,
|
|
168
|
+
},
|
|
169
|
+
])('should return nothing when $label', ({ hasContractToDownload, courseProductRelationId }) => {
|
|
170
|
+
mockHasContractToDownload = hasContractToDownload;
|
|
146
171
|
mockCanSignContracts = false;
|
|
147
172
|
mockContractsToSignCount = 0;
|
|
148
173
|
|
|
149
174
|
render(
|
|
150
175
|
<Wrapper>
|
|
151
176
|
<ContractActionsBar
|
|
152
|
-
courseProductRelationId={
|
|
177
|
+
courseProductRelationId={courseProductRelationId}
|
|
153
178
|
organizationId={faker.string.uuid()}
|
|
154
179
|
/>
|
|
155
180
|
</Wrapper>,
|
|
@@ -15,9 +15,10 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
|
|
|
15
15
|
organizationId,
|
|
16
16
|
courseProductRelationId,
|
|
17
17
|
});
|
|
18
|
-
const hasContractToDownload = useHasContractToDownload(organizationId);
|
|
18
|
+
const hasContractToDownload = useHasContractToDownload(organizationId, courseProductRelationId);
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const canDownloadContracts = hasContractToDownload && !courseProductRelationId;
|
|
21
|
+
const nbAvailableActions = [canSignContracts, canDownloadContracts].filter((val) => val).length;
|
|
21
22
|
return (
|
|
22
23
|
nbAvailableActions > 0 && (
|
|
23
24
|
<div
|
|
@@ -35,7 +36,7 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
|
|
|
35
36
|
/>
|
|
36
37
|
</div>
|
|
37
38
|
)}
|
|
38
|
-
{
|
|
39
|
+
{canDownloadContracts && <BulkDownloadContractButton organizationId={organizationId} />}
|
|
39
40
|
</div>
|
|
40
41
|
)
|
|
41
42
|
);
|
package/js/pages/TeacherDashboardContractsLayout/hooks/useDefaultOrganizationId/index.spec.tsx
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
2
|
+
import { PropsWithChildren } from 'react';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
4
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
5
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
6
|
+
import fetchMock from 'fetch-mock';
|
|
7
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
8
|
+
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
9
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
10
|
+
import { OrganizationFactory } from 'utils/test/factories/joanie';
|
|
11
|
+
import { Organization } from 'types/Joanie';
|
|
12
|
+
import useDefaultOrganizationId from '.';
|
|
13
|
+
|
|
14
|
+
jest.mock('utils/context', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: mockRichieContextFactory({
|
|
17
|
+
authentication: { backend: 'fonzie', endpoint: 'https://demo.test' },
|
|
18
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
19
|
+
}).one(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
interface WrapperProps {
|
|
23
|
+
routePath: string;
|
|
24
|
+
initialEntry: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('useDefaultOrganizationId', () => {
|
|
28
|
+
const organizations: {
|
|
29
|
+
routeOrganization: Organization;
|
|
30
|
+
queryOrganization: Organization;
|
|
31
|
+
userOrganizationList: Organization[];
|
|
32
|
+
} = {
|
|
33
|
+
routeOrganization: OrganizationFactory().one(),
|
|
34
|
+
queryOrganization: OrganizationFactory().one(),
|
|
35
|
+
userOrganizationList: OrganizationFactory().many(2),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const Wrapper = ({ children, routePath, initialEntry }: PropsWithChildren<WrapperProps>) => {
|
|
39
|
+
return (
|
|
40
|
+
<IntlProvider locale="en">
|
|
41
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
42
|
+
<JoanieSessionProvider>
|
|
43
|
+
<MemoryRouter initialEntries={[initialEntry]}>
|
|
44
|
+
<Routes>
|
|
45
|
+
<Route path={routePath} element={children} />
|
|
46
|
+
</Routes>
|
|
47
|
+
</MemoryRouter>
|
|
48
|
+
</JoanieSessionProvider>
|
|
49
|
+
</QueryClientProvider>
|
|
50
|
+
</IntlProvider>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
// Joanie provider's calls
|
|
56
|
+
fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
|
|
57
|
+
fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
|
|
58
|
+
fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
fetchMock.restore();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it.each([
|
|
66
|
+
{
|
|
67
|
+
testLabel: 'route organization before query',
|
|
68
|
+
routeOrganization: organizations.routeOrganization,
|
|
69
|
+
queryOrganization: organizations.queryOrganization,
|
|
70
|
+
userOrganizationList: organizations.userOrganizationList,
|
|
71
|
+
expectedOrganizationId: organizations.routeOrganization.id,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
testLabel: 'query organization before first element of list',
|
|
75
|
+
routeOrganization: undefined,
|
|
76
|
+
queryOrganization: organizations.queryOrganization,
|
|
77
|
+
userOrganizationList: organizations.userOrganizationList,
|
|
78
|
+
expectedOrganizationId: organizations.queryOrganization.id,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
testLabel: 'first element of list when nothing else is found',
|
|
82
|
+
routeOrganization: undefined,
|
|
83
|
+
queryOrganization: undefined,
|
|
84
|
+
userOrganizationList: organizations.userOrganizationList,
|
|
85
|
+
expectedOrganizationId: organizations.userOrganizationList[0].id,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
testLabel: 'undefined when user have no organization in his list',
|
|
89
|
+
routeOrganization: undefined,
|
|
90
|
+
queryOrganization: undefined,
|
|
91
|
+
userOrganizationList: [],
|
|
92
|
+
expectedOrganizationId: undefined,
|
|
93
|
+
},
|
|
94
|
+
])(
|
|
95
|
+
'should return $testLabel',
|
|
96
|
+
async ({
|
|
97
|
+
routeOrganization,
|
|
98
|
+
queryOrganization,
|
|
99
|
+
userOrganizationList,
|
|
100
|
+
expectedOrganizationId,
|
|
101
|
+
}) => {
|
|
102
|
+
let routePath = '/';
|
|
103
|
+
if (routeOrganization) {
|
|
104
|
+
routePath += ':organizationId/';
|
|
105
|
+
}
|
|
106
|
+
let initialEntry = '/';
|
|
107
|
+
if (routeOrganization) {
|
|
108
|
+
initialEntry += `${routeOrganization.id}/`;
|
|
109
|
+
}
|
|
110
|
+
if (queryOrganization) {
|
|
111
|
+
initialEntry += `?organization_id=${queryOrganization.id}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fetchMock.get(
|
|
115
|
+
'https://joanie.test/api/v1.0/organizations/',
|
|
116
|
+
[...userOrganizationList, routeOrganization, queryOrganization].filter(
|
|
117
|
+
(organization) => organization !== undefined,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
const { result } = renderHook(useDefaultOrganizationId, {
|
|
121
|
+
wrapper: ({ children }) => (
|
|
122
|
+
<Wrapper routePath={routePath} initialEntry={initialEntry}>
|
|
123
|
+
{children}
|
|
124
|
+
</Wrapper>
|
|
125
|
+
),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// when looking in organization list defaultOrganization will be updated when organizations are fetched.
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(result.current).toBe(expectedOrganizationId);
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
2
|
+
import { useOrganizations } from 'hooks/useOrganizations';
|
|
3
|
+
import { Organization } from 'types/Joanie';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* return organization id with this priority:
|
|
7
|
+
* * route param
|
|
8
|
+
* * query param
|
|
9
|
+
* * first organization of user's organizations
|
|
10
|
+
*/
|
|
11
|
+
const useDefaultOrganizationId = () => {
|
|
12
|
+
const { organizationId: routeOrganizationId } = useParams<{
|
|
13
|
+
organizationId?: Organization['id'];
|
|
14
|
+
}>();
|
|
15
|
+
const [searchParams] = useSearchParams();
|
|
16
|
+
const queryOrganizationId = searchParams.get('organization_id') || undefined;
|
|
17
|
+
const { items: organizations } = useOrganizations(undefined, {
|
|
18
|
+
enabled: !routeOrganizationId && !queryOrganizationId,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
routeOrganizationId ||
|
|
23
|
+
queryOrganizationId ||
|
|
24
|
+
(organizations.length > 0 ? organizations[0].id : undefined)
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default useDefaultOrganizationId;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { useOrganizationContracts } from 'hooks/useContracts';
|
|
2
2
|
import { PER_PAGE } from 'settings';
|
|
3
|
-
import { ContractState, Organization } from 'types/Joanie';
|
|
3
|
+
import { ContractState, CourseProductRelation, Organization } from 'types/Joanie';
|
|
4
4
|
|
|
5
|
-
const useHasContractToDownload = (
|
|
5
|
+
const useHasContractToDownload = (
|
|
6
|
+
organizationId: Organization['id'],
|
|
7
|
+
courseProductRelationId?: CourseProductRelation['id'],
|
|
8
|
+
) => {
|
|
6
9
|
const {
|
|
7
10
|
items: contracts,
|
|
8
11
|
states: { isFetched },
|
|
9
12
|
} = useOrganizationContracts({
|
|
10
13
|
organization_id: organizationId,
|
|
14
|
+
course_product_relation_id: courseProductRelationId,
|
|
11
15
|
signature_state: ContractState.SIGNED,
|
|
12
16
|
page: 1,
|
|
13
17
|
page_size: PER_PAGE.teacherContractList,
|
package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import fetchMock from 'fetch-mock';
|
|
2
|
+
import { PropsWithChildren } from 'react';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
4
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
6
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
7
|
+
import { act } from 'react-dom/test-utils';
|
|
8
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
9
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
10
|
+
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
11
|
+
import { CourseProductRelationFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
12
|
+
import { ContractState } from 'types/Joanie';
|
|
13
|
+
import useTeacherContractFilters from '.';
|
|
14
|
+
|
|
15
|
+
jest.mock('utils/context', () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
default: mockRichieContextFactory({
|
|
18
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
19
|
+
joanie_backend: { endpoint: 'https://joanie.test' },
|
|
20
|
+
}).one(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
interface WrapperProps {
|
|
24
|
+
routePath: string;
|
|
25
|
+
initialEntry: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('useTeacherContractFilters', () => {
|
|
29
|
+
const Wrapper = ({ children, routePath, initialEntry }: PropsWithChildren<WrapperProps>) => {
|
|
30
|
+
return (
|
|
31
|
+
<IntlProvider locale="en">
|
|
32
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
33
|
+
<JoanieSessionProvider>
|
|
34
|
+
<MemoryRouter initialEntries={[initialEntry]}>
|
|
35
|
+
<Routes>
|
|
36
|
+
<Route path={routePath} element={children} />
|
|
37
|
+
</Routes>
|
|
38
|
+
</MemoryRouter>
|
|
39
|
+
</JoanieSessionProvider>
|
|
40
|
+
</QueryClientProvider>
|
|
41
|
+
</IntlProvider>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
// Joanie provider's calls
|
|
46
|
+
fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
|
|
47
|
+
fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
|
|
48
|
+
fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
fetchMock.restore();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return default filter when called in a route without parameters', async () => {
|
|
56
|
+
const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
|
|
57
|
+
// fetching user's organizations to initialize default organizationId.
|
|
58
|
+
fetchMock.get('https://joanie.test/api/v1.0/organizations/', [defaultOrganization]);
|
|
59
|
+
const { result } = renderHook(useTeacherContractFilters, {
|
|
60
|
+
wrapper: ({ children }) => (
|
|
61
|
+
<Wrapper routePath="/" initialEntry="/">
|
|
62
|
+
{children}
|
|
63
|
+
</Wrapper>
|
|
64
|
+
),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
expect(result.current.initialFilters).toStrictEqual({
|
|
69
|
+
contract_ids: [],
|
|
70
|
+
organization_id: defaultOrganization.id,
|
|
71
|
+
course_product_relation_id: undefined,
|
|
72
|
+
signature_state: ContractState.SIGNED,
|
|
73
|
+
});
|
|
74
|
+
expect(result.current.filters).toStrictEqual({
|
|
75
|
+
contract_ids: [],
|
|
76
|
+
organization_id: defaultOrganization.id,
|
|
77
|
+
course_product_relation_id: undefined,
|
|
78
|
+
signature_state: ContractState.SIGNED,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should use route parameters values when given', async () => {
|
|
84
|
+
const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
|
|
85
|
+
const filteredOrganization = OrganizationFactory({ id: 'filtered' }).one();
|
|
86
|
+
const routeOrganization = OrganizationFactory({ id: 'route' }).one();
|
|
87
|
+
const routeCourseProductRelation = CourseProductRelationFactory().one();
|
|
88
|
+
// fetching user's organizations to initialize default organizationId.
|
|
89
|
+
fetchMock.get('https://joanie.test/api/v1.0/organizations/', [
|
|
90
|
+
defaultOrganization,
|
|
91
|
+
filteredOrganization,
|
|
92
|
+
]);
|
|
93
|
+
const { result } = renderHook(useTeacherContractFilters, {
|
|
94
|
+
wrapper: ({ children }) => (
|
|
95
|
+
<Wrapper
|
|
96
|
+
routePath="/:organizationId/:courseProductRelationId"
|
|
97
|
+
initialEntry={`/${routeOrganization.id}/${routeCourseProductRelation.id}?organization_id=${filteredOrganization.id}&signature_state=${ContractState?.UNSIGNED}&contract_ids=1&contract_ids=2`}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</Wrapper>
|
|
101
|
+
),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
expect(result.current.initialFilters).toStrictEqual({
|
|
106
|
+
contract_ids: ['1', '2'],
|
|
107
|
+
organization_id: routeOrganization.id,
|
|
108
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
109
|
+
signature_state: ContractState.UNSIGNED,
|
|
110
|
+
});
|
|
111
|
+
expect(result.current.filters).toStrictEqual({
|
|
112
|
+
contract_ids: ['1', '2'],
|
|
113
|
+
organization_id: routeOrganization.id,
|
|
114
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
115
|
+
signature_state: ContractState.UNSIGNED,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should use organizationId from query parameters when it's not in route params", async () => {
|
|
121
|
+
const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
|
|
122
|
+
const filteredOrganization = OrganizationFactory({ id: 'filtered' }).one();
|
|
123
|
+
const routeCourseProductRelation = CourseProductRelationFactory({ id: 'route' }).one();
|
|
124
|
+
// fetching user's organizations to initialize default organizationId.
|
|
125
|
+
fetchMock.get('https://joanie.test/api/v1.0/organizations/', [
|
|
126
|
+
defaultOrganization,
|
|
127
|
+
filteredOrganization,
|
|
128
|
+
]);
|
|
129
|
+
const { result } = renderHook(useTeacherContractFilters, {
|
|
130
|
+
wrapper: ({ children }) => (
|
|
131
|
+
<Wrapper
|
|
132
|
+
routePath="/:courseProductRelationId"
|
|
133
|
+
initialEntry={`/${routeCourseProductRelation.id}/?organization_id=${filteredOrganization.id}&signature_state=${ContractState?.UNSIGNED}&contract_ids=1&contract_ids=2`}
|
|
134
|
+
>
|
|
135
|
+
{children}
|
|
136
|
+
</Wrapper>
|
|
137
|
+
),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
expect(result.current.initialFilters).toStrictEqual({
|
|
142
|
+
contract_ids: ['1', '2'],
|
|
143
|
+
organization_id: filteredOrganization.id,
|
|
144
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
145
|
+
signature_state: ContractState.UNSIGNED,
|
|
146
|
+
});
|
|
147
|
+
expect(result.current.filters).toStrictEqual({
|
|
148
|
+
contract_ids: ['1', '2'],
|
|
149
|
+
organization_id: filteredOrganization.id,
|
|
150
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
151
|
+
signature_state: ContractState.UNSIGNED,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('setFilters should update filter state', async () => {
|
|
157
|
+
const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
|
|
158
|
+
const routeOrganization = OrganizationFactory({ id: 'route' }).one();
|
|
159
|
+
const routeCourseProductRelation = CourseProductRelationFactory().one();
|
|
160
|
+
// fetching user's organizations to initialize default organizationId.
|
|
161
|
+
fetchMock.get('https://joanie.test/api/v1.0/organizations/', [defaultOrganization]);
|
|
162
|
+
const { result } = renderHook(useTeacherContractFilters, {
|
|
163
|
+
wrapper: ({ children }) => (
|
|
164
|
+
<Wrapper routePath="/" initialEntry="/">
|
|
165
|
+
{children}
|
|
166
|
+
</Wrapper>
|
|
167
|
+
),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const expectedInitialFilters = {
|
|
171
|
+
contract_ids: [],
|
|
172
|
+
organization_id: defaultOrganization.id,
|
|
173
|
+
course_product_relation_id: undefined,
|
|
174
|
+
signature_state: ContractState.SIGNED,
|
|
175
|
+
};
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(result.current.initialFilters).toStrictEqual(expectedInitialFilters);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const newFilters = {
|
|
181
|
+
contract_ids: ['1', '2'],
|
|
182
|
+
organization_id: routeOrganization.id,
|
|
183
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
184
|
+
signature_state: ContractState.UNSIGNED,
|
|
185
|
+
};
|
|
186
|
+
act(() => {
|
|
187
|
+
result.current.setFilters(newFilters);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(result.current.filters).toStrictEqual(newFilters);
|
|
191
|
+
expect(result.current.initialFilters).toStrictEqual(expectedInitialFilters);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { ContractResourceQuery, ContractState } from 'types/Joanie';
|
|
4
|
+
import useDefaultOrganizationId from '../useDefaultOrganizationId';
|
|
5
|
+
|
|
6
|
+
export type TeacherDashboardContractsParams = {
|
|
7
|
+
organizationId?: string;
|
|
8
|
+
courseProductRelationId?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const useTeacherContractFilters = () => {
|
|
12
|
+
const { courseProductRelationId } = useParams<TeacherDashboardContractsParams>();
|
|
13
|
+
const [searchParams] = useSearchParams();
|
|
14
|
+
const searchFilters: ContractResourceQuery = useMemo(() => {
|
|
15
|
+
return {
|
|
16
|
+
organization_id: searchParams.get('organization_id') || undefined,
|
|
17
|
+
course_product_relation_id: searchParams.get('course_product_relation_id') || undefined,
|
|
18
|
+
contract_ids: searchParams.getAll('contract_ids') || undefined,
|
|
19
|
+
signature_state:
|
|
20
|
+
(searchParams.get('signature_state') as ContractState) || ContractState.SIGNED,
|
|
21
|
+
};
|
|
22
|
+
}, Array.from(searchParams.entries()));
|
|
23
|
+
|
|
24
|
+
// default orgnizationId between (ordered by priority): route, query, first user's organization.
|
|
25
|
+
const defaultOrganizationId = useDefaultOrganizationId();
|
|
26
|
+
|
|
27
|
+
const initialFilters = useMemo(() => {
|
|
28
|
+
return {
|
|
29
|
+
...searchFilters,
|
|
30
|
+
organization_id: defaultOrganizationId,
|
|
31
|
+
course_product_relation_id: courseProductRelationId,
|
|
32
|
+
};
|
|
33
|
+
}, [defaultOrganizationId]);
|
|
34
|
+
const [filters, setFilters] = useState<ContractResourceQuery>(initialFilters);
|
|
35
|
+
|
|
36
|
+
// update current filter with initial value when it's ready
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setFilters(initialFilters);
|
|
39
|
+
}, [initialFilters]);
|
|
40
|
+
|
|
41
|
+
return { initialFilters, filters, setFilters };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default useTeacherContractFilters;
|
|
@@ -78,7 +78,7 @@ describe('utils/search/getSuggestionsSection', () => {
|
|
|
78
78
|
expect(mockHandle).toHaveBeenCalledWith(
|
|
79
79
|
new Error(
|
|
80
80
|
'Failed to decode JSON in getSuggestionSection FetchError: invalid json response body at ' +
|
|
81
|
-
'/api/v1.0/courses/autocomplete/?query=some%20search reason: Unexpected token o
|
|
81
|
+
'/api/v1.0/courses/autocomplete/?query=some%20search reason: Unexpected token \'o\', "not json" is not valid JSON',
|
|
82
82
|
),
|
|
83
83
|
);
|
|
84
84
|
});
|
|
@@ -181,6 +181,9 @@ export const RichieContextFactory = factory<CommonDataProps['context']>(() => ({
|
|
|
181
181
|
release: faker.system.semver(),
|
|
182
182
|
sentry_dsn: null,
|
|
183
183
|
web_analytics_providers: null,
|
|
184
|
+
site_urls: {
|
|
185
|
+
terms_and_conditions: null,
|
|
186
|
+
},
|
|
184
187
|
}));
|
|
185
188
|
|
|
186
189
|
export const CourseLightFactory = factory<Course>(() => {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { IntlProvider } from 'react-intl';
|
|
3
3
|
import { PropsWithChildren } from 'react';
|
|
4
|
-
import { EnrollmentFactory } from 'utils/test/factories/joanie';
|
|
4
|
+
import { CredentialOrderFactory, EnrollmentFactory } from 'utils/test/factories/joanie';
|
|
5
5
|
import { Priority } from 'types';
|
|
6
|
-
import { Enrollment } from 'types/Joanie';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { CourseRun, Enrollment } from 'types/Joanie';
|
|
7
|
+
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
8
|
+
import { CourseRunFactoryFromPriority } from 'utils/test/factories/richie';
|
|
9
|
+
import { noop } from 'utils';
|
|
10
|
+
import { computeState } from 'utils/CourseRuns';
|
|
11
|
+
import { DashboardItemCourseEnrollingRun, Enrolled } from './DashboardItemCourseEnrolling';
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Most of the component of this file are tested from DashboardItemEnrollment.spec.tsx and
|
|
@@ -16,49 +19,109 @@ describe('<Enrolled/>', () => {
|
|
|
16
19
|
return <IntlProvider locale="en">{children}</IntlProvider>;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
it.each([
|
|
23
|
+
{
|
|
24
|
+
buttonTestLabel: 'and access course button',
|
|
25
|
+
priority: Priority.ONGOING_OPEN,
|
|
26
|
+
expectButton: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
buttonTestLabel: 'and no access course button',
|
|
30
|
+
priority: Priority.FUTURE_OPEN,
|
|
31
|
+
expectButton: false,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
buttonTestLabel: 'and access course button',
|
|
35
|
+
priority: Priority.ARCHIVED_OPEN,
|
|
36
|
+
expectButton: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
buttonTestLabel: 'and no access course button',
|
|
40
|
+
priority: Priority.FUTURE_NOT_YET_OPEN,
|
|
41
|
+
expectButton: false,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
buttonTestLabel: 'and no access course button',
|
|
45
|
+
priority: Priority.FUTURE_CLOSED,
|
|
46
|
+
expectButton: false,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
buttonTestLabel: 'and access course button',
|
|
50
|
+
priority: Priority.ONGOING_CLOSED,
|
|
51
|
+
expectButton: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
buttonTestLabel: 'and access course button',
|
|
55
|
+
priority: Priority.ARCHIVED_CLOSED,
|
|
56
|
+
expectButton: true,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
buttonTestLabel: 'and no access course button',
|
|
60
|
+
priority: Priority.TO_BE_SCHEDULED,
|
|
61
|
+
expectButton: false,
|
|
62
|
+
},
|
|
63
|
+
])(
|
|
64
|
+
'handles enrollments with priority=$priority $buttonTestLabel',
|
|
65
|
+
async ({ priority, expectButton }) => {
|
|
66
|
+
const enrollment: Enrollment = EnrollmentFactory().one();
|
|
67
|
+
enrollment.course_run.state.priority = priority;
|
|
68
|
+
render(<Enrolled enrollment={enrollment} />, { wrapper });
|
|
69
|
+
await screen.findByText(
|
|
70
|
+
'You are enrolled for the session from ' +
|
|
71
|
+
new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
|
|
72
|
+
new Date(enrollment.course_run.start),
|
|
73
|
+
) +
|
|
74
|
+
' to ' +
|
|
75
|
+
new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(
|
|
76
|
+
new Date(enrollment.course_run.end),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
if (expectButton) {
|
|
80
|
+
const link = screen.getByRole('link', { name: 'Access to course' });
|
|
81
|
+
expect(link).toBeEnabled();
|
|
82
|
+
expect(link).toHaveAttribute('href', enrollment.course_run.resource_link);
|
|
83
|
+
} else {
|
|
84
|
+
expect(screen.queryByRole('link', { name: 'Access to course' })).toBeNull();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('<DashboardItemCourseEnrollingRun/>', () => {
|
|
91
|
+
it.each([
|
|
92
|
+
[Priority.ONGOING_OPEN, false],
|
|
93
|
+
[Priority.FUTURE_OPEN, false],
|
|
94
|
+
[Priority.ARCHIVED_OPEN, false],
|
|
95
|
+
[Priority.FUTURE_NOT_YET_OPEN, true],
|
|
96
|
+
[Priority.FUTURE_CLOSED, false],
|
|
97
|
+
[Priority.ONGOING_CLOSED, false],
|
|
98
|
+
[Priority.ARCHIVED_CLOSED, false],
|
|
99
|
+
[Priority.TO_BE_SCHEDULED, false],
|
|
100
|
+
])(
|
|
101
|
+
`handles correctly enrollment_start date displaying with priority=%s`,
|
|
102
|
+
async (priority, expectEnrollmentNotYetOpened) => {
|
|
103
|
+
const order = CredentialOrderFactory().one();
|
|
104
|
+
const courseRun = CourseRunFactoryFromPriority(priority)().one();
|
|
105
|
+
courseRun.state = computeState(courseRun);
|
|
106
|
+
const joanieCourseRun = courseRun as unknown as CourseRun;
|
|
107
|
+
joanieCourseRun.course = order.course;
|
|
108
|
+
|
|
109
|
+
render(
|
|
110
|
+
<IntlProvider locale="en">
|
|
111
|
+
<DashboardItemCourseEnrollingRun
|
|
112
|
+
order={order}
|
|
113
|
+
courseRun={joanieCourseRun}
|
|
114
|
+
selected={false}
|
|
115
|
+
enroll={noop}
|
|
116
|
+
/>
|
|
117
|
+
</IntlProvider>,
|
|
118
|
+
);
|
|
39
119
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
runTest(Priority.ARCHIVED_OPEN, true);
|
|
48
|
-
});
|
|
49
|
-
it('handles enrollments with priority=FUTURE_NOT_YET_OPEN', () => {
|
|
50
|
-
runTest(Priority.FUTURE_NOT_YET_OPEN, false);
|
|
51
|
-
});
|
|
52
|
-
it('handles enrollments with priority=FUTURE_CLOSED', () => {
|
|
53
|
-
runTest(Priority.FUTURE_CLOSED, false);
|
|
54
|
-
});
|
|
55
|
-
it('handles enrollments with priority=ONGOING_CLOSED', () => {
|
|
56
|
-
runTest(Priority.ONGOING_CLOSED, true);
|
|
57
|
-
});
|
|
58
|
-
it('handles enrollments with priority=ARCHIVED_CLOSED', () => {
|
|
59
|
-
runTest(Priority.ARCHIVED_CLOSED, true);
|
|
60
|
-
});
|
|
61
|
-
it('handles enrollments with priority=TO_BE_SCHEDULED', () => {
|
|
62
|
-
runTest(Priority.TO_BE_SCHEDULED, false);
|
|
63
|
-
});
|
|
120
|
+
if (expectEnrollmentNotYetOpened) {
|
|
121
|
+
screen.getByText(/Enrollment will open on/);
|
|
122
|
+
} else {
|
|
123
|
+
expect(screen.queryByText(/Enrollment will open on/)).not.toBeInTheDocument();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
);
|
|
64
127
|
});
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
3
|
import { Button } from '@openfun/cunningham-react';
|
|
4
4
|
import { CoursesHelper } from 'utils/CoursesHelper';
|
|
5
5
|
import { Priority } from 'types';
|
|
6
6
|
import {
|
|
7
|
+
AbstractCourse,
|
|
8
|
+
CertificateOrder,
|
|
7
9
|
CourseRun,
|
|
8
|
-
Enrollment,
|
|
9
10
|
CredentialOrder,
|
|
10
|
-
|
|
11
|
+
Enrollment,
|
|
11
12
|
Product,
|
|
12
|
-
CertificateOrder,
|
|
13
13
|
} from 'types/Joanie';
|
|
14
14
|
import { Spinner } from 'components/Spinner';
|
|
15
15
|
import Banner, { BannerType } from 'components/Banner';
|
|
@@ -208,7 +208,7 @@ interface DashboardItemCourseEnrollingRunProps {
|
|
|
208
208
|
product?: Product;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
const DashboardItemCourseEnrollingRun = ({
|
|
211
|
+
export const DashboardItemCourseEnrollingRun = ({
|
|
212
212
|
courseRun,
|
|
213
213
|
selected,
|
|
214
214
|
enroll,
|
|
@@ -247,7 +247,7 @@ const DashboardItemCourseEnrollingRun = ({
|
|
|
247
247
|
}}
|
|
248
248
|
/>
|
|
249
249
|
</div>
|
|
250
|
-
{
|
|
250
|
+
{courseRun.state.priority === Priority.FUTURE_NOT_YET_OPEN && (
|
|
251
251
|
<div className="dashboard-item__course-enrolling__run__not-opened">
|
|
252
252
|
<FormattedMessage
|
|
253
253
|
{...messages.enrollmentNotYetOpened}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "richie-education",
|
|
3
|
-
"version": "2.25.0-b2.
|
|
3
|
+
"version": "2.25.0-b2.dev62",
|
|
4
4
|
"description": "A CMS to build learning portals for Open Education",
|
|
5
5
|
"main": "sandbox/manage.py",
|
|
6
6
|
"scripts": {
|
|
@@ -147,6 +147,6 @@
|
|
|
147
147
|
"workerDirectory": "../richie/static/richie/js"
|
|
148
148
|
},
|
|
149
149
|
"volta": {
|
|
150
|
-
"node": "
|
|
150
|
+
"node": "20.11.0"
|
|
151
151
|
}
|
|
152
152
|
}
|
|
@@ -131,18 +131,12 @@
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
&__logo {
|
|
134
|
-
|
|
134
|
+
height: 100%;
|
|
135
135
|
width: 100%;
|
|
136
|
-
padding-bottom: 56.25%; // Aspect ratio 16/9
|
|
137
136
|
|
|
138
137
|
& > img {
|
|
139
|
-
position: absolute;
|
|
140
|
-
top: 0;
|
|
141
|
-
right: 0;
|
|
142
|
-
bottom: 0;
|
|
143
|
-
left: 0;
|
|
144
|
-
width: 100%;
|
|
145
138
|
height: 100%;
|
|
139
|
+
width: 100%;
|
|
146
140
|
object-fit: contain;
|
|
147
141
|
object-position: center;
|
|
148
142
|
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
2
|
-
import { useParams, useSearchParams } from 'react-router-dom';
|
|
3
|
-
import { ContractResourceQuery, ContractState } from 'types/Joanie';
|
|
4
|
-
|
|
5
|
-
export type TeacherDashboardContractsParams = {
|
|
6
|
-
organizationId?: string;
|
|
7
|
-
courseProductRelationId?: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const useTeacherContractFilters = () => {
|
|
11
|
-
const { organizationId, courseProductRelationId } = useParams<TeacherDashboardContractsParams>();
|
|
12
|
-
const [searchParams] = useSearchParams();
|
|
13
|
-
|
|
14
|
-
const initialFilters = useMemo(
|
|
15
|
-
() => ({
|
|
16
|
-
signature_state:
|
|
17
|
-
(searchParams.get('signature_state') as ContractState) || ContractState.SIGNED,
|
|
18
|
-
organization_id: organizationId,
|
|
19
|
-
course_product_relation_id: courseProductRelationId,
|
|
20
|
-
}),
|
|
21
|
-
[],
|
|
22
|
-
);
|
|
23
|
-
const [filters, setFilters] = useState<ContractResourceQuery>(initialFilters);
|
|
24
|
-
|
|
25
|
-
return { initialFilters, filters, setFilters };
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export default useTeacherContractFilters;
|