richie-education 2.25.0-b2.dev124 → 2.25.0-b2.dev128
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/hooks/useUnionResource/index.spec.tsx +98 -97
- package/js/hooks/useUnionResource/index.ts +23 -2
- package/js/pages/DashboardCourses/index.spec.tsx +17 -49
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +2 -4
- package/js/utils/OrderHelper/index.ts +54 -0
- package/js/utils/test/factories/joanie.ts +1 -1
- package/js/utils/test/wrappers/BaseAppWrapper.tsx +19 -0
- package/js/utils/test/wrappers/BaseJoanieAppWrapper.tsx +20 -0
- package/js/utils/test/wrappers/JoanieAppWrapper.tsx +6 -16
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +2 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +8 -24
- package/js/widgets/Dashboard/components/DashboardItem/Order/{OrderStateMessage → OrderStateLearnerMessage}/index.spec.tsx +10 -19
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +60 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -77
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +127 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +60 -0
- package/package.json +1 -1
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
2
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
-
import { IntlProvider } from 'react-intl';
|
|
4
2
|
import fetchMock from 'fetch-mock';
|
|
5
|
-
import
|
|
3
|
+
import queryString from 'query-string';
|
|
6
4
|
import { PaginatedResourceQuery } from 'types/Joanie';
|
|
7
|
-
import { History, HistoryContext } from 'hooks/useHistory';
|
|
8
|
-
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
9
|
-
import { SessionProvider } from 'contexts/SessionContext';
|
|
10
5
|
import { Deferred } from 'utils/test/deferred';
|
|
11
|
-
|
|
12
6
|
import { noop } from 'utils';
|
|
13
7
|
import { mockPaginatedResponse } from 'utils/test/mockPaginatedResponse';
|
|
14
8
|
import { PER_PAGE } from 'settings';
|
|
15
9
|
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
16
|
-
import {
|
|
10
|
+
import { BaseAppWrapper } from 'utils/test/wrappers/BaseAppWrapper';
|
|
17
11
|
import { QueryConfig, FetchDataFunction } from './utils/fetchEntity';
|
|
18
12
|
import useUnionResource from '.';
|
|
19
13
|
|
|
@@ -37,49 +31,6 @@ interface TestDataB {
|
|
|
37
31
|
created_on: string;
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
const renderUseUnionResource = <
|
|
41
|
-
DataA extends FetchEntityData,
|
|
42
|
-
DataB extends FetchEntityData,
|
|
43
|
-
FiltersA extends PaginatedResourceQuery,
|
|
44
|
-
FiltersB extends PaginatedResourceQuery,
|
|
45
|
-
>(
|
|
46
|
-
queryAConfig: QueryConfig<DataA, FiltersA>,
|
|
47
|
-
queryBConfig: QueryConfig<DataB, FiltersB>,
|
|
48
|
-
) => {
|
|
49
|
-
const Wrapper = ({ client, children }: PropsWithChildren<{ client?: QueryClient }>) => {
|
|
50
|
-
const historyPushState = jest.fn();
|
|
51
|
-
const historyReplaceState = jest.fn();
|
|
52
|
-
const makeHistoryOf: (params: any) => History = () => [
|
|
53
|
-
{
|
|
54
|
-
state: { name: '', data: {} },
|
|
55
|
-
title: '',
|
|
56
|
-
url: `/`,
|
|
57
|
-
},
|
|
58
|
-
historyPushState,
|
|
59
|
-
historyReplaceState,
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<QueryClientProvider client={client ?? createTestQueryClient({ user: true })}>
|
|
64
|
-
<IntlProvider locale="en">
|
|
65
|
-
<HistoryContext.Provider value={makeHistoryOf({})}>
|
|
66
|
-
<SessionProvider>{children}</SessionProvider>
|
|
67
|
-
</HistoryContext.Provider>
|
|
68
|
-
</IntlProvider>
|
|
69
|
-
</QueryClientProvider>
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
return renderHook(
|
|
74
|
-
() =>
|
|
75
|
-
useUnionResource<DataA, DataB, FiltersA, FiltersB>({
|
|
76
|
-
queryAConfig,
|
|
77
|
-
queryBConfig,
|
|
78
|
-
}),
|
|
79
|
-
{ wrapper: Wrapper },
|
|
80
|
-
);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
34
|
describe('useUnionResource', () => {
|
|
84
35
|
const perPage = PER_PAGE.useUnionResources;
|
|
85
36
|
let dataAList: TestDataA[];
|
|
@@ -111,14 +62,14 @@ describe('useUnionResource', () => {
|
|
|
111
62
|
|
|
112
63
|
{ name: 'TestDataB', id: '14', created_on: '2022-12-03' },
|
|
113
64
|
];
|
|
114
|
-
const dummyFetchWrapper = async (url: string) => {
|
|
115
|
-
const res = await fetch(url);
|
|
65
|
+
const dummyFetchWrapper = async (url: string, queryParams: { [key: string]: any }) => {
|
|
66
|
+
const res = await fetch(`${url}?${queryString.stringify(queryParams)}`);
|
|
116
67
|
return res.json();
|
|
117
68
|
};
|
|
118
|
-
const fetchDataA = ({ page }: { page: number }) =>
|
|
119
|
-
dummyFetchWrapper(
|
|
69
|
+
const fetchDataA = ({ page, isFiltered }: { page: number; isFiltered?: boolean }) =>
|
|
70
|
+
dummyFetchWrapper('http://data.a/', { page, isFiltered });
|
|
120
71
|
const fetchDataB = ({ page }: { page: number }) =>
|
|
121
|
-
dummyFetchWrapper(
|
|
72
|
+
dummyFetchWrapper('http://data.b/', { page });
|
|
122
73
|
|
|
123
74
|
queryAConfig = {
|
|
124
75
|
queryKey: ['resourceA'],
|
|
@@ -132,11 +83,6 @@ describe('useUnionResource', () => {
|
|
|
132
83
|
};
|
|
133
84
|
});
|
|
134
85
|
|
|
135
|
-
afterEach(() => {
|
|
136
|
-
jest.clearAllMocks();
|
|
137
|
-
fetchMock.restore();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
86
|
it('should handle loading state', async () => {
|
|
141
87
|
const dataADeferred = new Deferred();
|
|
142
88
|
const dataBDeferred = new Deferred();
|
|
@@ -144,12 +90,16 @@ describe('useUnionResource', () => {
|
|
|
144
90
|
const pendingDataBPromise = () => dataBDeferred.promise;
|
|
145
91
|
queryAConfig.fn = pendingDataAPromise as FetchDataFunction<TestDataA, TestDataAFilters>;
|
|
146
92
|
queryBConfig.fn = pendingDataBPromise as FetchDataFunction<TestDataB, PaginatedResourceQuery>;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
93
|
+
|
|
94
|
+
const { result } = renderHook(
|
|
95
|
+
() =>
|
|
96
|
+
useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
|
|
97
|
+
queryAConfig,
|
|
98
|
+
queryBConfig,
|
|
99
|
+
}),
|
|
100
|
+
{ wrapper: BaseAppWrapper },
|
|
101
|
+
);
|
|
102
|
+
|
|
153
103
|
expect(result.current.isLoading).toBe(true);
|
|
154
104
|
expect(result.current.hasMore).toBe(false);
|
|
155
105
|
|
|
@@ -164,12 +114,15 @@ describe('useUnionResource', () => {
|
|
|
164
114
|
it('should render less than 1 page of dataA', async () => {
|
|
165
115
|
fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([dataAList[0]], 1, false));
|
|
166
116
|
fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([], 0, false));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
117
|
+
|
|
118
|
+
const { result } = renderHook(
|
|
119
|
+
() =>
|
|
120
|
+
useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
|
|
121
|
+
queryAConfig,
|
|
122
|
+
queryBConfig,
|
|
123
|
+
}),
|
|
124
|
+
{ wrapper: BaseAppWrapper },
|
|
125
|
+
);
|
|
173
126
|
|
|
174
127
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
175
128
|
expect(result.current.hasMore).toBe(false);
|
|
@@ -180,12 +133,15 @@ describe('useUnionResource', () => {
|
|
|
180
133
|
it('should render less than 1 page of dataB', async () => {
|
|
181
134
|
fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([], 0, false));
|
|
182
135
|
fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([dataBList[0]], 1, false));
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
136
|
+
|
|
137
|
+
const { result } = renderHook(
|
|
138
|
+
() =>
|
|
139
|
+
useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
|
|
140
|
+
queryAConfig,
|
|
141
|
+
queryBConfig,
|
|
142
|
+
}),
|
|
143
|
+
{ wrapper: BaseAppWrapper },
|
|
144
|
+
);
|
|
189
145
|
|
|
190
146
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
191
147
|
expect(result.current.hasMore).toBe(false);
|
|
@@ -196,12 +152,15 @@ describe('useUnionResource', () => {
|
|
|
196
152
|
it('should renders less than 1 page of both dataA and dataB', async () => {
|
|
197
153
|
fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([dataAList[0]], 1, false));
|
|
198
154
|
fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([dataBList[0]], 1, false));
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
155
|
+
|
|
156
|
+
const { result } = renderHook(
|
|
157
|
+
() =>
|
|
158
|
+
useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
|
|
159
|
+
queryAConfig,
|
|
160
|
+
queryBConfig,
|
|
161
|
+
}),
|
|
162
|
+
{ wrapper: BaseAppWrapper },
|
|
163
|
+
);
|
|
205
164
|
|
|
206
165
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
207
166
|
expect(result.current.hasMore).toBe(false);
|
|
@@ -237,12 +196,14 @@ describe('useUnionResource', () => {
|
|
|
237
196
|
mockPaginatedResponse(dataBList.slice(perPage * 2, perPage * 3), dataAList.length, false),
|
|
238
197
|
);
|
|
239
198
|
|
|
240
|
-
const { result } =
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
199
|
+
const { result } = renderHook(
|
|
200
|
+
() =>
|
|
201
|
+
useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
|
|
202
|
+
queryAConfig,
|
|
203
|
+
queryBConfig,
|
|
204
|
+
}),
|
|
205
|
+
{ wrapper: BaseAppWrapper },
|
|
206
|
+
);
|
|
246
207
|
|
|
247
208
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
248
209
|
expect(result.current.hasMore).toBe(true);
|
|
@@ -297,12 +258,16 @@ describe('useUnionResource', () => {
|
|
|
297
258
|
const dataADeferred = new Deferred();
|
|
298
259
|
const pendingDataAPromise = () => dataADeferred.promise;
|
|
299
260
|
queryAConfig.fn = pendingDataAPromise as FetchDataFunction<TestDataA, TestDataAFilters>;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
261
|
+
|
|
262
|
+
const { result } = renderHook(
|
|
263
|
+
() =>
|
|
264
|
+
useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
|
|
265
|
+
queryAConfig,
|
|
266
|
+
queryBConfig,
|
|
267
|
+
}),
|
|
268
|
+
{ wrapper: BaseAppWrapper },
|
|
269
|
+
);
|
|
270
|
+
|
|
306
271
|
expect(result.current.isLoading).toBe(true);
|
|
307
272
|
|
|
308
273
|
await act(() => {
|
|
@@ -313,4 +278,40 @@ describe('useUnionResource', () => {
|
|
|
313
278
|
expect(result.current.isLoading).toBe(false);
|
|
314
279
|
expect(result.current.error).toBe('An error occurred while fetching data. Please retry later.');
|
|
315
280
|
});
|
|
281
|
+
|
|
282
|
+
it('should refetch data when filters change', async () => {
|
|
283
|
+
fetchMock.get('http://data.a/?page=1', mockPaginatedResponse([dataAList[0]], 1, false));
|
|
284
|
+
fetchMock.get('http://data.b/?page=1', mockPaginatedResponse([], 0, false));
|
|
285
|
+
|
|
286
|
+
const { result, rerender } = renderHook(
|
|
287
|
+
(queries: {
|
|
288
|
+
queryA?: QueryConfig<TestDataA, TestDataAFilters>;
|
|
289
|
+
queryB?: QueryConfig<TestDataB, PaginatedResourceQuery>;
|
|
290
|
+
}) =>
|
|
291
|
+
useUnionResource<TestDataA, TestDataB, TestDataAFilters, PaginatedResourceQuery>({
|
|
292
|
+
queryAConfig: queries?.queryA || queryAConfig,
|
|
293
|
+
queryBConfig: queries?.queryB || queryBConfig,
|
|
294
|
+
}),
|
|
295
|
+
{ wrapper: BaseAppWrapper },
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
299
|
+
let calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
300
|
+
expect(calledUrls).toHaveLength(2);
|
|
301
|
+
expect(calledUrls).toContain('http://data.a/?page=1');
|
|
302
|
+
expect(calledUrls).toContain('http://data.b/?page=1');
|
|
303
|
+
|
|
304
|
+
queryAConfig.filters = { isFiltered: true };
|
|
305
|
+
fetchMock.get(
|
|
306
|
+
'http://data.a/?isFiltered=true&page=1',
|
|
307
|
+
mockPaginatedResponse([dataAList[0]], 1, false),
|
|
308
|
+
);
|
|
309
|
+
rerender({ queryA: queryAConfig, queryB: queryBConfig });
|
|
310
|
+
expect(result.current.isLoading).toBe(true);
|
|
311
|
+
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
|
312
|
+
|
|
313
|
+
calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
314
|
+
expect(calledUrls).toHaveLength(4);
|
|
315
|
+
expect(calledUrls).toContain('http://data.a/?isFiltered=true&page=1');
|
|
316
|
+
});
|
|
316
317
|
});
|
|
@@ -90,6 +90,8 @@ const useUnionResource = <
|
|
|
90
90
|
const eofRef = useRef<Record<string, number>>(queryClient.getQueryData(eofQueryKey) ?? {});
|
|
91
91
|
log('eof', eofRef.current);
|
|
92
92
|
|
|
93
|
+
const [unionQueryKey, setUnionQueryKey] = useState<string>();
|
|
94
|
+
|
|
93
95
|
const reset = () => {
|
|
94
96
|
setStack([]);
|
|
95
97
|
setPage(0);
|
|
@@ -104,6 +106,18 @@ const useUnionResource = <
|
|
|
104
106
|
useQueryKeyInvalidateListener(queryBConfig.queryKey, reset);
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
// filters have changes, new results will be fetch.
|
|
111
|
+
// let's reset every previous fetches states
|
|
112
|
+
reset();
|
|
113
|
+
|
|
114
|
+
// We need to fetch new results.
|
|
115
|
+
// reset all states isn't enought. If we've a research without results
|
|
116
|
+
// then the next reset would do nothing.
|
|
117
|
+
// to force execution of useEffect::fetchNewPage(), we use a uniq key build with current queries filters.
|
|
118
|
+
setUnionQueryKey(JSON.stringify(queryAConfig.filters) + JSON.stringify(queryBConfig.filters));
|
|
119
|
+
}, [JSON.stringify(queryAConfig.filters), JSON.stringify(queryBConfig.filters)]);
|
|
120
|
+
|
|
107
121
|
useEffect(() => {
|
|
108
122
|
async function fetchNewPage() {
|
|
109
123
|
const {
|
|
@@ -145,11 +159,18 @@ const useUnionResource = <
|
|
|
145
159
|
|
|
146
160
|
// we request more entities than we can display in the right order
|
|
147
161
|
// it's time for new fetching and sorting.
|
|
148
|
-
if (cursor > integrityCount) {
|
|
162
|
+
if (!isSyncing && cursor > integrityCount) {
|
|
149
163
|
setIsSyncing(true);
|
|
150
164
|
fetchNewPage();
|
|
151
165
|
}
|
|
152
|
-
}, [
|
|
166
|
+
}, [
|
|
167
|
+
cursor,
|
|
168
|
+
// FIXME(rlecellier): when stack.length === 0, invalidate the query will not refetch.
|
|
169
|
+
// stack.length is added in the dependency array to force a new fetch on reset.
|
|
170
|
+
stack.length,
|
|
171
|
+
// unionQueryKey assure that we refetch data when query filters change.
|
|
172
|
+
unionQueryKey,
|
|
173
|
+
]);
|
|
153
174
|
|
|
154
175
|
const cursorToUse = Math.min(cursor, integrityCount);
|
|
155
176
|
const next = () => {
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { act, getByRole,
|
|
2
|
-
import { QueryClient
|
|
3
|
-
import { IntlProvider } from 'react-intl';
|
|
1
|
+
import { act, getByRole, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
4
3
|
import fetchMock from 'fetch-mock';
|
|
5
4
|
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import {
|
|
7
|
-
UserFactory,
|
|
8
|
-
RichieContextFactory as mockRichieContextFactory,
|
|
9
|
-
} from 'utils/test/factories/richie';
|
|
10
|
-
import { History, HistoryContext } from 'hooks/useHistory';
|
|
5
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
11
6
|
import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
|
|
12
7
|
import {
|
|
13
8
|
CourseProductRelationFactory,
|
|
@@ -15,7 +10,6 @@ import {
|
|
|
15
10
|
CredentialOrderFactory,
|
|
16
11
|
} from 'utils/test/factories/joanie';
|
|
17
12
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
18
|
-
import { SessionProvider } from 'contexts/SessionContext';
|
|
19
13
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRouteMessages';
|
|
20
14
|
import { CourseLight, CourseProductRelation, Enrollment, CredentialOrder } from 'types/Joanie';
|
|
21
15
|
import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
|
|
@@ -25,6 +19,9 @@ import { isOrder } from 'pages/DashboardCourses/useOrdersEnrollments';
|
|
|
25
19
|
import { noop } from 'utils';
|
|
26
20
|
import { PER_PAGE } from 'settings';
|
|
27
21
|
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
22
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
23
|
+
import { render } from 'utils/test/render';
|
|
24
|
+
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
28
25
|
|
|
29
26
|
jest.mock('utils/context', () => ({
|
|
30
27
|
__esModule: true,
|
|
@@ -52,44 +49,8 @@ jest.mock('hooks/useIntersectionObserver', () => ({
|
|
|
52
49
|
}));
|
|
53
50
|
|
|
54
51
|
describe('<DashboardCourses/>', () => {
|
|
52
|
+
setupJoanieSession();
|
|
55
53
|
const perPage = PER_PAGE.useOrdersEnrollments;
|
|
56
|
-
const historyPushState = jest.fn();
|
|
57
|
-
const historyReplaceState = jest.fn();
|
|
58
|
-
const makeHistoryOf: (params: any) => History = () => [
|
|
59
|
-
{
|
|
60
|
-
state: { name: '', data: {} },
|
|
61
|
-
title: '',
|
|
62
|
-
url: `/`,
|
|
63
|
-
},
|
|
64
|
-
historyPushState,
|
|
65
|
-
historyReplaceState,
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
|
|
70
|
-
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
|
|
71
|
-
fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
afterEach(() => {
|
|
75
|
-
jest.clearAllMocks();
|
|
76
|
-
fetchMock.restore();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const Wrapper = ({ client }: { client?: QueryClient }) => {
|
|
80
|
-
const user = UserFactory().one();
|
|
81
|
-
return (
|
|
82
|
-
<QueryClientProvider client={client ?? createTestQueryClient({ user })}>
|
|
83
|
-
<IntlProvider locale="en">
|
|
84
|
-
<HistoryContext.Provider value={makeHistoryOf({})}>
|
|
85
|
-
<SessionProvider>
|
|
86
|
-
<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />
|
|
87
|
-
</SessionProvider>
|
|
88
|
-
</HistoryContext.Provider>
|
|
89
|
-
</IntlProvider>
|
|
90
|
-
</QueryClientProvider>
|
|
91
|
-
);
|
|
92
|
-
};
|
|
93
54
|
|
|
94
55
|
const mockOrders = (orders: CredentialOrder[], client?: QueryClient) => {
|
|
95
56
|
const relations: Record<string, CourseProductRelation> = {};
|
|
@@ -150,7 +111,9 @@ describe('<DashboardCourses/>', () => {
|
|
|
150
111
|
enrollmentsDeferred.promise,
|
|
151
112
|
);
|
|
152
113
|
|
|
153
|
-
render(<
|
|
114
|
+
render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
|
|
115
|
+
wrapper: BaseJoanieAppWrapper,
|
|
116
|
+
});
|
|
154
117
|
|
|
155
118
|
await expectSpinner('Loading orders and enrollments...');
|
|
156
119
|
expect(await screen.queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
|
|
@@ -242,7 +205,9 @@ describe('<DashboardCourses/>', () => {
|
|
|
242
205
|
|
|
243
206
|
const entities = merge(orders, enrollments);
|
|
244
207
|
|
|
245
|
-
render(<
|
|
208
|
+
render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
|
|
209
|
+
wrapper: BaseJoanieAppWrapper,
|
|
210
|
+
});
|
|
246
211
|
|
|
247
212
|
// Slice 1.
|
|
248
213
|
await expectNoSpinner('Loading orders and enrollments...');
|
|
@@ -277,7 +242,10 @@ describe('<DashboardCourses/>', () => {
|
|
|
277
242
|
{ results: [], next: null, previous: null, count: 0 },
|
|
278
243
|
);
|
|
279
244
|
|
|
280
|
-
render(<
|
|
245
|
+
render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
|
|
246
|
+
wrapper: BaseJoanieAppWrapper,
|
|
247
|
+
});
|
|
248
|
+
|
|
281
249
|
ordersDeferred.resolve({
|
|
282
250
|
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
283
251
|
body: 'Internal Server Error',
|
|
@@ -55,7 +55,7 @@ describe('pages/CourseLearnerDataGrid', () => {
|
|
|
55
55
|
expect(cells[2]).toHaveTextContent(
|
|
56
56
|
intl.formatDate(new Date(courseOrder.created_on), DEFAULT_DATE_FORMAT),
|
|
57
57
|
);
|
|
58
|
-
expect(cells[3]).toHaveTextContent('
|
|
58
|
+
expect(cells[3]).toHaveTextContent('Certified');
|
|
59
59
|
expect(within(cells[4]).getByText('Contact')).toBeInTheDocument();
|
|
60
60
|
});
|
|
61
61
|
});
|
package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { Button, DataGrid, DataGridProps, PaginationProps, Row } from '@openfun/
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
4
|
import { NestedCourseOrder } from 'types/Joanie';
|
|
5
5
|
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
6
|
-
import
|
|
6
|
+
import OrderStateTeacherMessage from 'widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage';
|
|
7
7
|
import DashboardListAvatar from 'widgets/Dashboard/components/DashboardListAvatar';
|
|
8
8
|
|
|
9
9
|
const messages = defineMessages({
|
|
@@ -74,10 +74,8 @@ const CourseLearnerDataGrid = ({
|
|
|
74
74
|
headerName: intl.formatMessage(messages.columnState),
|
|
75
75
|
enableSorting: false,
|
|
76
76
|
renderCell: (params: { row: Row }) => {
|
|
77
|
-
// TODO(rlecellier): create NestedOrderCourseStateMessage that's get label dedicated to teatchers.
|
|
78
|
-
// like signature needed for studdent is awaiting signature for a teacher.
|
|
79
77
|
return (
|
|
80
|
-
<
|
|
78
|
+
<OrderStateTeacherMessage
|
|
81
79
|
order={params.row.courseOrder}
|
|
82
80
|
contractDefinition={params.row.courseOrder.product.contract_definition_id}
|
|
83
81
|
/>
|
|
@@ -7,10 +7,52 @@ import {
|
|
|
7
7
|
NestedCourseOrder,
|
|
8
8
|
} from 'types/Joanie';
|
|
9
9
|
|
|
10
|
+
export enum OrderStatus {
|
|
11
|
+
DRAFT = 'draft',
|
|
12
|
+
SUBMITTED = 'submitted',
|
|
13
|
+
PENDING = 'pending',
|
|
14
|
+
CANCELED = 'canceled',
|
|
15
|
+
WAITING_SIGNATURE = 'waiting_signature',
|
|
16
|
+
WAITING_COUNTER_SIGNATURE = 'waiting_counter_signature',
|
|
17
|
+
COMPLETED = 'completed',
|
|
18
|
+
ON_GOING = 'on_going',
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
/**
|
|
11
22
|
* Helper class for orders
|
|
12
23
|
*/
|
|
13
24
|
export class OrderHelper {
|
|
25
|
+
static getState(order: Order | NestedCourseOrder, contractDefinition?: ContractDefinition) {
|
|
26
|
+
const { certificate_id: certificateId } = order;
|
|
27
|
+
|
|
28
|
+
if (order.state === OrderState.VALIDATED) {
|
|
29
|
+
if (OrderHelper.orderNeedsSignature(order, contractDefinition)) {
|
|
30
|
+
return OrderStatus.WAITING_SIGNATURE;
|
|
31
|
+
}
|
|
32
|
+
if (OrderHelper.orderNeedsCounterSignature(order)) {
|
|
33
|
+
return OrderStatus.WAITING_COUNTER_SIGNATURE;
|
|
34
|
+
}
|
|
35
|
+
if (certificateId) {
|
|
36
|
+
return OrderStatus.COMPLETED;
|
|
37
|
+
} else {
|
|
38
|
+
return OrderStatus.ON_GOING;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const orderStatusMap = {
|
|
43
|
+
[OrderState.DRAFT]: OrderStatus.DRAFT,
|
|
44
|
+
[OrderState.SUBMITTED]: OrderStatus.SUBMITTED,
|
|
45
|
+
[OrderState.PENDING]: OrderStatus.PENDING,
|
|
46
|
+
[OrderState.CANCELED]: OrderStatus.CANCELED,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (order.state in orderStatusMap) {
|
|
50
|
+
return orderStatusMap[order.state];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
14
56
|
/**
|
|
15
57
|
* return an Order from the given list that match given productId
|
|
16
58
|
*/
|
|
@@ -33,4 +75,16 @@ export class OrderHelper {
|
|
|
33
75
|
!(order.contract && order.contract.student_signed_on)
|
|
34
76
|
);
|
|
35
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* tell us if a order need to be sign by the organization.
|
|
81
|
+
*/
|
|
82
|
+
static orderNeedsCounterSignature(order: Order | NestedCourseOrder) {
|
|
83
|
+
return (
|
|
84
|
+
order?.state === OrderState.VALIDATED &&
|
|
85
|
+
order.contract &&
|
|
86
|
+
order.contract.student_signed_on &&
|
|
87
|
+
!order.contract.organization_signed_on
|
|
88
|
+
);
|
|
89
|
+
}
|
|
36
90
|
}
|
|
@@ -146,7 +146,7 @@ export const ContractFactory = factory((): Contract => {
|
|
|
146
146
|
export const ContractLightFactory = factory((): ContractLight => {
|
|
147
147
|
return {
|
|
148
148
|
id: faker.string.uuid(),
|
|
149
|
-
student_signed_on:
|
|
149
|
+
student_signed_on: null,
|
|
150
150
|
organization_signed_on: null,
|
|
151
151
|
};
|
|
152
152
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
2
|
+
import { SessionProvider } from 'contexts/SessionContext';
|
|
3
|
+
import { IntlWrapper } from './IntlWrapper';
|
|
4
|
+
import { ReactQueryWrapper } from './ReactQueryWrapper';
|
|
5
|
+
import { AppWrapperProps } from './types';
|
|
6
|
+
|
|
7
|
+
export const BaseAppWrapper = ({
|
|
8
|
+
children,
|
|
9
|
+
intlOptions,
|
|
10
|
+
queryOptions,
|
|
11
|
+
}: PropsWithChildren<AppWrapperProps>) => {
|
|
12
|
+
return (
|
|
13
|
+
<IntlWrapper {...(intlOptions || { locale: 'en' })}>
|
|
14
|
+
<ReactQueryWrapper {...(queryOptions || {})}>
|
|
15
|
+
<SessionProvider>{children}</SessionProvider>
|
|
16
|
+
</ReactQueryWrapper>
|
|
17
|
+
</IntlWrapper>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PropsWithChildren } from 'react';
|
|
2
|
+
|
|
3
|
+
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
4
|
+
import { IntlWrapper } from './IntlWrapper';
|
|
5
|
+
import { ReactQueryWrapper } from './ReactQueryWrapper';
|
|
6
|
+
import { AppWrapperProps } from './types';
|
|
7
|
+
|
|
8
|
+
export const BaseJoanieAppWrapper = ({
|
|
9
|
+
children,
|
|
10
|
+
intlOptions,
|
|
11
|
+
queryOptions,
|
|
12
|
+
}: PropsWithChildren<AppWrapperProps>) => {
|
|
13
|
+
return (
|
|
14
|
+
<IntlWrapper {...(intlOptions || { locale: 'en' })}>
|
|
15
|
+
<ReactQueryWrapper {...(queryOptions || {})}>
|
|
16
|
+
<JoanieSessionProvider>{children}</JoanieSessionProvider>
|
|
17
|
+
</ReactQueryWrapper>
|
|
18
|
+
</IntlWrapper>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { CunninghamProvider } from '@openfun/cunningham-react';
|
|
2
2
|
import { PropsWithChildren } from 'react';
|
|
3
3
|
import fetchMock from 'fetch-mock';
|
|
4
|
-
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
5
4
|
import { DashboardBreadcrumbsProvider } from 'widgets/Dashboard/contexts/DashboardBreadcrumbsContext';
|
|
6
|
-
import { IntlWrapper } from './IntlWrapper';
|
|
7
|
-
import { ReactQueryWrapper } from './ReactQueryWrapper';
|
|
8
5
|
import { RouterWrapper } from './RouterWrapper';
|
|
9
6
|
import { AppWrapperProps } from './types';
|
|
7
|
+
import { BaseJoanieAppWrapper } from './BaseJoanieAppWrapper';
|
|
10
8
|
|
|
11
9
|
export const setupJoanieSession = () => {
|
|
12
10
|
beforeEach(() => {
|
|
@@ -21,10 +19,6 @@ export const setupJoanieSession = () => {
|
|
|
21
19
|
};
|
|
22
20
|
};
|
|
23
21
|
|
|
24
|
-
export const JoanieSessionWrapper = ({ children }: PropsWithChildren) => {
|
|
25
|
-
return <JoanieSessionProvider>{children}</JoanieSessionProvider>;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
22
|
export const JoanieAppWrapper = ({
|
|
29
23
|
children,
|
|
30
24
|
intlOptions,
|
|
@@ -32,16 +26,12 @@ export const JoanieAppWrapper = ({
|
|
|
32
26
|
routerOptions,
|
|
33
27
|
}: PropsWithChildren<AppWrapperProps>) => {
|
|
34
28
|
return (
|
|
35
|
-
<
|
|
29
|
+
<BaseJoanieAppWrapper intlOptions={intlOptions} queryOptions={queryOptions}>
|
|
36
30
|
<CunninghamProvider>
|
|
37
|
-
<
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
<RouterWrapper {...routerOptions}>{children}</RouterWrapper>
|
|
41
|
-
</DashboardBreadcrumbsProvider>
|
|
42
|
-
</JoanieSessionWrapper>
|
|
43
|
-
</ReactQueryWrapper>
|
|
31
|
+
<DashboardBreadcrumbsProvider>
|
|
32
|
+
<RouterWrapper {...routerOptions}>{children}</RouterWrapper>
|
|
33
|
+
</DashboardBreadcrumbsProvider>
|
|
44
34
|
</CunninghamProvider>
|
|
45
|
-
</
|
|
35
|
+
</BaseJoanieAppWrapper>
|
|
46
36
|
);
|
|
47
37
|
};
|
|
@@ -20,7 +20,7 @@ import { DashboardSubItemsList } from '../DashboardSubItemsList';
|
|
|
20
20
|
import { DashboardItemCourseEnrolling } from '../CourseEnrolling';
|
|
21
21
|
import { DashboardItem } from '../index';
|
|
22
22
|
import { DashboardItemContract } from '../Contract';
|
|
23
|
-
import
|
|
23
|
+
import OrderStateLearnerMessage from './OrderStateLearnerMessage';
|
|
24
24
|
|
|
25
25
|
const messages = defineMessages({
|
|
26
26
|
accessCourse: {
|
|
@@ -159,7 +159,7 @@ export const DashboardItemOrder = ({
|
|
|
159
159
|
<div className="dashboard-item-order__footer">
|
|
160
160
|
<div className="dashboard-item__block__status">
|
|
161
161
|
<Icon name={IconTypeEnum.SCHOOL} />
|
|
162
|
-
<
|
|
162
|
+
<OrderStateLearnerMessage
|
|
163
163
|
order={order}
|
|
164
164
|
contractDefinition={product?.contract_definition}
|
|
165
165
|
/>
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
// FIXME: this test is about useUnionResource behavior.
|
|
2
2
|
// we need to rewrite it in useUnionResource tests suite as small and generic as possible.
|
|
3
|
-
import { QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
-
import { IntlProvider } from 'react-intl';
|
|
5
3
|
import fetchMock from 'fetch-mock';
|
|
6
|
-
import { act,
|
|
4
|
+
import { act, screen, waitFor, within } from '@testing-library/react';
|
|
7
5
|
import userEvent from '@testing-library/user-event';
|
|
8
6
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
9
|
-
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
10
|
-
import { SessionProvider } from 'contexts/SessionContext';
|
|
11
7
|
import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
|
|
12
8
|
import {
|
|
13
9
|
ContractFactory,
|
|
@@ -19,6 +15,9 @@ import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRouteMessa
|
|
|
19
15
|
import { Deferred } from 'utils/test/deferred';
|
|
20
16
|
import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
|
|
21
17
|
import { CONTRACT_SETTINGS } from 'settings';
|
|
18
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
19
|
+
import { render } from 'utils/test/render';
|
|
20
|
+
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
22
21
|
|
|
23
22
|
jest.mock('utils/context', () => ({
|
|
24
23
|
__esModule: true,
|
|
@@ -35,31 +34,15 @@ jest.mock('hooks/useIntersectionObserver', () => ({
|
|
|
35
34
|
}));
|
|
36
35
|
|
|
37
36
|
describe('<DashboardItemOrder/> Contract', () => {
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
41
|
-
<IntlProvider locale="en">
|
|
42
|
-
<SessionProvider>
|
|
43
|
-
<DashboardTest initialRoute={route} />
|
|
44
|
-
</SessionProvider>
|
|
45
|
-
</IntlProvider>
|
|
46
|
-
</QueryClientProvider>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
37
|
+
setupJoanieSession();
|
|
49
38
|
|
|
50
39
|
beforeEach(() => {
|
|
51
40
|
jest.useFakeTimers();
|
|
52
41
|
jest.clearAllTimers();
|
|
53
|
-
|
|
54
|
-
fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
|
|
55
|
-
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
|
|
56
|
-
fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
|
|
57
42
|
});
|
|
58
43
|
|
|
59
44
|
afterEach(() => {
|
|
60
45
|
jest.restoreAllMocks();
|
|
61
|
-
jest.clearAllMocks();
|
|
62
|
-
fetchMock.restore();
|
|
63
46
|
});
|
|
64
47
|
|
|
65
48
|
describe('writable', () => {
|
|
@@ -103,9 +86,10 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
103
86
|
// RTL too. See https://github.com/testing-library/user-event/issues/833.
|
|
104
87
|
const user = userEvent.setup({ delay: null });
|
|
105
88
|
|
|
106
|
-
render(
|
|
89
|
+
render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
|
|
90
|
+
wrapper: BaseJoanieAppWrapper,
|
|
91
|
+
});
|
|
107
92
|
|
|
108
|
-
// console.log('apiCalls:2', fetchMock.calls().length);
|
|
109
93
|
await expectNoSpinner('Loading orders and enrollments...');
|
|
110
94
|
|
|
111
95
|
expect(
|
|
@@ -7,22 +7,10 @@ import {
|
|
|
7
7
|
CredentialOrderFactory,
|
|
8
8
|
} from 'utils/test/factories/joanie';
|
|
9
9
|
import { OrderState } from 'types/Joanie';
|
|
10
|
-
import
|
|
10
|
+
import OrderStateLearnerMessage, { messages } from '.';
|
|
11
11
|
|
|
12
12
|
const intl = createIntl({ locale: 'en' });
|
|
13
13
|
|
|
14
|
-
/*
|
|
15
|
-
@TODO (rlecellier): Rewrite everything with these guidlignes
|
|
16
|
-
Order lifecycle:
|
|
17
|
-
* Draft: order has been created
|
|
18
|
-
* Submitted: order information have been validated
|
|
19
|
-
* Pending: payment has failed but can be retried
|
|
20
|
-
* Validated:
|
|
21
|
-
* Completed (Validated with generated certificate)
|
|
22
|
-
* On going (Validated without generated certificate)
|
|
23
|
-
* Signature needed: !order.product.contract.isSign
|
|
24
|
-
* Canceled: has been canceled
|
|
25
|
-
*/
|
|
26
14
|
describe('<DashboardItemOrder/>', () => {
|
|
27
15
|
const Wrapper = ({ children }: PropsWithChildren) => (
|
|
28
16
|
<IntlProvider locale="en">{children}</IntlProvider>
|
|
@@ -39,7 +27,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
39
27
|
const order = CredentialOrderFactory({ state }).one();
|
|
40
28
|
render(
|
|
41
29
|
<Wrapper>
|
|
42
|
-
<
|
|
30
|
+
<OrderStateLearnerMessage order={order} />
|
|
43
31
|
</Wrapper>,
|
|
44
32
|
);
|
|
45
33
|
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
|
|
@@ -60,7 +48,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
60
48
|
}).one();
|
|
61
49
|
render(
|
|
62
50
|
<Wrapper>
|
|
63
|
-
<
|
|
51
|
+
<OrderStateLearnerMessage order={orderWithContract} />
|
|
64
52
|
</Wrapper>,
|
|
65
53
|
);
|
|
66
54
|
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
|
|
@@ -77,7 +65,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
77
65
|
|
|
78
66
|
render(
|
|
79
67
|
<Wrapper>
|
|
80
|
-
<
|
|
68
|
+
<OrderStateLearnerMessage order={order} contractDefinition={contractDefinition} />
|
|
81
69
|
</Wrapper>,
|
|
82
70
|
);
|
|
83
71
|
expect(screen.getByText('Signature required')).toBeInTheDocument();
|
|
@@ -91,7 +79,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
91
79
|
}).one();
|
|
92
80
|
render(
|
|
93
81
|
<Wrapper>
|
|
94
|
-
<
|
|
82
|
+
<OrderStateLearnerMessage order={order} />
|
|
95
83
|
</Wrapper>,
|
|
96
84
|
);
|
|
97
85
|
expect(
|
|
@@ -104,12 +92,15 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
104
92
|
it('should display message for validated order that have a generated certificate', () => {
|
|
105
93
|
const order = CredentialOrderFactory({
|
|
106
94
|
state: OrderState.VALIDATED,
|
|
107
|
-
contract: ContractFactory({
|
|
95
|
+
contract: ContractFactory({
|
|
96
|
+
student_signed_on: new Date().toISOString(),
|
|
97
|
+
organization_signed_on: new Date().toISOString(),
|
|
98
|
+
}).one(),
|
|
108
99
|
certificate_id: 'FAKE_CERTIFICATE_ID',
|
|
109
100
|
}).one();
|
|
110
101
|
render(
|
|
111
102
|
<Wrapper>
|
|
112
|
-
<
|
|
103
|
+
<OrderStateLearnerMessage order={order} />
|
|
113
104
|
</Wrapper>,
|
|
114
105
|
);
|
|
115
106
|
expect(
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
import OrderStateMessage, { OrderStateMessageBaseProps } from '../OrderStateMessage';
|
|
3
|
+
|
|
4
|
+
export const messages = defineMessages({
|
|
5
|
+
statusDraft: {
|
|
6
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusDraft',
|
|
7
|
+
description: 'Status shown on the dashboard order item when order is draft.',
|
|
8
|
+
defaultMessage: 'Draft',
|
|
9
|
+
},
|
|
10
|
+
statusSubmitted: {
|
|
11
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusSubmitted',
|
|
12
|
+
description: 'Status shown on the dashboard order item when order is submitted.',
|
|
13
|
+
defaultMessage: 'Submitted',
|
|
14
|
+
},
|
|
15
|
+
statusPending: {
|
|
16
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusPending',
|
|
17
|
+
description: 'Status shown on the dashboard order item when order is pending.',
|
|
18
|
+
defaultMessage: 'Pending',
|
|
19
|
+
},
|
|
20
|
+
statusOnGoing: {
|
|
21
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusOnGoing',
|
|
22
|
+
description:
|
|
23
|
+
'Status shown on the dashboard order item when order is validated with no certificate',
|
|
24
|
+
defaultMessage: 'On going',
|
|
25
|
+
},
|
|
26
|
+
statusCompleted: {
|
|
27
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusCompleted',
|
|
28
|
+
description:
|
|
29
|
+
'Status shown on the dashboard order item when order is validated with certificate',
|
|
30
|
+
defaultMessage: 'Completed',
|
|
31
|
+
},
|
|
32
|
+
statusWaitingSignature: {
|
|
33
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusWaitingSignature',
|
|
34
|
+
description:
|
|
35
|
+
"Status shown on the dashboard order item when order is validated with contract's learner signature missing.",
|
|
36
|
+
defaultMessage: 'Signature required',
|
|
37
|
+
},
|
|
38
|
+
statusWaitingCounterSignature: {
|
|
39
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusWaitingCounterSignature',
|
|
40
|
+
description:
|
|
41
|
+
"Status shown on the dashboard order item when order is validated with contract's organization signature missing.",
|
|
42
|
+
defaultMessage: 'On going',
|
|
43
|
+
},
|
|
44
|
+
statusCanceled: {
|
|
45
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusCanceled',
|
|
46
|
+
description: 'Status shown on the dashboard order item when order is canceled',
|
|
47
|
+
defaultMessage: 'Canceled',
|
|
48
|
+
},
|
|
49
|
+
statusOther: {
|
|
50
|
+
id: 'components.DashboardItem.Order.OrderStateMessage.statusOther',
|
|
51
|
+
description: 'Status shown on the dashboard order item when order status is unknown',
|
|
52
|
+
defaultMessage: '{state}',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const OrderStateLearnerMessage = (props: OrderStateMessageBaseProps) => {
|
|
57
|
+
return <OrderStateMessage {...props} messages={messages} />;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default OrderStateLearnerMessage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormattedMessage,
|
|
1
|
+
import { FormattedMessage, MessageDescriptor } from 'react-intl';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
CertificateOrder,
|
|
@@ -9,96 +9,46 @@ import {
|
|
|
9
9
|
} from 'types/Joanie';
|
|
10
10
|
import { StringHelper } from 'utils/StringHelper';
|
|
11
11
|
import { handle } from 'utils/errors/handle';
|
|
12
|
-
import { OrderHelper } from 'utils/OrderHelper';
|
|
12
|
+
import { OrderHelper, OrderStatus } from 'utils/OrderHelper';
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
statusDraft: {
|
|
16
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusDraft',
|
|
17
|
-
description: 'Status shown on the dashboard order item when order is draft.',
|
|
18
|
-
defaultMessage: 'Draft',
|
|
19
|
-
},
|
|
20
|
-
statusSubmitted: {
|
|
21
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusSubmitted',
|
|
22
|
-
description: 'Status shown on the dashboard order item when order is submitted.',
|
|
23
|
-
defaultMessage: 'Submitted',
|
|
24
|
-
},
|
|
25
|
-
statusPending: {
|
|
26
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusPending',
|
|
27
|
-
description: 'Status shown on the dashboard order item when order is pending.',
|
|
28
|
-
defaultMessage: 'Pending',
|
|
29
|
-
},
|
|
30
|
-
statusOnGoing: {
|
|
31
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusOnGoing',
|
|
32
|
-
description:
|
|
33
|
-
'Status shown on the dashboard order item when order is validated with no certificate',
|
|
34
|
-
defaultMessage: 'On going',
|
|
35
|
-
},
|
|
36
|
-
statusCompleted: {
|
|
37
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusCompleted',
|
|
38
|
-
description:
|
|
39
|
-
'Status shown on the dashboard order item when order is validated with certificate',
|
|
40
|
-
defaultMessage: 'Completed',
|
|
41
|
-
},
|
|
42
|
-
statusWaitingSignature: {
|
|
43
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusWaitingSignature',
|
|
44
|
-
description:
|
|
45
|
-
"Status shown on the dashboard order item when order is validated with contract's learner signature missing.",
|
|
46
|
-
defaultMessage: 'Signature required',
|
|
47
|
-
},
|
|
48
|
-
statusCanceled: {
|
|
49
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusCanceled',
|
|
50
|
-
description: 'Status shown on the dashboard order item when order is canceled',
|
|
51
|
-
defaultMessage: 'Canceled',
|
|
52
|
-
},
|
|
53
|
-
statusOther: {
|
|
54
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusOther',
|
|
55
|
-
description: 'Status shown on the dashboard order item when order status is unknown',
|
|
56
|
-
defaultMessage: '{state}',
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
interface OrderStateMessageProps {
|
|
14
|
+
export interface OrderStateMessageBaseProps {
|
|
61
15
|
order: CredentialOrder | CertificateOrder | NestedCourseOrder;
|
|
62
16
|
contractDefinition?: ContractDefinition;
|
|
63
17
|
}
|
|
64
18
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
[OrderState.DRAFT]: messages.statusDraft,
|
|
69
|
-
[OrderState.SUBMITTED]: messages.statusSubmitted,
|
|
70
|
-
[OrderState.PENDING]: messages.statusPending,
|
|
71
|
-
[OrderState.CANCELED]: messages.statusCanceled,
|
|
72
|
-
};
|
|
19
|
+
interface OrderStateMessageProps extends OrderStateMessageBaseProps {
|
|
20
|
+
messages: Record<string, MessageDescriptor>;
|
|
21
|
+
}
|
|
73
22
|
|
|
23
|
+
const OrderStateMessage = ({ order, contractDefinition, messages }: OrderStateMessageProps) => {
|
|
74
24
|
useEffect(() => {
|
|
75
25
|
if (!Object.values(OrderState).includes(order.state)) {
|
|
76
26
|
handle(new Error(`Unknown order state ${order.state}`));
|
|
77
27
|
}
|
|
78
28
|
}, [order.state]);
|
|
79
29
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
return
|
|
30
|
+
const orderStatusMessagesMap = {
|
|
31
|
+
[OrderStatus.DRAFT]: messages.statusDraft,
|
|
32
|
+
[OrderStatus.SUBMITTED]: messages.statusSubmitted,
|
|
33
|
+
[OrderStatus.PENDING]: messages.statusPending,
|
|
34
|
+
[OrderStatus.CANCELED]: messages.statusCanceled,
|
|
35
|
+
[OrderStatus.WAITING_SIGNATURE]: messages.statusWaitingSignature,
|
|
36
|
+
[OrderStatus.WAITING_COUNTER_SIGNATURE]: messages.statusWaitingCounterSignature,
|
|
37
|
+
[OrderStatus.COMPLETED]: messages.statusCompleted,
|
|
38
|
+
[OrderStatus.ON_GOING]: messages.statusOnGoing,
|
|
39
|
+
};
|
|
40
|
+
const status = OrderHelper.getState(order, contractDefinition);
|
|
41
|
+
|
|
42
|
+
if (status === null) {
|
|
43
|
+
return (
|
|
44
|
+
<FormattedMessage
|
|
45
|
+
{...messages.statusOther}
|
|
46
|
+
values={{ state: StringHelper.capitalizeFirst(order.state) }}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
94
49
|
}
|
|
95
50
|
|
|
96
|
-
return
|
|
97
|
-
<FormattedMessage
|
|
98
|
-
{...messages.statusOther}
|
|
99
|
-
values={{ state: StringHelper.capitalizeFirst(order.state) }}
|
|
100
|
-
/>
|
|
101
|
-
);
|
|
51
|
+
return <FormattedMessage {...orderStatusMessagesMap[status]} />;
|
|
102
52
|
};
|
|
103
53
|
|
|
104
54
|
export default OrderStateMessage;
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { IntlProvider, createIntl } from 'react-intl';
|
|
4
|
+
import {
|
|
5
|
+
ContractDefinitionFactory,
|
|
6
|
+
ContractFactory,
|
|
7
|
+
CredentialOrderFactory,
|
|
8
|
+
} from 'utils/test/factories/joanie';
|
|
9
|
+
import { OrderState } from 'types/Joanie';
|
|
10
|
+
import OrderStateTeacherMessage, { messages } from '.';
|
|
11
|
+
|
|
12
|
+
const intl = createIntl({ locale: 'en' });
|
|
13
|
+
|
|
14
|
+
describe('<OrderStateTeacherMessage/>', () => {
|
|
15
|
+
const Wrapper = ({ children }: PropsWithChildren) => (
|
|
16
|
+
<IntlProvider locale="en">{children}</IntlProvider>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
[OrderState.DRAFT, 'Pending'],
|
|
21
|
+
[OrderState.SUBMITTED, 'Pending'],
|
|
22
|
+
[OrderState.PENDING, 'Pending'],
|
|
23
|
+
[OrderState.CANCELED, 'Canceled'],
|
|
24
|
+
])(
|
|
25
|
+
'should display message from order state: %s when order have no contract',
|
|
26
|
+
(state, expectedMessage) => {
|
|
27
|
+
const order = CredentialOrderFactory({ state }).one();
|
|
28
|
+
render(
|
|
29
|
+
<Wrapper>
|
|
30
|
+
<OrderStateTeacherMessage order={order} />
|
|
31
|
+
</Wrapper>,
|
|
32
|
+
);
|
|
33
|
+
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
[OrderState.DRAFT, 'Pending'],
|
|
39
|
+
[OrderState.SUBMITTED, 'Pending'],
|
|
40
|
+
[OrderState.PENDING, 'Pending'],
|
|
41
|
+
[OrderState.CANCELED, 'Canceled'],
|
|
42
|
+
])(
|
|
43
|
+
'should display message from order state: %s when order have contract',
|
|
44
|
+
(state, expectedMessage) => {
|
|
45
|
+
const orderWithContract = CredentialOrderFactory({
|
|
46
|
+
state,
|
|
47
|
+
contract: ContractFactory().one(),
|
|
48
|
+
}).one();
|
|
49
|
+
render(
|
|
50
|
+
<Wrapper>
|
|
51
|
+
<OrderStateTeacherMessage
|
|
52
|
+
order={orderWithContract}
|
|
53
|
+
contractDefinition={ContractDefinitionFactory().one()}
|
|
54
|
+
/>
|
|
55
|
+
</Wrapper>,
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
it('should display message for validated order that need learner signature', () => {
|
|
62
|
+
const order = CredentialOrderFactory({
|
|
63
|
+
state: OrderState.VALIDATED,
|
|
64
|
+
contract: null,
|
|
65
|
+
}).one();
|
|
66
|
+
|
|
67
|
+
const contractDefinition = ContractDefinitionFactory().one();
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<Wrapper>
|
|
71
|
+
<OrderStateTeacherMessage order={order} contractDefinition={contractDefinition} />
|
|
72
|
+
</Wrapper>,
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByText("Pending for learner's signature")).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should display message for validated order that need organization signature', () => {
|
|
78
|
+
const order = CredentialOrderFactory({
|
|
79
|
+
state: OrderState.VALIDATED,
|
|
80
|
+
contract: ContractFactory({
|
|
81
|
+
student_signed_on: new Date().toISOString(),
|
|
82
|
+
}).one(),
|
|
83
|
+
}).one();
|
|
84
|
+
const contractDefinition = ContractDefinitionFactory().one();
|
|
85
|
+
|
|
86
|
+
render(
|
|
87
|
+
<Wrapper>
|
|
88
|
+
<OrderStateTeacherMessage order={order} contractDefinition={contractDefinition} />
|
|
89
|
+
</Wrapper>,
|
|
90
|
+
);
|
|
91
|
+
expect(screen.getByText('To be signed')).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should display message for validated order that don't have a generated certificate", () => {
|
|
95
|
+
const order = CredentialOrderFactory({
|
|
96
|
+
state: OrderState.VALIDATED,
|
|
97
|
+
certificate_id: undefined,
|
|
98
|
+
}).one();
|
|
99
|
+
render(
|
|
100
|
+
<Wrapper>
|
|
101
|
+
<OrderStateTeacherMessage order={order} />
|
|
102
|
+
</Wrapper>,
|
|
103
|
+
);
|
|
104
|
+
expect(
|
|
105
|
+
screen.getByText(intl.formatMessage(messages.statusOnGoing), {
|
|
106
|
+
exact: false,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should display message for validated order that have a generated certificate', () => {
|
|
112
|
+
const order = CredentialOrderFactory({
|
|
113
|
+
state: OrderState.VALIDATED,
|
|
114
|
+
certificate_id: 'FAKE_CERTIFICATE_ID',
|
|
115
|
+
}).one();
|
|
116
|
+
render(
|
|
117
|
+
<Wrapper>
|
|
118
|
+
<OrderStateTeacherMessage order={order} />
|
|
119
|
+
</Wrapper>,
|
|
120
|
+
);
|
|
121
|
+
expect(
|
|
122
|
+
screen.getByText(intl.formatMessage(messages.statusCompleted), {
|
|
123
|
+
exact: false,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
import OrderStateMessage, { OrderStateMessageBaseProps } from '../OrderStateMessage';
|
|
3
|
+
|
|
4
|
+
export const messages = defineMessages({
|
|
5
|
+
statusDraft: {
|
|
6
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusDraft',
|
|
7
|
+
description: 'Status shown on the dashboard order item when order is draft.',
|
|
8
|
+
defaultMessage: 'Pending',
|
|
9
|
+
},
|
|
10
|
+
statusSubmitted: {
|
|
11
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusSubmitted',
|
|
12
|
+
description: 'Status shown on the dashboard order item when order is submitted.',
|
|
13
|
+
defaultMessage: 'Pending',
|
|
14
|
+
},
|
|
15
|
+
statusPending: {
|
|
16
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusPending',
|
|
17
|
+
description: 'Status shown on the dashboard order item when order is pending.',
|
|
18
|
+
defaultMessage: 'Pending',
|
|
19
|
+
},
|
|
20
|
+
statusOnGoing: {
|
|
21
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusOnGoing',
|
|
22
|
+
description:
|
|
23
|
+
'Status shown on the dashboard order item when order is validated with no certificate',
|
|
24
|
+
defaultMessage: 'Enrolled',
|
|
25
|
+
},
|
|
26
|
+
statusCompleted: {
|
|
27
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusCompleted',
|
|
28
|
+
description:
|
|
29
|
+
'Status shown on the dashboard order item when order is validated with certificate',
|
|
30
|
+
defaultMessage: 'Certified',
|
|
31
|
+
},
|
|
32
|
+
statusWaitingSignature: {
|
|
33
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusWaitingSignature',
|
|
34
|
+
description:
|
|
35
|
+
"Status shown on the dashboard order item when order is validated with contract's learner signature missing.",
|
|
36
|
+
defaultMessage: "Pending for learner's signature",
|
|
37
|
+
},
|
|
38
|
+
statusWaitingCounterSignature: {
|
|
39
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusWaitingCounterSignature',
|
|
40
|
+
description:
|
|
41
|
+
"Status shown on the dashboard order item when order is validated with contract's organization signature missing.",
|
|
42
|
+
defaultMessage: 'To be signed',
|
|
43
|
+
},
|
|
44
|
+
statusCanceled: {
|
|
45
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusCanceled',
|
|
46
|
+
description: 'Status shown on the dashboard order item when order is canceled',
|
|
47
|
+
defaultMessage: 'Canceled',
|
|
48
|
+
},
|
|
49
|
+
statusOther: {
|
|
50
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusOther',
|
|
51
|
+
description: 'Status shown on the dashboard order item when order status is unknown',
|
|
52
|
+
defaultMessage: '{state}',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const OrderStateTeacherMessage = (props: OrderStateMessageBaseProps) => {
|
|
57
|
+
return <OrderStateMessage {...props} messages={messages} />;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default OrderStateTeacherMessage;
|