richie-education 2.25.0-b2.dev46 → 2.25.0-b2.dev58
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/TeacherDashboardCourseList/index.tsx +21 -22
- 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/Joanie.ts +18 -3
- package/js/types/index.ts +1 -0
- package/js/utils/search/getSuggestionsSection/index.spec.ts +1 -1
- package/js/utils/test/factories/joanie.ts +1 -0
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +45 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.tsx +16 -5
- package/js/widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment/index.ts +6 -9
- 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
|
|
@@ -53,6 +53,16 @@ const TeacherDashboardCourseList = ({
|
|
|
53
53
|
{titleTranslated && (
|
|
54
54
|
<h2 className="dashboard-course-list__title dashboard__page_title">{titleTranslated}</h2>
|
|
55
55
|
)}
|
|
56
|
+
{courseAndProductList.length > 0 ? (
|
|
57
|
+
<CourseGlimpseList
|
|
58
|
+
courses={getCourseGlimpseListProps(courseAndProductList, intl, organizationId)}
|
|
59
|
+
context={context}
|
|
60
|
+
className="dashboard__course-glimpse-list"
|
|
61
|
+
/>
|
|
62
|
+
) : (
|
|
63
|
+
<FormattedMessage {...messages.emptyList} />
|
|
64
|
+
)}
|
|
65
|
+
|
|
56
66
|
{isLoading && (
|
|
57
67
|
<Spinner aria-labelledby="loading-courses-data">
|
|
58
68
|
<span id="loading-courses-data">
|
|
@@ -60,28 +70,17 @@ const TeacherDashboardCourseList = ({
|
|
|
60
70
|
</span>
|
|
61
71
|
</Spinner>
|
|
62
72
|
)}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
disabled={isLoading}
|
|
75
|
-
ref={loadMoreButtonRef}
|
|
76
|
-
color="tertiary"
|
|
77
|
-
>
|
|
78
|
-
<FormattedMessage {...messages.loadMore} />
|
|
79
|
-
</Button>
|
|
80
|
-
)}
|
|
81
|
-
</>
|
|
82
|
-
) : (
|
|
83
|
-
<FormattedMessage {...messages.emptyList} />
|
|
84
|
-
))}
|
|
73
|
+
|
|
74
|
+
{hasMore && (
|
|
75
|
+
<Button
|
|
76
|
+
onClick={() => next()}
|
|
77
|
+
disabled={isLoading}
|
|
78
|
+
ref={loadMoreButtonRef}
|
|
79
|
+
color="tertiary"
|
|
80
|
+
>
|
|
81
|
+
<FormattedMessage {...messages.loadMore} />
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
85
84
|
</div>
|
|
86
85
|
);
|
|
87
86
|
};
|
|
@@ -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;
|
package/js/types/Joanie.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CourseState } from 'types';
|
|
1
|
+
import type { CourseState, OpenEdXEnrollment } from 'types';
|
|
2
2
|
import type { Maybe, Nullable } from 'types/utils';
|
|
3
3
|
import { Resource, ResourcesQuery } from 'hooks/useResources';
|
|
4
4
|
import { OrderResourcesQuery } from 'hooks/useOrders';
|
|
@@ -190,8 +190,23 @@ export interface Enrollment {
|
|
|
190
190
|
created_on: string;
|
|
191
191
|
orders: OrderEnrollment[];
|
|
192
192
|
product_relations: CourseProductRelation[];
|
|
193
|
-
certificate_id
|
|
194
|
-
}
|
|
193
|
+
certificate_id: Nullable<string>;
|
|
194
|
+
}
|
|
195
|
+
export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj is Enrollment => {
|
|
196
|
+
if (!obj || typeof obj !== 'object') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return (
|
|
200
|
+
'is_active' in obj &&
|
|
201
|
+
'state' in obj &&
|
|
202
|
+
'course_run' in obj &&
|
|
203
|
+
'was_created_by_order' in obj &&
|
|
204
|
+
'created_on' in obj &&
|
|
205
|
+
'orders' in obj &&
|
|
206
|
+
'product_relations' in obj &&
|
|
207
|
+
'certificate_id' in obj
|
|
208
|
+
);
|
|
209
|
+
};
|
|
195
210
|
|
|
196
211
|
export interface EnrollmentLight {
|
|
197
212
|
id: string;
|
package/js/types/index.ts
CHANGED
|
@@ -69,4 +69,5 @@ export interface OpenEdXEnrollment {
|
|
|
69
69
|
* Use an unknown type to make sure we do not depend on any LMS-specific fields
|
|
70
70
|
* on enrollment objects, just use HTTP response codes.
|
|
71
71
|
*/
|
|
72
|
+
// TODO(rlecellier): rename into UnknownEnrollment
|
|
72
73
|
export type Enrollment = unknown;
|
|
@@ -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
|
});
|
package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx
CHANGED
|
@@ -3,7 +3,9 @@ import fetchMock from 'fetch-mock';
|
|
|
3
3
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
4
4
|
import { IntlProvider } from 'react-intl';
|
|
5
5
|
|
|
6
|
+
import { faker } from '@faker-js/faker';
|
|
6
7
|
import { Deferred } from 'utils/test/deferred';
|
|
8
|
+
import { EnrollmentFactory as JoanieEnrollment } from 'utils/test/factories/joanie';
|
|
7
9
|
import {
|
|
8
10
|
CourseRunFactory,
|
|
9
11
|
RichieContextFactory as mockRichieContextFactory,
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
import { SessionProvider } from 'contexts/SessionContext';
|
|
13
15
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
14
16
|
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
17
|
+
import { Priority } from 'types';
|
|
15
18
|
import CourseRunEnrollment from './index';
|
|
16
19
|
|
|
17
20
|
jest.mock('utils/errors/handle');
|
|
@@ -96,10 +99,11 @@ describe('<CourseRunEnrollment /> with joanie backend ', () => {
|
|
|
96
99
|
it('shows an error message when enrollment get request failed', async () => {
|
|
97
100
|
const user = UserFactory().one();
|
|
98
101
|
const courseRun = CourseRunFactory().one();
|
|
99
|
-
|
|
102
|
+
const joanieEnrollmentId = faker.string.uuid();
|
|
103
|
+
courseRun.resource_link = `https://joanie.endpoint/api/v1.0/course-runs/${joanieEnrollmentId}`;
|
|
100
104
|
|
|
101
105
|
fetchMock.get(
|
|
102
|
-
`${endpoint}/api/v1.0/enrollments/?
|
|
106
|
+
`${endpoint}/api/v1.0/enrollments/?course_run_id=${joanieEnrollmentId}`,
|
|
103
107
|
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
104
108
|
);
|
|
105
109
|
|
|
@@ -118,6 +122,45 @@ describe('<CourseRunEnrollment /> with joanie backend ', () => {
|
|
|
118
122
|
await screen.findByText('Enrollment fetching failed');
|
|
119
123
|
});
|
|
120
124
|
|
|
125
|
+
it('shows a link to the course if the user is already enrolled', async () => {
|
|
126
|
+
// Joanie session requests
|
|
127
|
+
let nbApiCalls = 3;
|
|
128
|
+
const user = UserFactory().one();
|
|
129
|
+
const joanieEnrollment = JoanieEnrollment({ is_active: true }).one();
|
|
130
|
+
const courseRun = CourseRunFactory().one();
|
|
131
|
+
courseRun.resource_link = `https://joanie.endpoint/api/v1.0/course-runs/${joanieEnrollment.course_run.id}/`;
|
|
132
|
+
courseRun.state.priority = Priority.ONGOING_OPEN;
|
|
133
|
+
|
|
134
|
+
fetchMock.get(
|
|
135
|
+
`${endpoint}/api/v1.0/enrollments/?course_run_id=${joanieEnrollment.course_run.id}`,
|
|
136
|
+
{
|
|
137
|
+
count: 1,
|
|
138
|
+
next: null,
|
|
139
|
+
previous: null,
|
|
140
|
+
results: [joanieEnrollment],
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
nbApiCalls += 1;
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
render(
|
|
147
|
+
<QueryClientProvider client={createTestQueryClient({ user })}>
|
|
148
|
+
<IntlProvider locale="en">
|
|
149
|
+
<SessionProvider>
|
|
150
|
+
<CourseRunEnrollment courseRun={courseRun} />
|
|
151
|
+
</SessionProvider>
|
|
152
|
+
</IntlProvider>
|
|
153
|
+
</QueryClientProvider>,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const $goToCourseButton = await screen.findByRole('link', { name: 'Go to course' });
|
|
158
|
+
expect($goToCourseButton).toBeInTheDocument();
|
|
159
|
+
expect($goToCourseButton).toHaveAttribute('href', joanieEnrollment.course_run.resource_link);
|
|
160
|
+
expect(screen.getByText('You are enrolled in this course run')).toBeInTheDocument();
|
|
161
|
+
expect(fetchMock.calls()).toHaveLength(nbApiCalls);
|
|
162
|
+
});
|
|
163
|
+
|
|
121
164
|
it('shows an "Unenroll" text and allows the user to unenroll', async () => {
|
|
122
165
|
const user = UserFactory().one();
|
|
123
166
|
const courseRun = CourseRunFactory().one();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useReducer } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
|
|
2
2
|
import { defineMessages, FormattedMessage } from 'react-intl';
|
|
3
3
|
import c from 'classnames';
|
|
4
4
|
import { Button } from '@openfun/cunningham-react';
|
|
@@ -12,6 +12,7 @@ import { HttpError } from 'utils/errors/HttpError';
|
|
|
12
12
|
import useCourseEnrollment from 'widgets/SyllabusCourseRunsList/hooks/useCourseEnrollment';
|
|
13
13
|
import { CourseRunUnenrollButton } from 'widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/CourseRunUnenrollmentButton';
|
|
14
14
|
import useDateRelative from 'hooks/useDateRelative';
|
|
15
|
+
import { isEnrollment as isJoanieEnrollment } from 'types/Joanie';
|
|
15
16
|
|
|
16
17
|
const messages = defineMessages({
|
|
17
18
|
enroll: {
|
|
@@ -181,9 +182,8 @@ const reducer = ({ step, context }: ReducerState, action: ReducerAction): Reduce
|
|
|
181
182
|
|
|
182
183
|
const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
|
|
183
184
|
const { user, login } = useSession();
|
|
184
|
-
const { enrollmentIsActive, setEnrollment, canUnenroll, states } =
|
|
185
|
-
props.courseRun.resource_link
|
|
186
|
-
);
|
|
185
|
+
const { enrollment, enrollmentIsActive, setEnrollment, canUnenroll, states } =
|
|
186
|
+
useCourseEnrollment(props.courseRun.resource_link);
|
|
187
187
|
const startDate = new Date(props.courseRun.start);
|
|
188
188
|
const isStarted = new Date() > startDate;
|
|
189
189
|
const relativeStartDate = useDateRelative(startDate);
|
|
@@ -226,6 +226,16 @@ const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
|
|
|
226
226
|
[courseRun, currentUser, dispatch, enrollmentIsActive],
|
|
227
227
|
);
|
|
228
228
|
|
|
229
|
+
const LmsCourseLink = useMemo(() => {
|
|
230
|
+
if (states.isLoading) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
if (isJoanieEnrollment(enrollment)) {
|
|
234
|
+
return enrollment.course_run.resource_link;
|
|
235
|
+
}
|
|
236
|
+
return courseRun.resource_link;
|
|
237
|
+
}, [courseRun, enrollment]);
|
|
238
|
+
|
|
229
239
|
useEffect(() => {
|
|
230
240
|
dispatch({
|
|
231
241
|
payload: { currentUser: user, isEnrolled: enrollmentIsActive },
|
|
@@ -303,7 +313,8 @@ const CourseRunEnrollment: React.FC<CourseRunEnrollmentProps> = (props) => {
|
|
|
303
313
|
return isStarted ? (
|
|
304
314
|
<div>
|
|
305
315
|
<Button
|
|
306
|
-
href={
|
|
316
|
+
href={LmsCourseLink === null ? '#' : LmsCourseLink}
|
|
317
|
+
disabled={LmsCourseLink === null}
|
|
307
318
|
className="course-run-enrollment__cta"
|
|
308
319
|
fullWidth={true}
|
|
309
320
|
>
|
|
@@ -21,22 +21,18 @@ const useCourseEnrollment = (resourceLink: string) => {
|
|
|
21
21
|
const queryClient = useQueryClient();
|
|
22
22
|
const EnrollmentAPI = EnrollmentApiInterface(resourceLink);
|
|
23
23
|
|
|
24
|
-
const [{ data: enrollment, isError, isLoading }, queryKey] = useSessionQuery(
|
|
24
|
+
const [{ data: enrollment, isError, isLoading: isEnrollmentLoading }, queryKey] = useSessionQuery(
|
|
25
25
|
['enrollment', resourceLink],
|
|
26
26
|
async () => {
|
|
27
27
|
return EnrollmentAPI.get(resourceLink, user!);
|
|
28
28
|
},
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
const [{ data: isActive, refetch: refetchIsActive }] =
|
|
32
|
-
[...queryKey, 'is_active'],
|
|
33
|
-
async () => EnrollmentAPI.isEnrolled(enrollment),
|
|
34
|
-
{
|
|
31
|
+
const [{ data: isActive, refetch: refetchIsActive, isLoading: isActiveLoading }] =
|
|
32
|
+
useSessionQuery([...queryKey, 'is_active'], async () => EnrollmentAPI.isEnrolled(enrollment), {
|
|
35
33
|
// Enrollment is null if it has been fetched
|
|
36
|
-
enabled: !!user && enrollment !== undefined && !
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
|
|
34
|
+
enabled: !!user && enrollment !== undefined && !isEnrollmentLoading,
|
|
35
|
+
});
|
|
40
36
|
const { mutateAsync } = useSessionMutation({
|
|
41
37
|
mutationFn: (activeEnrollment: boolean = true) =>
|
|
42
38
|
EnrollmentAPI.set(resourceLink, user!, enrollment, activeEnrollment),
|
|
@@ -61,6 +57,7 @@ const useCourseEnrollment = (resourceLink: string) => {
|
|
|
61
57
|
errors: {
|
|
62
58
|
get: isError,
|
|
63
59
|
},
|
|
60
|
+
isLoading: isEnrollmentLoading || isActiveLoading,
|
|
64
61
|
},
|
|
65
62
|
};
|
|
66
63
|
};
|
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.dev58",
|
|
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;
|