richie-education 2.25.0-b2.dev176 → 2.25.0-b2.dev183
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.spec.ts +2 -1
- package/js/api/joanie.ts +1 -51
- package/js/api/lms/dummy.ts +22 -0
- package/js/api/lms/openedx-fonzie.spec.ts +20 -0
- package/js/api/lms/openedx-fonzie.ts +35 -0
- package/js/api/utils.ts +51 -0
- package/js/components/AddressesManagement/AddressForm/index.tsx +15 -14
- package/js/components/Form/Form/index.tsx +23 -0
- package/js/components/Form/index.ts +3 -0
- package/js/hooks/useDashboardAddressForm.tsx +13 -12
- package/js/hooks/useOpenEdxProfile/index.ts +45 -0
- package/js/hooks/useOpenEdxProfile/utils/index.spec.ts +69 -0
- package/js/hooks/useOpenEdxProfile/utils/index.ts +131 -0
- package/js/pages/DashboardAddressesManagement/_styles.scss +1 -1
- package/js/pages/DashboardAddressesManagement/index.spec.tsx +34 -12
- package/js/pages/DashboardAddressesManagement/index.tsx +14 -9
- package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.spec.tsx +156 -115
- package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.tsx +9 -8
- package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +92 -121
- package/js/pages/DashboardOpenEdxProfile/index.spec.tsx +131 -0
- package/js/pages/DashboardOpenEdxProfile/index.tsx +237 -0
- package/js/pages/DashboardPreferences/index.tsx +2 -0
- package/js/types/api.ts +6 -1
- package/js/types/openEdx.ts +41 -0
- package/js/utils/react-query/useSessionMutation/index.spec.tsx +1 -1
- package/js/utils/react-query/useSessionQuery/index.spec.tsx +1 -1
- package/js/utils/test/factories/openEdx.tsx +23 -0
- package/js/widgets/Dashboard/components/DashboardBox/_styles.scss +6 -0
- package/js/widgets/Dashboard/components/DashboardBox/index.tsx +4 -0
- package/package.json +1 -1
- package/scss/objects/_form.scss +9 -2
package/js/api/joanie.spec.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fetchMock from 'fetch-mock';
|
|
2
2
|
import { ResourcesQuery } from 'hooks/useResources';
|
|
3
3
|
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
4
|
-
import { buildApiUrl
|
|
4
|
+
import { buildApiUrl } from './joanie';
|
|
5
|
+
import { getResponseBody } from './utils';
|
|
5
6
|
|
|
6
7
|
describe('api/joanie', () => {
|
|
7
8
|
it('getResponse should handle empty response body', async () => {
|
package/js/api/joanie.ts
CHANGED
|
@@ -12,61 +12,11 @@ import queryString from 'query-string';
|
|
|
12
12
|
import type * as Joanie from 'types/Joanie';
|
|
13
13
|
import { AuthenticationApi } from 'api/authentication';
|
|
14
14
|
import context from 'utils/context';
|
|
15
|
-
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
16
15
|
import { JOANIE_API_VERSION } from 'settings';
|
|
17
16
|
import { ResourcesQuery } from 'hooks/useResources';
|
|
18
17
|
import { ObjectHelper } from 'utils/ObjectHelper';
|
|
19
18
|
import { Maybe } from 'types/utils';
|
|
20
|
-
|
|
21
|
-
interface CheckStatusOptions {
|
|
22
|
-
fallbackValue: any;
|
|
23
|
-
ignoredErrorStatus: (number | HttpStatusCode)[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function getFileFromResponse(response: Response): Promise<File> {
|
|
27
|
-
const filenameRegex = /filename="(.*)";/;
|
|
28
|
-
const dispositionHeader = response.headers.get('Content-Disposition');
|
|
29
|
-
const matches = dispositionHeader?.match(filenameRegex);
|
|
30
|
-
|
|
31
|
-
return new File([await response.blob()], matches ? matches[1] : '', {
|
|
32
|
-
type: response.headers.get('Content-Type') || '',
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getResponseBody(response: Response) {
|
|
37
|
-
if (response.headers.get('Content-Type') === 'application/json') {
|
|
38
|
-
return response.json();
|
|
39
|
-
}
|
|
40
|
-
const fileType = ['application/pdf', 'application/zip'];
|
|
41
|
-
if (fileType.includes(response.headers.get('Content-Type') || '')) {
|
|
42
|
-
return new Promise((resolve) => resolve(response));
|
|
43
|
-
}
|
|
44
|
-
return response.text();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/*
|
|
48
|
-
A util to manage Joanie API responses.
|
|
49
|
-
It parses properly the response according to its `Content-Type`
|
|
50
|
-
otherwise it throws an `HttpError`.
|
|
51
|
-
|
|
52
|
-
`options` arguments accept an array of ignoredErrorStatus. If the response
|
|
53
|
-
fails with one of this status code, the `fallbackValue` will return and no error will
|
|
54
|
-
be raised.
|
|
55
|
-
*/
|
|
56
|
-
export function checkStatus(
|
|
57
|
-
response: Response,
|
|
58
|
-
options: CheckStatusOptions = { fallbackValue: null, ignoredErrorStatus: [] },
|
|
59
|
-
): Promise<any> {
|
|
60
|
-
if (response.ok) {
|
|
61
|
-
return getResponseBody(response);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (options.ignoredErrorStatus.includes(response.status)) {
|
|
65
|
-
return Promise.resolve(options.fallbackValue);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
throw new HttpError(response.status, response.statusText);
|
|
69
|
-
}
|
|
19
|
+
import { checkStatus, getFileFromResponse } from './utils';
|
|
70
20
|
|
|
71
21
|
/*
|
|
72
22
|
Generate default headers used for most of Joanie requests. It defined `Content-Type`
|
package/js/api/lms/dummy.ts
CHANGED
|
@@ -6,6 +6,12 @@ import { UnknownEnrollment, OpenEdXEnrollment } from 'types';
|
|
|
6
6
|
import { location } from 'utils/indirection/window';
|
|
7
7
|
import { CURRENT_JOANIE_DEV_DEMO_USER, RICHIE_USER_TOKEN } from 'settings';
|
|
8
8
|
import { base64Decode } from 'utils/base64Parser';
|
|
9
|
+
import {
|
|
10
|
+
OpenEdxGender,
|
|
11
|
+
OpenEdxLanguageIsoCode,
|
|
12
|
+
OpenEdxLevelOfEducation,
|
|
13
|
+
OpenEdxApiProfile,
|
|
14
|
+
} from 'types/openEdx';
|
|
9
15
|
|
|
10
16
|
type JWTPayload = {
|
|
11
17
|
email: string;
|
|
@@ -89,6 +95,22 @@ const API = (APIConf: LMSBackend | AuthenticationBackend): APILms => {
|
|
|
89
95
|
localStorage.removeItem(RICHIE_DUMMY_IS_LOGGED_IN);
|
|
90
96
|
},
|
|
91
97
|
accessToken: () => sessionStorage.getItem(RICHIE_USER_TOKEN),
|
|
98
|
+
account: {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
100
|
+
get: (username: string): Promise<OpenEdxApiProfile> => {
|
|
101
|
+
return Promise.resolve({
|
|
102
|
+
username: 'j_do',
|
|
103
|
+
name: 'John Do',
|
|
104
|
+
email: 'j.do@whois.net',
|
|
105
|
+
country: 'fr',
|
|
106
|
+
level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
|
|
107
|
+
gender: OpenEdxGender.MALE,
|
|
108
|
+
year_of_birth: '1971',
|
|
109
|
+
'pref-lang': OpenEdxLanguageIsoCode.ENGLISH,
|
|
110
|
+
language_proficiencies: [{ code: OpenEdxLanguageIsoCode.ENGLISH }],
|
|
111
|
+
} as OpenEdxApiProfile);
|
|
112
|
+
},
|
|
113
|
+
},
|
|
92
114
|
},
|
|
93
115
|
enrollment: {
|
|
94
116
|
get: async (url: string, user: Nullable<User>) =>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { faker } from '@faker-js/faker';
|
|
2
2
|
import fetchMock from 'fetch-mock';
|
|
3
3
|
import { RICHIE_USER_TOKEN } from 'settings';
|
|
4
|
+
import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
|
|
4
5
|
import FonzieAPIInterface from './openedx-fonzie';
|
|
5
6
|
|
|
6
7
|
jest.mock('utils/context', () => ({
|
|
@@ -30,6 +31,25 @@ describe('Fonzie API', () => {
|
|
|
30
31
|
await expect(api.user.me()).resolves.toEqual(user);
|
|
31
32
|
});
|
|
32
33
|
|
|
34
|
+
it('uses its own route to get user profile', async () => {
|
|
35
|
+
const openEdxApiProfile = OpenEdxApiProfileFactory().one();
|
|
36
|
+
const { 'pref-lang': language, ...account } = openEdxApiProfile;
|
|
37
|
+
fetchMock.get(
|
|
38
|
+
`https://demo.endpoint.api/api/user/v1/accounts/${openEdxApiProfile.username}`,
|
|
39
|
+
account,
|
|
40
|
+
);
|
|
41
|
+
fetchMock.get(
|
|
42
|
+
`https://demo.endpoint.api/api/user/v1/preferences/${openEdxApiProfile.username}`,
|
|
43
|
+
{
|
|
44
|
+
'pref-lang': language,
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const api = FonzieAPIInterface(configuration);
|
|
49
|
+
expect(await api.user.account!.get(openEdxApiProfile.username)).toEqual(openEdxApiProfile);
|
|
50
|
+
expect(fetchMock.calls()).toHaveLength(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
33
53
|
it('is able to retrieve access token within the session storage', () => {
|
|
34
54
|
const accessToken = faker.string.uuid();
|
|
35
55
|
sessionStorage.setItem(RICHIE_USER_TOKEN, accessToken);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { AuthenticationBackend, LMSBackend } from 'types/commonDataProps';
|
|
2
2
|
import { APILms } from 'types/api';
|
|
3
3
|
import { RICHIE_USER_TOKEN } from 'settings';
|
|
4
|
+
import { isHttpError } from 'utils/errors/HttpError';
|
|
5
|
+
import { handle } from 'utils/errors/handle';
|
|
6
|
+
import { OpenEdxApiProfile } from 'types/openEdx';
|
|
7
|
+
import { checkStatus } from 'api/utils';
|
|
4
8
|
import OpenEdxHawthornApiInterface from './openedx-hawthorn';
|
|
5
9
|
|
|
6
10
|
/**
|
|
@@ -24,6 +28,8 @@ const API = (APIConf: AuthenticationBackend | LMSBackend): APILms => {
|
|
|
24
28
|
routes: {
|
|
25
29
|
user: {
|
|
26
30
|
me: `${APIConf.endpoint}/api/v1.0/user/me`,
|
|
31
|
+
account: `${APIConf.endpoint}/api/user/v1/accounts/:username`,
|
|
32
|
+
preferences: `${APIConf.endpoint}/api/user/v1/preferences/:username`,
|
|
27
33
|
},
|
|
28
34
|
},
|
|
29
35
|
};
|
|
@@ -36,6 +42,35 @@ const API = (APIConf: AuthenticationBackend | LMSBackend): APILms => {
|
|
|
36
42
|
accessToken: () => {
|
|
37
43
|
return sessionStorage.getItem(RICHIE_USER_TOKEN);
|
|
38
44
|
},
|
|
45
|
+
account: {
|
|
46
|
+
get: async (username: string) => {
|
|
47
|
+
const options: RequestInit = {
|
|
48
|
+
credentials: 'include',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const account = await fetch(
|
|
53
|
+
APIOptions.routes.user.account.replace(':username', username),
|
|
54
|
+
options,
|
|
55
|
+
).then(checkStatus);
|
|
56
|
+
const preferences = await fetch(
|
|
57
|
+
APIOptions.routes.user.preferences.replace(':username', username),
|
|
58
|
+
options,
|
|
59
|
+
).then(checkStatus);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...account,
|
|
63
|
+
...preferences,
|
|
64
|
+
} as OpenEdxApiProfile;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
if (isHttpError(e)) {
|
|
67
|
+
handle(new Error(`[GET - Account] > ${e.code} - ${e.message}`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
},
|
|
39
74
|
},
|
|
40
75
|
};
|
|
41
76
|
};
|
package/js/api/utils.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
2
|
+
|
|
3
|
+
interface CheckStatusOptions {
|
|
4
|
+
fallbackValue: any;
|
|
5
|
+
ignoredErrorStatus: (number | HttpStatusCode)[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getFileFromResponse(response: Response): Promise<File> {
|
|
9
|
+
const filenameRegex = /filename="(.*)";/;
|
|
10
|
+
const dispositionHeader = response.headers.get('Content-Disposition');
|
|
11
|
+
const matches = dispositionHeader?.match(filenameRegex);
|
|
12
|
+
|
|
13
|
+
return new File([await response.blob()], matches ? matches[1] : '', {
|
|
14
|
+
type: response.headers.get('Content-Type') || '',
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getResponseBody(response: Response) {
|
|
19
|
+
if (response.headers.get('Content-Type') === 'application/json') {
|
|
20
|
+
return response.json();
|
|
21
|
+
}
|
|
22
|
+
const fileType = ['application/pdf', 'application/zip'];
|
|
23
|
+
if (fileType.includes(response.headers.get('Content-Type') || '')) {
|
|
24
|
+
return new Promise((resolve) => resolve(response));
|
|
25
|
+
}
|
|
26
|
+
return response.text();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
A util to manage API responses.
|
|
31
|
+
It parses properly the response according to its `Content-Type`
|
|
32
|
+
otherwise it throws an `HttpError`.
|
|
33
|
+
|
|
34
|
+
`options` arguments accept an array of ignoredErrorStatus. If the response
|
|
35
|
+
fails with one of this status code, the `fallbackValue` will return and no error will
|
|
36
|
+
be raised.
|
|
37
|
+
*/
|
|
38
|
+
export function checkStatus(
|
|
39
|
+
response: Response,
|
|
40
|
+
options: CheckStatusOptions = { fallbackValue: null, ignoredErrorStatus: [] },
|
|
41
|
+
): Promise<any> {
|
|
42
|
+
if (response.ok) {
|
|
43
|
+
return getResponseBody(response);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.ignoredErrorStatus.includes(response.status)) {
|
|
47
|
+
return Promise.resolve(options.fallbackValue);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new HttpError(response.status, response.statusText);
|
|
51
|
+
}
|
|
@@ -10,6 +10,7 @@ import Input from 'components/Form/Input';
|
|
|
10
10
|
import { useAddresses } from 'hooks/useAddresses';
|
|
11
11
|
import type { Address } from 'types/Joanie';
|
|
12
12
|
import { messages as formMessages } from 'components/Form/messages';
|
|
13
|
+
import Form from 'components/Form';
|
|
13
14
|
import validationSchema from './validationSchema';
|
|
14
15
|
|
|
15
16
|
export interface AddressFormValues extends Omit<Address, 'id' | 'is_main'> {
|
|
@@ -60,11 +61,11 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
60
61
|
|
|
61
62
|
return (
|
|
62
63
|
<FormProvider {...form}>
|
|
63
|
-
<
|
|
64
|
+
<Form name="address-form" onSubmit={handleSubmit(onSubmit)} noValidate>
|
|
64
65
|
<p className="form__required-fields-note">
|
|
65
66
|
<FormattedMessage {...formMessages.formOptionalFieldsText} />
|
|
66
67
|
</p>
|
|
67
|
-
<
|
|
68
|
+
<Form.Row>
|
|
68
69
|
<Input
|
|
69
70
|
className="form-field"
|
|
70
71
|
required
|
|
@@ -72,8 +73,8 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
72
73
|
name="title"
|
|
73
74
|
label={intl.formatMessage(messages.titleInputLabel)}
|
|
74
75
|
/>
|
|
75
|
-
</
|
|
76
|
-
<
|
|
76
|
+
</Form.Row>
|
|
77
|
+
<Form.Row>
|
|
77
78
|
<Input
|
|
78
79
|
className="form-field"
|
|
79
80
|
required
|
|
@@ -86,16 +87,16 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
86
87
|
name="last_name"
|
|
87
88
|
label={intl.formatMessage(messages.last_nameInputLabel)}
|
|
88
89
|
/>
|
|
89
|
-
</
|
|
90
|
-
<
|
|
90
|
+
</Form.Row>
|
|
91
|
+
<Form.Row>
|
|
91
92
|
<Input
|
|
92
93
|
required
|
|
93
94
|
fullWidth
|
|
94
95
|
name="address"
|
|
95
96
|
label={intl.formatMessage(messages.addressInputLabel)}
|
|
96
97
|
/>
|
|
97
|
-
</
|
|
98
|
-
<
|
|
98
|
+
</Form.Row>
|
|
99
|
+
<Form.Row>
|
|
99
100
|
<Input
|
|
100
101
|
className="form-field"
|
|
101
102
|
required
|
|
@@ -109,17 +110,17 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
109
110
|
name="city"
|
|
110
111
|
label={intl.formatMessage(messages.cityInputLabel)}
|
|
111
112
|
/>
|
|
112
|
-
</
|
|
113
|
-
<
|
|
113
|
+
</Form.Row>
|
|
114
|
+
<Form.Row>
|
|
114
115
|
<CountrySelectField
|
|
115
116
|
name="country"
|
|
116
117
|
label={intl.formatMessage(messages.countryInputLabel)}
|
|
117
118
|
state={formState.errors.country ? 'error' : 'default'}
|
|
118
119
|
/>
|
|
119
|
-
</
|
|
120
|
+
</Form.Row>
|
|
120
121
|
|
|
121
122
|
{!address ? (
|
|
122
|
-
<
|
|
123
|
+
<Form.Row>
|
|
123
124
|
<Checkbox
|
|
124
125
|
aria-invalid={!!formState.errors?.save}
|
|
125
126
|
id="save"
|
|
@@ -132,7 +133,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
132
133
|
)}
|
|
133
134
|
{...register('save')}
|
|
134
135
|
/>
|
|
135
|
-
</
|
|
136
|
+
</Form.Row>
|
|
136
137
|
) : null}
|
|
137
138
|
|
|
138
139
|
<footer className="form__footer">
|
|
@@ -155,7 +156,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
155
156
|
</Button>
|
|
156
157
|
)}
|
|
157
158
|
</footer>
|
|
158
|
-
</
|
|
159
|
+
</Form>
|
|
159
160
|
</FormProvider>
|
|
160
161
|
);
|
|
161
162
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import c from 'classnames';
|
|
2
|
+
import { PropsWithChildren } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FormProps
|
|
5
|
+
extends React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> {}
|
|
6
|
+
|
|
7
|
+
const Form = ({ children, onSubmit, className, name, noValidate = true }: FormProps) => {
|
|
8
|
+
return (
|
|
9
|
+
<form className={c('form', className)} name={name} onSubmit={onSubmit} noValidate={noValidate}>
|
|
10
|
+
{children}
|
|
11
|
+
</form>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
Form.Row = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
|
|
16
|
+
return <div className={c('form-row', className)}>{children}</div>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
Form.Footer = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
|
|
20
|
+
return <div className={c('form-footer', className)}>{children}</div>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default Form;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import Form from './Form';
|
|
2
|
+
|
|
1
3
|
export { default as Select } from './Select';
|
|
2
4
|
export { CountrySelectField } from './CountrySelectField';
|
|
3
5
|
export { messages } from './messages';
|
|
4
6
|
export * from './utils';
|
|
5
7
|
export * from './ValidationErrors';
|
|
8
|
+
export default Form;
|
|
@@ -11,6 +11,7 @@ import { messages as formMessages } from 'components/Form/messages';
|
|
|
11
11
|
import { CountrySelectField } from 'components/Form/CountrySelectField';
|
|
12
12
|
import Input from 'components/Form/Input';
|
|
13
13
|
import { Address } from 'types/Joanie';
|
|
14
|
+
import Form from 'components/Form';
|
|
14
15
|
|
|
15
16
|
const messages = defineMessages({
|
|
16
17
|
isMainInputLabel: {
|
|
@@ -64,19 +65,19 @@ export const useDashboardAddressForm = (address?: Address) => {
|
|
|
64
65
|
|
|
65
66
|
const FormView = (
|
|
66
67
|
<FormProvider {...form}>
|
|
67
|
-
<
|
|
68
|
+
<Form noValidate>
|
|
68
69
|
<p className="form__required-fields-note">
|
|
69
70
|
<FormattedMessage {...formMessages.formOptionalFieldsText} />
|
|
70
71
|
</p>
|
|
71
|
-
<
|
|
72
|
+
<Form.Row>
|
|
72
73
|
<Input
|
|
73
74
|
required
|
|
74
75
|
fullWidth
|
|
75
76
|
name="title"
|
|
76
77
|
label={intl.formatMessage(managementMessages.titleInputLabel)}
|
|
77
78
|
/>
|
|
78
|
-
</
|
|
79
|
-
<
|
|
79
|
+
</Form.Row>
|
|
80
|
+
<Form.Row>
|
|
80
81
|
<Input
|
|
81
82
|
className="form-field"
|
|
82
83
|
required
|
|
@@ -90,17 +91,17 @@ export const useDashboardAddressForm = (address?: Address) => {
|
|
|
90
91
|
name="last_name"
|
|
91
92
|
label={intl.formatMessage(managementMessages.last_nameInputLabel)}
|
|
92
93
|
/>
|
|
93
|
-
</
|
|
94
|
-
<
|
|
94
|
+
</Form.Row>
|
|
95
|
+
<Form.Row>
|
|
95
96
|
<Input
|
|
96
97
|
required
|
|
97
98
|
fullWidth
|
|
98
99
|
name="address"
|
|
99
100
|
label={intl.formatMessage(managementMessages.addressInputLabel)}
|
|
100
101
|
/>
|
|
101
|
-
</
|
|
102
|
+
</Form.Row>
|
|
102
103
|
|
|
103
|
-
<
|
|
104
|
+
<Form.Row>
|
|
104
105
|
<Input
|
|
105
106
|
className="form-field"
|
|
106
107
|
required
|
|
@@ -114,13 +115,13 @@ export const useDashboardAddressForm = (address?: Address) => {
|
|
|
114
115
|
name="city"
|
|
115
116
|
label={intl.formatMessage(managementMessages.cityInputLabel)}
|
|
116
117
|
/>
|
|
117
|
-
</
|
|
118
|
-
<
|
|
118
|
+
</Form.Row>
|
|
119
|
+
<Form.Row>
|
|
119
120
|
<CountrySelectField
|
|
120
121
|
name="country"
|
|
121
122
|
label={intl.formatMessage(managementMessages.countryInputLabel)}
|
|
122
123
|
/>
|
|
123
|
-
</
|
|
124
|
+
</Form.Row>
|
|
124
125
|
{!(address && address.is_main) && (
|
|
125
126
|
<Checkbox
|
|
126
127
|
aria-invalid={!!formState.errors?.is_main}
|
|
@@ -135,7 +136,7 @@ export const useDashboardAddressForm = (address?: Address) => {
|
|
|
135
136
|
{...register('is_main')}
|
|
136
137
|
/>
|
|
137
138
|
)}
|
|
138
|
-
</
|
|
139
|
+
</Form>
|
|
139
140
|
</FormProvider>
|
|
140
141
|
);
|
|
141
142
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { AuthenticationApi } from 'api/authentication';
|
|
4
|
+
import { useSessionQuery } from 'utils/react-query/useSessionQuery';
|
|
5
|
+
import { OpenEdxProfile, parseOpenEdxApiProfile } from './utils';
|
|
6
|
+
|
|
7
|
+
const messages = defineMessages({
|
|
8
|
+
errorGet: {
|
|
9
|
+
id: 'hooks.useOpenEdxProfile.errorGet',
|
|
10
|
+
description: 'Error message shown to the user when openEdx profile fetch request fails.',
|
|
11
|
+
defaultMessage: 'An error occurred while fetching your profile. Please retry later.',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
interface UseOpenEdxProfileProps {
|
|
16
|
+
username: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const useOpenEdxProfile = ({ username }: UseOpenEdxProfileProps) => {
|
|
20
|
+
if (!AuthenticationApi) {
|
|
21
|
+
throw new Error('AuthenticationApi is not defined');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!AuthenticationApi!.account) {
|
|
25
|
+
throw new Error('Current AuthenticationApi does not support account request');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const intl = useIntl();
|
|
29
|
+
const [error, setError] = useState<string>();
|
|
30
|
+
|
|
31
|
+
const queryFn: () => Promise<OpenEdxProfile> = useCallback(async () => {
|
|
32
|
+
try {
|
|
33
|
+
const openEdxApiProfile = await AuthenticationApi!.account!.get(username);
|
|
34
|
+
return parseOpenEdxApiProfile(intl, openEdxApiProfile);
|
|
35
|
+
} catch {
|
|
36
|
+
setError(intl.formatMessage(messages.errorGet));
|
|
37
|
+
}
|
|
38
|
+
return Promise.reject();
|
|
39
|
+
}, [username, AuthenticationApi]);
|
|
40
|
+
|
|
41
|
+
const [queryHandler] = useSessionQuery<OpenEdxProfile>(['open-edx-profile'], queryFn);
|
|
42
|
+
return { data: queryHandler.data, error };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default useOpenEdxProfile;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createIntl } from 'react-intl';
|
|
2
|
+
import { faker } from '@faker-js/faker';
|
|
3
|
+
import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
|
|
4
|
+
import { OpenEdxGender, OpenEdxLanguageIsoCode, OpenEdxLevelOfEducation } from 'types/openEdx';
|
|
5
|
+
import { parseOpenEdxApiProfile } from '.';
|
|
6
|
+
|
|
7
|
+
describe('useOpenEdxProfile > utils', () => {
|
|
8
|
+
it('parseOpenEdxApiProfile should format values', () => {
|
|
9
|
+
const profile = parseOpenEdxApiProfile(
|
|
10
|
+
createIntl({ locale: 'en' }),
|
|
11
|
+
OpenEdxApiProfileFactory({
|
|
12
|
+
username: 'John',
|
|
13
|
+
name: 'Do',
|
|
14
|
+
email: 'john.do@whereis.net',
|
|
15
|
+
'pref-lang': OpenEdxLanguageIsoCode.FRENCH,
|
|
16
|
+
country: 'fr',
|
|
17
|
+
level_of_education: OpenEdxLevelOfEducation.MASTER_OR_PROFESSIONNAL_DEGREE,
|
|
18
|
+
gender: OpenEdxGender.MALE,
|
|
19
|
+
year_of_birth: '01/01/1970',
|
|
20
|
+
language_proficiencies: [{ code: OpenEdxLanguageIsoCode.ENGLISH }],
|
|
21
|
+
date_joined: '01/01/1970',
|
|
22
|
+
}).one(),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(profile).toStrictEqual({
|
|
26
|
+
username: 'John',
|
|
27
|
+
name: 'Do',
|
|
28
|
+
email: 'john.do@whereis.net',
|
|
29
|
+
language: 'French',
|
|
30
|
+
country: 'France',
|
|
31
|
+
levelOfEducation: 'Master',
|
|
32
|
+
gender: 'Male',
|
|
33
|
+
yearOfBirth: '01/01/1970',
|
|
34
|
+
favoriteLanguage: 'English',
|
|
35
|
+
dateJoined: '01/01/1970',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('parseOpenEdxApiProfile should format default values', () => {
|
|
40
|
+
const profile = parseOpenEdxApiProfile(
|
|
41
|
+
createIntl({ locale: 'en' }),
|
|
42
|
+
OpenEdxApiProfileFactory({
|
|
43
|
+
username: faker.internet.userName(),
|
|
44
|
+
name: '',
|
|
45
|
+
email: faker.internet.email(),
|
|
46
|
+
'pref-lang': undefined,
|
|
47
|
+
country: null,
|
|
48
|
+
level_of_education: null,
|
|
49
|
+
gender: null,
|
|
50
|
+
year_of_birth: null,
|
|
51
|
+
language_proficiencies: [],
|
|
52
|
+
date_joined: Date.toString(),
|
|
53
|
+
}).one(),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(profile).toStrictEqual({
|
|
57
|
+
username: profile.username,
|
|
58
|
+
name: ' - ',
|
|
59
|
+
email: profile.email,
|
|
60
|
+
language: ' - ',
|
|
61
|
+
country: ' - ',
|
|
62
|
+
levelOfEducation: ' - ',
|
|
63
|
+
gender: ' - ',
|
|
64
|
+
yearOfBirth: ' - ',
|
|
65
|
+
favoriteLanguage: ' - ',
|
|
66
|
+
dateJoined: profile.dateJoined,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|