richie-education 3.1.3-dev3 → 3.1.3-dev30
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/.storybook/__mocks__/utils/context.ts +4 -0
- package/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
- package/js/components/CourseGlimpse/index.spec.tsx +18 -0
- package/js/components/CourseGlimpse/index.stories.tsx +75 -4
- package/js/components/CourseGlimpse/index.tsx +4 -0
- package/js/components/CourseGlimpse/utils.ts +35 -30
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +13 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
- package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +171 -29
- package/js/components/SaleTunnel/index.stories.tsx +17 -3
- package/js/components/SaleTunnel/index.tsx +2 -2
- package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
- package/js/hooks/useContractArchive/index.ts +3 -3
- package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -8
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
- package/js/hooks/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
- package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
- package/js/types/Course.ts +4 -0
- package/js/types/Joanie.ts +36 -29
- package/js/types/index.ts +6 -2
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +19 -25
- package/js/utils/test/factories/richie.ts +10 -2
- package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +19 -34
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +35 -8
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +54 -8
- package/package.json +1 -1
- package/scss/objects/_course_glimpses.scss +16 -0
- package/js/hooks/useCourseProductRelation/index.ts +0 -44
|
@@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
4
4
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
5
5
|
import {
|
|
6
6
|
CourseListItemFactory,
|
|
7
|
-
|
|
7
|
+
OfferingFactory,
|
|
8
8
|
OrganizationFactory,
|
|
9
9
|
} from 'utils/test/factories/joanie';
|
|
10
10
|
import { expectNoSpinner } from 'utils/test/expectSpinner';
|
|
@@ -59,8 +59,8 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
|
|
|
59
59
|
mockPaginatedResponse(CourseListItemFactory().many(15), 15, false),
|
|
60
60
|
);
|
|
61
61
|
fetchMock.get(
|
|
62
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
63
|
-
mockPaginatedResponse(
|
|
62
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/?product_type=credential&page=1&page_size=${perPage}`,
|
|
63
|
+
mockPaginatedResponse(OfferingFactory().many(15), 15, false),
|
|
64
64
|
);
|
|
65
65
|
fetchMock.get(
|
|
66
66
|
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?signature_state=half_signed&page=1`,
|
|
@@ -76,7 +76,7 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
|
|
|
76
76
|
await expectNoSpinner('Loading courses...');
|
|
77
77
|
|
|
78
78
|
nbApiCalls += 1; // course api call
|
|
79
|
-
nbApiCalls += 1; //
|
|
79
|
+
nbApiCalls += 1; // offerings api call
|
|
80
80
|
nbApiCalls += 1; // contracts api call
|
|
81
81
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
82
82
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
@@ -84,7 +84,7 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
|
|
|
84
84
|
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/courses/?has_listed_course_runs=true&page=1&page_size=${perPage}`,
|
|
85
85
|
);
|
|
86
86
|
expect(calledUrls).toContain(
|
|
87
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
87
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/?product_type=credential&page=1&page_size=${perPage}`,
|
|
88
88
|
);
|
|
89
89
|
await expectNoSpinner('Loading organization...');
|
|
90
90
|
|
|
@@ -117,8 +117,8 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
|
|
|
117
117
|
mockPaginatedResponse(CourseListItemFactory().many(15), 15, false),
|
|
118
118
|
);
|
|
119
119
|
fetchMock.get(
|
|
120
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
121
|
-
mockPaginatedResponse(
|
|
120
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/?product_type=credential&page=1&page_size=${perPage}`,
|
|
121
|
+
mockPaginatedResponse(OfferingFactory().many(15), 15, false),
|
|
122
122
|
);
|
|
123
123
|
|
|
124
124
|
render(<TeacherDashboardOrganizationCourseLoader />, {
|
|
@@ -136,22 +136,22 @@ describe('components/TeacherDashboardOrganizationCourseLoader', () => {
|
|
|
136
136
|
mockPaginatedResponse(CourseListItemFactory().many(5), 5, false),
|
|
137
137
|
);
|
|
138
138
|
fetchMock.get(
|
|
139
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
140
|
-
mockPaginatedResponse(
|
|
139
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/?query=text+query&product_type=credential&page=1&page_size=${perPage}`,
|
|
140
|
+
mockPaginatedResponse(OfferingFactory().many(5), 5, false),
|
|
141
141
|
);
|
|
142
142
|
const user = userEvent.setup();
|
|
143
143
|
await user.type(screen.getByRole('textbox', { name: /Search/ }), 'text query');
|
|
144
144
|
await user.click(screen.getByRole('button', { name: /Search/ }));
|
|
145
145
|
|
|
146
146
|
nbApiCalls = 1; // course api call
|
|
147
|
-
nbApiCalls += 1; //
|
|
147
|
+
nbApiCalls += 1; // offerings api call
|
|
148
148
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
149
149
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
150
150
|
expect(calledUrls).toContain(
|
|
151
151
|
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/courses/?query=text+query&has_listed_course_runs=true&page=1&page_size=${perPage}`,
|
|
152
152
|
);
|
|
153
153
|
expect(calledUrls).toContain(
|
|
154
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
154
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/?query=text+query&product_type=credential&page=1&page_size=${perPage}`,
|
|
155
155
|
);
|
|
156
156
|
|
|
157
157
|
await waitFor(() => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { FormattedMessage, defineMessages } from 'react-intl';
|
|
2
2
|
import { useParams } from 'react-router';
|
|
3
3
|
|
|
4
|
+
import { useOffering } from 'hooks/useOffering';
|
|
4
5
|
import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
|
|
5
6
|
import { TeacherDashboardCourseSidebar } from 'widgets/Dashboard/components/TeacherDashboardCourseSidebar';
|
|
6
7
|
import { Spinner } from 'components/Spinner';
|
|
7
|
-
import { useCourseProductRelation } from 'hooks/useCourseProductRelation';
|
|
8
8
|
import { useBreadcrumbsPlaceholders } from 'hooks/useBreadcrumbsPlaceholders';
|
|
9
9
|
import { TeacherDashboardTraining } from '.';
|
|
10
10
|
|
|
@@ -22,17 +22,17 @@ const messages = defineMessages({
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
export const TeacherDashboardTrainingLoader = () => {
|
|
25
|
-
const {
|
|
26
|
-
|
|
25
|
+
const { offeringId, organizationId } = useParams<{
|
|
26
|
+
offeringId: string;
|
|
27
27
|
organizationId?: string;
|
|
28
28
|
}>();
|
|
29
29
|
|
|
30
30
|
const {
|
|
31
|
-
item:
|
|
31
|
+
item: offering,
|
|
32
32
|
states: { fetching },
|
|
33
|
-
} =
|
|
33
|
+
} = useOffering(offeringId, { organization_id: organizationId });
|
|
34
34
|
useBreadcrumbsPlaceholders({
|
|
35
|
-
courseTitle:
|
|
35
|
+
courseTitle: offering?.product.title ?? '',
|
|
36
36
|
});
|
|
37
37
|
return (
|
|
38
38
|
<DashboardLayout sidebar={<TeacherDashboardCourseSidebar />}>
|
|
@@ -48,7 +48,7 @@ export const TeacherDashboardTrainingLoader = () => {
|
|
|
48
48
|
</span>
|
|
49
49
|
</Spinner>
|
|
50
50
|
) : (
|
|
51
|
-
<TeacherDashboardTraining
|
|
51
|
+
<TeacherDashboardTraining offering={offering} />
|
|
52
52
|
)}
|
|
53
53
|
</DashboardLayout>
|
|
54
54
|
);
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
UserFactory,
|
|
12
12
|
} from 'utils/test/factories/richie';
|
|
13
13
|
import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
|
|
14
|
-
import {
|
|
14
|
+
import { OfferingFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
15
15
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
16
16
|
import { expectNoSpinner } from 'utils/test/expectSpinner';
|
|
17
17
|
import { DashboardBreadcrumbsProvider } from 'widgets/Dashboard/contexts/DashboardBreadcrumbsContext';
|
|
@@ -46,15 +46,9 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it('should render TeacherDashboardTrainingLoader page', async () => {
|
|
49
|
-
const
|
|
50
|
-
fetchMock.get(
|
|
51
|
-
|
|
52
|
-
[],
|
|
53
|
-
);
|
|
54
|
-
fetchMock.get(
|
|
55
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
|
|
56
|
-
courseProductRelation,
|
|
57
|
-
);
|
|
49
|
+
const offering = OfferingFactory().one();
|
|
50
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/?offering_id=${offering.id}`, []);
|
|
51
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/`, offering);
|
|
58
52
|
|
|
59
53
|
const user = UserFactory().one();
|
|
60
54
|
render(
|
|
@@ -67,12 +61,12 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
67
61
|
router={createMemoryRouter(
|
|
68
62
|
[
|
|
69
63
|
{
|
|
70
|
-
path: ':
|
|
64
|
+
path: ':offeringId',
|
|
71
65
|
element: <TeacherDashboardTrainingLoader />,
|
|
72
66
|
},
|
|
73
67
|
],
|
|
74
68
|
{
|
|
75
|
-
initialEntries: [`/${
|
|
69
|
+
initialEntries: [`/${offering.id}`],
|
|
76
70
|
},
|
|
77
71
|
)}
|
|
78
72
|
/>
|
|
@@ -88,12 +82,10 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
88
82
|
await expectNoSpinner('Loading course...');
|
|
89
83
|
|
|
90
84
|
nbApiCalls += 1; // organizations api call
|
|
91
|
-
nbApiCalls += 1; //
|
|
85
|
+
nbApiCalls += 1; // offerings api call
|
|
92
86
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
93
87
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
94
|
-
expect(calledUrls).toContain(
|
|
95
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
|
|
96
|
-
);
|
|
88
|
+
expect(calledUrls).toContain(`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/`);
|
|
97
89
|
|
|
98
90
|
// main titles
|
|
99
91
|
expect(
|
|
@@ -103,27 +95,27 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
103
95
|
).toBeInTheDocument();
|
|
104
96
|
|
|
105
97
|
expect(
|
|
106
|
-
screen.getAllByRole('heading', { name: capitalize(
|
|
98
|
+
screen.getAllByRole('heading', { name: capitalize(offering.product.title) }),
|
|
107
99
|
).toHaveLength(2);
|
|
108
100
|
|
|
109
|
-
const nbCourseRun =
|
|
101
|
+
const nbCourseRun = offering.product.target_courses.reduce(
|
|
110
102
|
(acc, course) => acc + course.course_runs.length,
|
|
111
103
|
0,
|
|
112
104
|
);
|
|
113
105
|
expect(screen.getAllByRole('link', { name: 'Go to course area' })).toHaveLength(nbCourseRun);
|
|
114
106
|
});
|
|
115
107
|
|
|
116
|
-
it('should fetch
|
|
108
|
+
it('should fetch offering with organization id if there is one in the path', async () => {
|
|
117
109
|
const organization = OrganizationFactory().one();
|
|
118
|
-
const
|
|
110
|
+
const offering = OfferingFactory({
|
|
119
111
|
organizations: [organization],
|
|
120
112
|
}).one();
|
|
121
113
|
fetchMock.get(
|
|
122
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
123
|
-
|
|
114
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/${offering.id}/`,
|
|
115
|
+
offering,
|
|
124
116
|
);
|
|
125
117
|
fetchMock.get(
|
|
126
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?
|
|
118
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?offering_id=${offering.id}&signature_state=half_signed&page=1&page_size=25`,
|
|
127
119
|
[],
|
|
128
120
|
);
|
|
129
121
|
|
|
@@ -138,12 +130,12 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
138
130
|
router={createMemoryRouter(
|
|
139
131
|
[
|
|
140
132
|
{
|
|
141
|
-
path: '/:organizationId/:
|
|
133
|
+
path: '/:organizationId/:offeringId',
|
|
142
134
|
element: <TeacherDashboardTrainingLoader />,
|
|
143
135
|
},
|
|
144
136
|
],
|
|
145
137
|
{
|
|
146
|
-
initialEntries: [`/${organization.id}/${
|
|
138
|
+
initialEntries: [`/${organization.id}/${offering.id}`],
|
|
147
139
|
},
|
|
148
140
|
)}
|
|
149
141
|
/>
|
|
@@ -159,11 +151,11 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
159
151
|
await expectNoSpinner('Loading course...');
|
|
160
152
|
|
|
161
153
|
nbApiCalls += 1; // contracts api call
|
|
162
|
-
nbApiCalls += 1; //
|
|
154
|
+
nbApiCalls += 1; // offerings api call
|
|
163
155
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
164
156
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
165
157
|
expect(calledUrls).toContain(
|
|
166
|
-
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/
|
|
158
|
+
`https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/${offering.id}/`,
|
|
167
159
|
);
|
|
168
160
|
|
|
169
161
|
// main titles
|
|
@@ -174,10 +166,10 @@ describe('components/TeacherDashboardTrainingLoader', () => {
|
|
|
174
166
|
).toBeInTheDocument();
|
|
175
167
|
|
|
176
168
|
expect(
|
|
177
|
-
screen.getAllByRole('heading', { name: capitalize(
|
|
169
|
+
screen.getAllByRole('heading', { name: capitalize(offering.product.title) }),
|
|
178
170
|
).toHaveLength(2);
|
|
179
171
|
|
|
180
|
-
const nbCourseRun =
|
|
172
|
+
const nbCourseRun = offering.product.target_courses.reduce(
|
|
181
173
|
(acc, course) => acc + course.course_runs.length,
|
|
182
174
|
0,
|
|
183
175
|
);
|
|
@@ -6,37 +6,33 @@ import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
|
|
|
6
6
|
import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard';
|
|
7
7
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
8
8
|
import Banner, { BannerType } from 'components/Banner';
|
|
9
|
-
import {
|
|
9
|
+
import { Offering } from 'types/Joanie';
|
|
10
10
|
|
|
11
11
|
const messages = defineMessages({
|
|
12
|
-
|
|
12
|
+
errorNoOffering: {
|
|
13
13
|
defaultMessage: "This product doesn't exist",
|
|
14
|
-
description: 'Message displayed when requested
|
|
15
|
-
id: 'components.TeacherDashboardTraining.
|
|
14
|
+
description: 'Message displayed when requested offering is not found',
|
|
15
|
+
id: 'components.TeacherDashboardTraining.errorNoOffering',
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
interface TeacherDashboardTrainingProps {
|
|
20
|
-
|
|
20
|
+
offering: Offering;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const TeacherDashboardTraining = ({
|
|
24
|
-
courseProductRelation,
|
|
25
|
-
}: TeacherDashboardTrainingProps) => {
|
|
23
|
+
export const TeacherDashboardTraining = ({ offering }: TeacherDashboardTrainingProps) => {
|
|
26
24
|
const intl = useIntl();
|
|
27
|
-
return
|
|
25
|
+
return offering ? (
|
|
28
26
|
<div className="teacher-course-page">
|
|
29
27
|
<DashboardCard
|
|
30
28
|
className="icon-arrow-right-rounded"
|
|
31
29
|
header={
|
|
32
30
|
<div>
|
|
33
31
|
<div className="dashboard__title_container--small">
|
|
34
|
-
<h2 className="dashboard__title--large">
|
|
35
|
-
{capitalize(courseProductRelation.product.title)}
|
|
36
|
-
</h2>
|
|
32
|
+
<h2 className="dashboard__title--large">{capitalize(offering.product.title)}</h2>
|
|
37
33
|
</div>
|
|
38
|
-
{
|
|
39
|
-
<div className="dashboard__quote">{
|
|
34
|
+
{offering.product.description && (
|
|
35
|
+
<div className="dashboard__quote">{offering.product.description}</div>
|
|
40
36
|
)}
|
|
41
37
|
</div>
|
|
42
38
|
}
|
|
@@ -44,7 +40,7 @@ export const TeacherDashboardTraining = ({
|
|
|
44
40
|
fullWidth
|
|
45
41
|
/>
|
|
46
42
|
<DashboardLayout.NestedSection>
|
|
47
|
-
{
|
|
43
|
+
{offering.product.target_courses.map((course) => (
|
|
48
44
|
<DashboardLayout.Section key={`course_target_${course.code}`}>
|
|
49
45
|
<DashboardCard
|
|
50
46
|
className="icon-arrow-right-rounded"
|
|
@@ -67,7 +63,7 @@ export const TeacherDashboardTraining = ({
|
|
|
67
63
|
</div>
|
|
68
64
|
) : (
|
|
69
65
|
<Banner
|
|
70
|
-
message={intl.formatMessage(messages.
|
|
66
|
+
message={intl.formatMessage(messages.errorNoOffering)}
|
|
71
67
|
type={BannerType.ERROR}
|
|
72
68
|
rounded
|
|
73
69
|
/>
|
package/js/types/Course.ts
CHANGED
|
@@ -47,6 +47,10 @@ export interface Course extends Resource {
|
|
|
47
47
|
certificate_price: Nullable<number>;
|
|
48
48
|
price: Nullable<number>;
|
|
49
49
|
price_currency: string;
|
|
50
|
+
discounted_price: Nullable<number>;
|
|
51
|
+
discount: Nullable<string>;
|
|
52
|
+
certificate_discounted_price: Nullable<number>;
|
|
53
|
+
certificate_discount: Nullable<string>;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export function isRichieCourse(course: Course | JoanieCourse): course is Course {
|
package/js/types/Joanie.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface Organization {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface OrganizationResourceQuery extends ResourcesQuery {
|
|
41
|
-
|
|
41
|
+
offering_id?: Offering['id'];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface ContractDefinition {
|
|
@@ -149,6 +149,10 @@ export interface Product {
|
|
|
149
149
|
state: CourseState;
|
|
150
150
|
instructions: Nullable<string>;
|
|
151
151
|
contract_definition?: ContractDefinition;
|
|
152
|
+
discounted_price: Nullable<number>;
|
|
153
|
+
discount: Nullable<string>;
|
|
154
|
+
certificate_discounted_price: Nullable<number>;
|
|
155
|
+
certificate_discount: Nullable<string>;
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
export interface CredentialProduct extends Product {
|
|
@@ -174,7 +178,7 @@ export interface DefinitionResourcesProduct {
|
|
|
174
178
|
contract_definition_id: Nullable<ContractDefinition['id']>;
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
export interface
|
|
181
|
+
export interface OfferingLight {
|
|
178
182
|
id: string;
|
|
179
183
|
course: CourseLight;
|
|
180
184
|
organizations: Organization[];
|
|
@@ -182,13 +186,25 @@ export interface CourseProductRelationLight {
|
|
|
182
186
|
created_on: string;
|
|
183
187
|
}
|
|
184
188
|
|
|
185
|
-
export interface
|
|
186
|
-
|
|
189
|
+
export interface OfferingRule {
|
|
190
|
+
discounted_price: Nullable<number>;
|
|
191
|
+
discount_rate: Nullable<number>;
|
|
192
|
+
discount_amount: Nullable<number>;
|
|
193
|
+
discount_start: Nullable<string>;
|
|
194
|
+
discount_end: Nullable<string>;
|
|
195
|
+
description: Nullable<string>;
|
|
196
|
+
nb_available_seats: Nullable<number>;
|
|
197
|
+
has_seat_limit: boolean;
|
|
198
|
+
has_seats_left: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface Offering extends OfferingLight {
|
|
187
202
|
is_withdrawable: boolean;
|
|
203
|
+
rules: OfferingRule;
|
|
188
204
|
}
|
|
189
|
-
export function
|
|
190
|
-
entity: CourseListItem |
|
|
191
|
-
): entity is
|
|
205
|
+
export function isOffering(
|
|
206
|
+
entity: CourseListItem | OfferingLight | RichieCourse,
|
|
207
|
+
): entity is OfferingLight {
|
|
192
208
|
return 'course' in entity && 'product' in entity;
|
|
193
209
|
}
|
|
194
210
|
|
|
@@ -233,7 +249,7 @@ export interface Enrollment {
|
|
|
233
249
|
was_created_by_order: boolean;
|
|
234
250
|
created_on: string;
|
|
235
251
|
orders: OrderEnrollment[];
|
|
236
|
-
|
|
252
|
+
offerings: Offering[];
|
|
237
253
|
certificate_id: Nullable<string>;
|
|
238
254
|
}
|
|
239
255
|
export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj is Enrollment => {
|
|
@@ -247,7 +263,7 @@ export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj
|
|
|
247
263
|
'was_created_by_order' in obj &&
|
|
248
264
|
'created_on' in obj &&
|
|
249
265
|
'orders' in obj &&
|
|
250
|
-
'
|
|
266
|
+
'offerings' in obj &&
|
|
251
267
|
'certificate_id' in obj
|
|
252
268
|
);
|
|
253
269
|
};
|
|
@@ -325,7 +341,6 @@ export interface Order {
|
|
|
325
341
|
enrollment: Nullable<EnrollmentLight>;
|
|
326
342
|
organization_id: Organization['id'];
|
|
327
343
|
organization: Organization;
|
|
328
|
-
order_group_id?: OrderGroup['id'];
|
|
329
344
|
payment_schedule?: PaymentSchedule;
|
|
330
345
|
credit_card_id?: CreditCard['id'];
|
|
331
346
|
}
|
|
@@ -398,18 +413,11 @@ export interface NestedCourseOrder {
|
|
|
398
413
|
|
|
399
414
|
export interface CourseOrderResourceQuery extends PaginatedResourceQuery {
|
|
400
415
|
course_id?: CourseListItem['id'];
|
|
401
|
-
|
|
416
|
+
offering_id?: Offering['id'];
|
|
402
417
|
organization_id?: Organization['id'];
|
|
403
418
|
product_id?: Product['id'];
|
|
404
419
|
}
|
|
405
420
|
|
|
406
|
-
export interface OrderGroup {
|
|
407
|
-
id: string;
|
|
408
|
-
is_active: boolean;
|
|
409
|
-
nb_seats: number;
|
|
410
|
-
nb_available_seats: number;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
421
|
export enum CreditCardBrand {
|
|
414
422
|
MASTERCARD = 'mastercard',
|
|
415
423
|
MAESTRO = 'maestro',
|
|
@@ -481,7 +489,6 @@ export interface AddressCreationPayload extends Omit<Address, 'id' | 'is_main'>
|
|
|
481
489
|
|
|
482
490
|
interface AbstractOrderProductCreationPayload {
|
|
483
491
|
product_id: Product['id'];
|
|
484
|
-
order_group_id?: OrderGroup['id'];
|
|
485
492
|
billing_address: Omit<Address, 'id' | 'is_main'>;
|
|
486
493
|
has_waived_withdrawal_right: boolean;
|
|
487
494
|
}
|
|
@@ -541,8 +548,8 @@ export interface CourseProductQueryFilters extends ResourcesQuery {
|
|
|
541
548
|
id?: Product['id'];
|
|
542
549
|
course_id?: CourseListItem['id'];
|
|
543
550
|
}
|
|
544
|
-
export interface
|
|
545
|
-
id?:
|
|
551
|
+
export interface OfferingQueryFilters extends PaginatedResourceQuery {
|
|
552
|
+
id?: Offering['id'];
|
|
546
553
|
organization_id?: Organization['id'];
|
|
547
554
|
product_type?: ProductType;
|
|
548
555
|
query?: string;
|
|
@@ -555,7 +562,7 @@ export enum ContractState {
|
|
|
555
562
|
}
|
|
556
563
|
export interface ContractResourceQuery extends PaginatedResourceQuery {
|
|
557
564
|
organization_id?: Organization['id'];
|
|
558
|
-
|
|
565
|
+
offering_id?: Offering['id'];
|
|
559
566
|
contract_ids?: Contract['id'][];
|
|
560
567
|
signature_state?: ContractState;
|
|
561
568
|
}
|
|
@@ -563,7 +570,7 @@ export interface ContractResourceQuery extends PaginatedResourceQuery {
|
|
|
563
570
|
export interface OrganizationContractSignatureLinksFilters {
|
|
564
571
|
contracts_ids?: string[];
|
|
565
572
|
organization_id: Organization['id'];
|
|
566
|
-
|
|
573
|
+
offering_ids?: Offering['id'][];
|
|
567
574
|
}
|
|
568
575
|
|
|
569
576
|
export interface ContractInvitationLinkResponse {
|
|
@@ -657,10 +664,10 @@ interface APIUser {
|
|
|
657
664
|
check: (id: string) => Promise<Response>;
|
|
658
665
|
create: ({
|
|
659
666
|
organization_id,
|
|
660
|
-
|
|
667
|
+
offering_id,
|
|
661
668
|
}: {
|
|
662
669
|
organization_id?: Organization['id'];
|
|
663
|
-
|
|
670
|
+
offering_id?: Offering['id'];
|
|
664
671
|
}) => Promise<{ url: string }>;
|
|
665
672
|
get: (id: string) => Promise<File>;
|
|
666
673
|
};
|
|
@@ -676,7 +683,7 @@ export interface API {
|
|
|
676
683
|
? Promise<Nullable<CourseListItem>>
|
|
677
684
|
: Promise<PaginatedResponse<CourseListItem>>;
|
|
678
685
|
products: {
|
|
679
|
-
get(filters?: CourseProductQueryFilters): Promise<Nullable<
|
|
686
|
+
get(filters?: CourseProductQueryFilters): Promise<Nullable<Offering>>;
|
|
680
687
|
paymentSchedule: {
|
|
681
688
|
get(filters?: CourseProductQueryFilters): Promise<Nullable<PaymentSchedule>>;
|
|
682
689
|
};
|
|
@@ -709,12 +716,12 @@ export interface API {
|
|
|
709
716
|
filters?: CourseRunFilters,
|
|
710
717
|
): CourseRunFilters extends { id: string } ? Promise<Nullable<CourseRun>> : Promise<CourseRun>;
|
|
711
718
|
};
|
|
712
|
-
|
|
719
|
+
offerings: {
|
|
713
720
|
get<Filters extends PaginatedResourceQuery = PaginatedResourceQuery>(
|
|
714
721
|
filters?: Filters,
|
|
715
722
|
): Filters extends { id: string }
|
|
716
|
-
? Promise<Nullable<
|
|
717
|
-
: Promise<PaginatedResponse<
|
|
723
|
+
? Promise<Nullable<Offering>>
|
|
724
|
+
: Promise<PaginatedResponse<OfferingLight>>;
|
|
718
725
|
};
|
|
719
726
|
contractDefinitions: {
|
|
720
727
|
previewTemplate(id: string): Promise<File>;
|
package/js/types/index.ts
CHANGED
|
@@ -35,11 +35,15 @@ export interface CourseRun {
|
|
|
35
35
|
title?: string;
|
|
36
36
|
snapshot?: string;
|
|
37
37
|
display_mode: CourseRunDisplayMode;
|
|
38
|
-
price?: number
|
|
38
|
+
price?: Nullable<number>;
|
|
39
39
|
price_currency?: string;
|
|
40
40
|
offer?: string;
|
|
41
|
-
certificate_price?: number
|
|
41
|
+
certificate_price?: Nullable<number>;
|
|
42
42
|
certificate_offer?: string;
|
|
43
|
+
discounted_price: Nullable<number>;
|
|
44
|
+
discount: Nullable<string>;
|
|
45
|
+
certificate_discounted_price: Nullable<number>;
|
|
46
|
+
certificate_discount: Nullable<string>;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export enum Priority {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IntlShape } from 'react-intl';
|
|
2
|
-
import {
|
|
2
|
+
import { Product, TargetCourse } from 'types/Joanie';
|
|
3
3
|
import { Maybe } from 'types/utils';
|
|
4
4
|
import { IntlHelper } from 'utils/IntlHelper';
|
|
5
5
|
import * as Joanie from 'types/Joanie';
|
|
@@ -44,10 +44,6 @@ export class ProductHelper {
|
|
|
44
44
|
return IntlHelper.getLocalizedLanguages(uniqueLanguages, intl);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
static getActiveOrderGroups(courseProductRelation: CourseProductRelation) {
|
|
48
|
-
return courseProductRelation.order_groups?.filter((orderGroup) => orderGroup.is_active);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
static hasRemainingSeats(product: Maybe<Product>) {
|
|
52
48
|
if (!product) return false;
|
|
53
49
|
return typeof product?.remaining_order_count !== 'number' || product.remaining_order_count > 0;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
CourseLight,
|
|
13
13
|
CourseListItem,
|
|
14
14
|
CourseProduct,
|
|
15
|
-
|
|
15
|
+
Offering,
|
|
16
16
|
CourseRun,
|
|
17
17
|
CredentialOrder,
|
|
18
18
|
CredentialProduct,
|
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
NestedCredentialOrder,
|
|
29
29
|
Order,
|
|
30
30
|
OrderEnrollment,
|
|
31
|
-
OrderGroup,
|
|
32
31
|
PaymentInstallment,
|
|
33
32
|
OrderLite,
|
|
34
33
|
OrderState,
|
|
@@ -90,7 +89,7 @@ export const EnrollmentFactory = factory((): Enrollment => {
|
|
|
90
89
|
id: faker.string.uuid(),
|
|
91
90
|
course_run: CourseRunWithCourseFactory().one(),
|
|
92
91
|
is_active: true,
|
|
93
|
-
|
|
92
|
+
offerings: OfferingFactory().many(1),
|
|
94
93
|
state: EnrollmentState.SET,
|
|
95
94
|
was_created_by_order: false,
|
|
96
95
|
created_on: faker.date.past({ years: 1 }).toISOString(),
|
|
@@ -197,7 +196,7 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
|
|
|
197
196
|
created_on: faker.date.past().toISOString(),
|
|
198
197
|
title: FactoryHelper.sequence((counter) => `Certificate Product ${counter}`),
|
|
199
198
|
type: ProductType.CREDENTIAL,
|
|
200
|
-
price: faker.number.int(),
|
|
199
|
+
price: faker.number.int({ min: 1, max: 1000, multipleOf: 10 }),
|
|
201
200
|
price_currency: faker.finance.currencyCode(),
|
|
202
201
|
call_to_action: faker.lorem.words(3),
|
|
203
202
|
certificate_definition: CertificationDefinitionFactory().one(),
|
|
@@ -206,6 +205,10 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
|
|
|
206
205
|
remaining_order_count: faker.number.int({ min: 1, max: 100 }),
|
|
207
206
|
state: CourseStateFactory().one(),
|
|
208
207
|
instructions: null,
|
|
208
|
+
discounted_price: null,
|
|
209
|
+
discount: null,
|
|
210
|
+
certificate_discounted_price: null,
|
|
211
|
+
certificate_discount: null,
|
|
209
212
|
};
|
|
210
213
|
});
|
|
211
214
|
|
|
@@ -294,25 +297,6 @@ export const CourseLightFactory = factory((): CourseLight => {
|
|
|
294
297
|
};
|
|
295
298
|
});
|
|
296
299
|
|
|
297
|
-
export const OrderGroupFactory = factory((): OrderGroup => {
|
|
298
|
-
const seats = faker.number.int({ min: 5, max: 100 });
|
|
299
|
-
return {
|
|
300
|
-
id: faker.string.uuid(),
|
|
301
|
-
is_active: true,
|
|
302
|
-
nb_seats: seats,
|
|
303
|
-
nb_available_seats: faker.number.int({ min: 2, max: seats }),
|
|
304
|
-
};
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
export const OrderGroupFullFactory = factory((): OrderGroup => {
|
|
308
|
-
return {
|
|
309
|
-
id: faker.string.uuid(),
|
|
310
|
-
is_active: true,
|
|
311
|
-
nb_seats: faker.number.int({ min: 5, max: 100 }),
|
|
312
|
-
nb_available_seats: 0,
|
|
313
|
-
};
|
|
314
|
-
});
|
|
315
|
-
|
|
316
300
|
export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
|
|
317
301
|
return {
|
|
318
302
|
id: faker.string.uuid(),
|
|
@@ -329,15 +313,25 @@ export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
|
|
|
329
313
|
};
|
|
330
314
|
});
|
|
331
315
|
|
|
332
|
-
export const
|
|
316
|
+
export const OfferingFactory = factory((): Offering => {
|
|
333
317
|
return {
|
|
334
318
|
id: faker.string.uuid(),
|
|
335
319
|
created_on: faker.date.past().toISOString(),
|
|
336
320
|
course: CourseFactory().one(),
|
|
337
321
|
product: ProductFactory().one(),
|
|
338
322
|
organizations: OrganizationFactory().many(1),
|
|
339
|
-
order_groups: [],
|
|
340
323
|
is_withdrawable: true,
|
|
324
|
+
rules: {
|
|
325
|
+
discounted_price: null,
|
|
326
|
+
discount_rate: null,
|
|
327
|
+
discount_amount: null,
|
|
328
|
+
discount_start: null,
|
|
329
|
+
discount_end: null,
|
|
330
|
+
description: null,
|
|
331
|
+
nb_available_seats: null,
|
|
332
|
+
has_seat_limit: false,
|
|
333
|
+
has_seats_left: true,
|
|
334
|
+
},
|
|
341
335
|
};
|
|
342
336
|
});
|
|
343
337
|
|