richie-education 2.25.0-b2.dev142 → 2.25.0-b2.dev145
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/api/joanie.ts +3 -1
- package/js/types/Joanie.ts +11 -3
- package/js/utils/CertificateHelper/index.spec.ts +47 -0
- package/js/utils/CertificateHelper/index.ts +19 -0
- package/js/utils/test/factories/joanie.ts +1 -0
- package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.spec.tsx +31 -16
- package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.tsx +15 -20
- package/package.json +1 -1
package/js/api/joanie.ts
CHANGED
|
@@ -28,7 +28,9 @@ export async function getFileFromResponse(response: Response): Promise<File> {
|
|
|
28
28
|
const dispositionHeader = response.headers.get('Content-Disposition');
|
|
29
29
|
const matches = dispositionHeader?.match(filenameRegex);
|
|
30
30
|
|
|
31
|
-
return new File([await response.blob()], matches ? matches[1] : ''
|
|
31
|
+
return new File([await response.blob()], matches ? matches[1] : '', {
|
|
32
|
+
type: response.headers.get('Content-Type') || '',
|
|
33
|
+
});
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function getResponseBody(response: Response) {
|
package/js/types/Joanie.ts
CHANGED
|
@@ -98,12 +98,20 @@ export interface CertificateDefinition {
|
|
|
98
98
|
description: string;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export
|
|
101
|
+
export type Certificate = {
|
|
102
102
|
id: string;
|
|
103
103
|
issued_on: string;
|
|
104
104
|
certificate_definition: CertificateDefinition;
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
} & (
|
|
106
|
+
| {
|
|
107
|
+
order: NestedCertificateOrder | NestedCredentialOrder;
|
|
108
|
+
enrollment: null;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
enrollment: EnrollmentLight;
|
|
112
|
+
order: null;
|
|
113
|
+
}
|
|
114
|
+
);
|
|
107
115
|
|
|
108
116
|
// - Organization
|
|
109
117
|
export interface OrganizationLight {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CertificateFactory,
|
|
3
|
+
CertificateOrderFactory,
|
|
4
|
+
CourseLightFactory,
|
|
5
|
+
CourseRunFactory,
|
|
6
|
+
CredentialOrderFactory,
|
|
7
|
+
EnrollmentLightFactory,
|
|
8
|
+
} from 'utils/test/factories/joanie';
|
|
9
|
+
import { CertificateHelper } from '.';
|
|
10
|
+
|
|
11
|
+
describe('CertificateHelper', () => {
|
|
12
|
+
it.each([undefined, null])(
|
|
13
|
+
'should return undefined if the certificate is not defined',
|
|
14
|
+
(emptyValue) => {
|
|
15
|
+
expect(CertificateHelper.getCourse(emptyValue)).toBeUndefined();
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
CertificateFactory({
|
|
21
|
+
enrollment: EnrollmentLightFactory({
|
|
22
|
+
course_run: CourseRunFactory({
|
|
23
|
+
course: CourseLightFactory({ title: 'Course 1' }).one(),
|
|
24
|
+
}).one(),
|
|
25
|
+
}).one(),
|
|
26
|
+
order: null,
|
|
27
|
+
}).one(),
|
|
28
|
+
CertificateFactory({
|
|
29
|
+
enrollment: null,
|
|
30
|
+
order: CredentialOrderFactory({
|
|
31
|
+
course: CourseLightFactory({ title: 'Course 1' }).one(),
|
|
32
|
+
}).one(),
|
|
33
|
+
}).one(),
|
|
34
|
+
CertificateFactory({
|
|
35
|
+
enrollment: null,
|
|
36
|
+
order: CertificateOrderFactory({
|
|
37
|
+
enrollment: EnrollmentLightFactory({
|
|
38
|
+
course_run: CourseRunFactory({
|
|
39
|
+
course: CourseLightFactory({ title: 'Course 1' }).one(),
|
|
40
|
+
}).one(),
|
|
41
|
+
}).one(),
|
|
42
|
+
}).one(),
|
|
43
|
+
}).one(),
|
|
44
|
+
])('should return the course from the certificate linked to ', (certificate) => {
|
|
45
|
+
expect(CertificateHelper.getCourse(certificate)?.title).toEqual('Course 1');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Certificate } from 'types/Joanie';
|
|
2
|
+
import { Nullable } from 'types/utils';
|
|
3
|
+
|
|
4
|
+
export class CertificateHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Get the course from a Certificate according to its type.
|
|
7
|
+
* Indeed, a Certificate can be linked to an Order or an Enrollment.
|
|
8
|
+
*/
|
|
9
|
+
static getCourse(certificate?: Nullable<Certificate>) {
|
|
10
|
+
if (!certificate) return undefined;
|
|
11
|
+
|
|
12
|
+
if (certificate.order) {
|
|
13
|
+
if (certificate.order.course) return certificate.order.course;
|
|
14
|
+
else return certificate.order.enrollment.course_run.course;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return certificate.enrollment.course_run.course;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -184,6 +184,7 @@ export const CertificateFactory = factory((): Certificate => {
|
|
|
184
184
|
id: faker.string.uuid(),
|
|
185
185
|
certificate_definition: CertificationDefinitionFactory().one(),
|
|
186
186
|
order: NestedCredentialOrderFactory().one(),
|
|
187
|
+
enrollment: null,
|
|
187
188
|
issued_on: faker.date.past().toISOString(),
|
|
188
189
|
};
|
|
189
190
|
});
|
|
@@ -12,6 +12,7 @@ import { DashboardItemCertificate } from 'widgets/Dashboard/components/Dashboard
|
|
|
12
12
|
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
13
13
|
import {
|
|
14
14
|
CertificateFactory,
|
|
15
|
+
EnrollmentLightFactory,
|
|
15
16
|
NestedCertificateOrderFactory,
|
|
16
17
|
NestedCredentialOrderFactory,
|
|
17
18
|
} from 'utils/test/factories/joanie';
|
|
@@ -27,14 +28,27 @@ jest.mock('utils/context', () => ({
|
|
|
27
28
|
|
|
28
29
|
describe.each([
|
|
29
30
|
{
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
// Link to a credential order
|
|
32
|
+
overrideFactory: () => ({
|
|
33
|
+
order: NestedCredentialOrderFactory().one(),
|
|
34
|
+
enrollment: null,
|
|
35
|
+
}),
|
|
32
36
|
},
|
|
33
37
|
{
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
// Link to a certificate order
|
|
39
|
+
overrideFactory: () => ({
|
|
40
|
+
order: NestedCertificateOrderFactory().one(),
|
|
41
|
+
enrollment: null,
|
|
42
|
+
}),
|
|
36
43
|
},
|
|
37
|
-
|
|
44
|
+
{
|
|
45
|
+
// Link to an enrollment
|
|
46
|
+
overrideFactory: () => ({
|
|
47
|
+
order: null,
|
|
48
|
+
enrollment: EnrollmentLightFactory().one(),
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
])('<DashboardCertificate/> $label', ({ overrideFactory }) => {
|
|
38
52
|
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
39
53
|
return (
|
|
40
54
|
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
@@ -66,21 +80,24 @@ describe.each([
|
|
|
66
80
|
});
|
|
67
81
|
|
|
68
82
|
it('displays a certificate', async () => {
|
|
69
|
-
const certificate: Certificate = CertificateFactory(
|
|
70
|
-
order: OrderFactory().one(),
|
|
71
|
-
}).one();
|
|
83
|
+
const certificate: Certificate = CertificateFactory(overrideFactory()).one();
|
|
72
84
|
render(
|
|
73
85
|
<DashboardItemCertificate certificate={certificate} productType={ProductType.CREDENTIAL} />,
|
|
74
86
|
{ wrapper: Wrapper },
|
|
75
87
|
);
|
|
76
88
|
|
|
77
89
|
await waitFor(() => screen.getByText(certificate.certificate_definition.title));
|
|
78
|
-
const orderCourse =
|
|
79
|
-
label === 'link to a credential order'
|
|
80
|
-
? certificate.order.course!
|
|
81
|
-
: certificate.order.enrollment!.course_run.course;
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
let course;
|
|
92
|
+
if (certificate.enrollment) {
|
|
93
|
+
course = certificate.enrollment.course_run.course;
|
|
94
|
+
} else if (certificate.order!.course) {
|
|
95
|
+
course = certificate.order!.course;
|
|
96
|
+
} else {
|
|
97
|
+
course = certificate.order!.enrollment.course_run.course;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
screen.getByText(course.title);
|
|
84
101
|
screen.getByText(
|
|
85
102
|
'Issued on ' +
|
|
86
103
|
new Intl.DateTimeFormat('en', DEFAULT_DATE_FORMAT).format(new Date(certificate.issued_on)),
|
|
@@ -88,9 +105,7 @@ describe.each([
|
|
|
88
105
|
});
|
|
89
106
|
|
|
90
107
|
it('downloads the certificate', async () => {
|
|
91
|
-
const certificate: Certificate = CertificateFactory(
|
|
92
|
-
order: OrderFactory().one(),
|
|
93
|
-
}).one();
|
|
108
|
+
const certificate: Certificate = CertificateFactory(overrideFactory()).one();
|
|
94
109
|
|
|
95
110
|
fetchMock.get(`https://joanie.test/api/v1.0/certificates/${certificate.id}/download/`, () => ({
|
|
96
111
|
status: HttpStatusCode.OK,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
1
2
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
2
3
|
import { Certificate, CertificateDefinition, CourseLight, ProductType } from 'types/Joanie';
|
|
3
4
|
import {
|
|
@@ -6,6 +7,7 @@ import {
|
|
|
6
7
|
} from 'widgets/Dashboard/components/DashboardItem/index';
|
|
7
8
|
import { Maybe } from 'types/utils';
|
|
8
9
|
import DownloadCertificateButton from 'components/DownloadCertificateButton';
|
|
10
|
+
import { CertificateHelper } from 'utils/CertificateHelper';
|
|
9
11
|
import CertificateStatus from '../CertificateStatus';
|
|
10
12
|
|
|
11
13
|
interface DashboardItemCertificateProps {
|
|
@@ -20,40 +22,33 @@ export const DashboardItemCertificate = ({
|
|
|
20
22
|
productType,
|
|
21
23
|
mode,
|
|
22
24
|
}: DashboardItemCertificateProps) => {
|
|
23
|
-
|
|
24
|
-
if (certificateDefinition) {
|
|
25
|
+
const getCertificateDefinition = () => {
|
|
26
|
+
if (certificate && certificateDefinition) {
|
|
25
27
|
throw new Error('certificate and certificateDefinition are mutually exclusive');
|
|
28
|
+
} else if (!certificate && !certificateDefinition) {
|
|
29
|
+
throw new Error('certificate or certificateDefinition is required');
|
|
26
30
|
}
|
|
27
|
-
certificateDefinition = certificate.certificate_definition;
|
|
28
|
-
} else if (certificateDefinition) {
|
|
29
|
-
if (certificate) {
|
|
30
|
-
throw new Error('certificate and certificateDefinition are mutually exclusive');
|
|
31
|
-
}
|
|
32
|
-
} else {
|
|
33
|
-
throw new Error('certificate or certificateDefinition is required');
|
|
34
|
-
}
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
return certificate ? certificate.certificate_definition : certificateDefinition;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const course: Maybe<CourseLight> = useMemo(
|
|
36
|
+
() => CertificateHelper.getCourse(certificate),
|
|
37
|
+
[certificate],
|
|
38
|
+
);
|
|
39
|
+
const definition = useMemo(getCertificateDefinition, [certificate]);
|
|
44
40
|
|
|
45
41
|
return (
|
|
46
42
|
<DashboardItem
|
|
47
43
|
mode={mode}
|
|
48
44
|
title={course?.title ?? ''}
|
|
49
45
|
code={'Ref. ' + (course?.code ?? '')}
|
|
50
|
-
imageUrl="https://d29emq8to944i.cloudfront.net/cba69447-b9f7-b4d7-c0d5-4d98b5280a4e/thumbnails/1659356729_1080.jpg"
|
|
51
46
|
imageFile={course?.cover}
|
|
52
47
|
footer={
|
|
53
48
|
<>
|
|
54
49
|
<div className="dashboard-certificate__body">
|
|
55
50
|
<Icon name={IconTypeEnum.CERTIFICATE} />
|
|
56
|
-
<span>{
|
|
51
|
+
<span>{definition!.title}</span>
|
|
57
52
|
</div>
|
|
58
53
|
<div className="dashboard-certificate__footer">
|
|
59
54
|
<span>
|