Skip to content

Commit 300a52e

Browse files
committed
feat: add extended profile fields functionality to profile page
1 parent f819a0b commit 300a52e

File tree

9 files changed

+541
-1
lines changed

9 files changed

+541
-1
lines changed

src/profile/ProfilePage.jsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
openForm,
1818
closeForm,
1919
updateDraft,
20+
getExtendedProfileFields as fetchExtraFieldsInfo,
2021
} from './data/actions';
2122

2223
// Components
@@ -42,6 +43,7 @@ import { profilePageSelector } from './data/selectors';
4243
import messages from './ProfilePage.messages';
4344

4445
import withParams from '../utils/hoc';
46+
import ExtendedProfileFields from './forms/ExtendedProfileFields';
4547

4648
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
4749

@@ -65,6 +67,7 @@ class ProfilePage extends React.Component {
6567

6668
componentDidMount() {
6769
this.props.fetchProfile(this.props.params.username);
70+
this.props.fetchExtraFieldsInfo({ is_register_page: true });
6871
sendTrackingLogEvent('edx.profile.viewed', {
6972
username: this.props.params.username,
7073
});
@@ -284,6 +287,13 @@ class ProfilePage extends React.Component {
284287
)}
285288
</div>
286289
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
290+
{this.props.extendedProfileFields.length > 0 && (
291+
<ExtendedProfileFields
292+
extendedProfileFields={this.props.extendedProfileFields}
293+
formId="extendedProfile"
294+
{...commonFormProps}
295+
/>
296+
)}
287297
{!this.isYOBDisabled() && this.renderAgeMessage()}
288298
{isBioBlockVisible && (
289299
<Bio
@@ -360,6 +370,28 @@ ProfilePage.propTypes = {
360370
name: PropTypes.string,
361371
visibilityName: PropTypes.string.isRequired,
362372

373+
// Extra profile fields
374+
extendedProfileFields: PropTypes.arrayOf(PropTypes.shape({
375+
name: PropTypes.string.isRequired,
376+
label: PropTypes.string.isRequired,
377+
default: PropTypes.unknown,
378+
placeholder: PropTypes.string,
379+
instructions: PropTypes.string,
380+
options: PropTypes.arrayOf(PropTypes.shape({
381+
value: PropTypes.string.isRequired,
382+
label: PropTypes.string.isRequired,
383+
})),
384+
error_message: PropTypes.shape({
385+
required: PropTypes.string,
386+
invalid: PropTypes.string,
387+
}),
388+
restrictions: PropTypes.shape({
389+
max_length: PropTypes.number,
390+
}),
391+
type: PropTypes.string.isRequired,
392+
value: PropTypes.unknown,
393+
})),
394+
363395
// Social links form data
364396
socialLinks: PropTypes.arrayOf(PropTypes.shape({
365397
platform: PropTypes.string,
@@ -395,6 +427,7 @@ ProfilePage.propTypes = {
395427
openForm: PropTypes.func.isRequired,
396428
closeForm: PropTypes.func.isRequired,
397429
updateDraft: PropTypes.func.isRequired,
430+
fetchExtraFieldsInfo: PropTypes.func.isRequired,
398431

399432
// Router
400433
params: PropTypes.shape({
@@ -422,6 +455,7 @@ ProfilePage.defaultProps = {
422455
courseCertificates: null,
423456
requiresParentalConsent: null,
424457
dateJoined: null,
458+
extendedProfileFields: [],
425459
};
426460

427461
export default connect(
@@ -434,5 +468,6 @@ export default connect(
434468
openForm,
435469
closeForm,
436470
updateDraft,
471+
fetchExtraFieldsInfo,
437472
},
438473
)(injectIntl(withParams(ProfilePage)));

src/profile/data/actions.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,29 @@ export const updateDraft = (name, value) => ({
147147
export const resetDrafts = () => ({
148148
type: RESET_DRAFTS,
149149
});
150+
151+
// Third party auth context
152+
export const EXTENDED_PROFILE_FIELDS = new AsyncActionType('EXTENDED_PROFILE_FIELDS', 'GET_EXTENDED_PROFILE_FIELDS');
153+
export const EXTENDED_PROFILE_FIELDS_CLEAR_ERROR_MSG = 'EXTENDED_PROFILE_FIELDS_CLEAR_ERROR_MSG';
154+
155+
export const getExtendedProfileFields = (urlParams) => ({
156+
type: EXTENDED_PROFILE_FIELDS.BASE,
157+
payload: { urlParams },
158+
});
159+
160+
export const getExtendedProfileFieldsBegin = () => ({
161+
type: EXTENDED_PROFILE_FIELDS.BEGIN,
162+
});
163+
164+
export const getExtendedProfileFieldsSuccess = (fields) => ({
165+
type: EXTENDED_PROFILE_FIELDS.SUCCESS,
166+
payload: fields,
167+
});
168+
169+
export const getExtendedProfileFieldsFailure = () => ({
170+
type: EXTENDED_PROFILE_FIELDS.FAILURE,
171+
});
172+
173+
export const clearThirdPartyAuthContextErrorMessage = () => ({
174+
type: EXTENDED_PROFILE_FIELDS_CLEAR_ERROR_MSG,
175+
});

src/profile/data/reducers.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
FETCH_PROFILE,
88
UPDATE_DRAFT,
99
RESET_DRAFTS,
10+
EXTENDED_PROFILE_FIELDS,
1011
} from './actions';
1112

1213
export const initialState = {
@@ -22,6 +23,7 @@ export const initialState = {
2223
drafts: {},
2324
isLoadingProfile: true,
2425
isAuthenticatedUserProfile: false,
26+
extendedProfileFields: [],
2527
};
2628

2729
const profilePage = (state = initialState, action = {}) => {
@@ -153,6 +155,14 @@ const profilePage = (state = initialState, action = {}) => {
153155
};
154156
}
155157
return state;
158+
case EXTENDED_PROFILE_FIELDS.BEGIN:
159+
case EXTENDED_PROFILE_FIELDS.FAILURE:
160+
case EXTENDED_PROFILE_FIELDS.SUCCESS:
161+
if (!action.payload) { return state; }
162+
return {
163+
...state,
164+
extendedProfileFields: action.payload,
165+
};
156166
default:
157167
return state;
158168
}

src/profile/data/sagas.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import {
3030
saveProfileSuccess,
3131
SAVE_PROFILE,
3232
SAVE_PROFILE_PHOTO,
33+
EXTENDED_PROFILE_FIELDS,
34+
getExtendedProfileFieldsBegin,
35+
getExtendedProfileFieldsSuccess,
36+
getExtendedProfileFieldsFailure,
3337
} from './actions';
3438
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
3539
import * as ProfileApiService from './services';
@@ -114,6 +118,7 @@ export function* handleSaveProfile(action) {
114118
'languageProficiencies',
115119
'name',
116120
'socialLinks',
121+
'extendedProfile',
117122
]);
118123

119124
const preferencesDrafts = pick(drafts, [
@@ -201,9 +206,24 @@ export function* handleDeleteProfilePhoto(action) {
201206
}
202207
}
203208

209+
export function* fetchThirdPartyAuthContext(action) {
210+
try {
211+
yield put(getExtendedProfileFieldsBegin());
212+
const {
213+
fields,
214+
} = yield call(ProfileApiService.getExtendedProfileFields, action.payload.urlParams);
215+
216+
yield put(getExtendedProfileFieldsSuccess(fields));
217+
} catch (e) {
218+
yield put(getExtendedProfileFieldsFailure());
219+
throw e;
220+
}
221+
}
222+
204223
export default function* profileSaga() {
205224
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
206225
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
207226
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
208227
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
228+
yield takeEvery(EXTENDED_PROFILE_FIELDS.BASE, fetchThirdPartyAuthContext);
209229
}

src/profile/data/selectors.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const formIdSelector = (state, props) => props.formId;
1111
export const userAccountSelector = state => state.userAccount;
1212

1313
export const profileAccountSelector = state => state.profilePage.account;
14+
export const extendedProfileFieldsSelector = state => state.profilePage.extendedProfileFields;
1415
export const profileDraftsSelector = state => state.profilePage.drafts;
1516
export const accountPrivacySelector = state => state.profilePage.preferences.accountPrivacy;
1617
export const profilePreferencesSelector = state => state.profilePage.preferences;
@@ -323,6 +324,7 @@ export const profilePageSelector = createSelector(
323324
isLoadingProfileSelector,
324325
draftSocialLinksByPlatformSelector,
325326
accountErrorsSelector,
327+
extendedProfileFieldsSelector,
326328
(
327329
account,
328330
formValues,
@@ -332,6 +334,7 @@ export const profilePageSelector = createSelector(
332334
isLoadingProfile,
333335
draftSocialLinksByPlatform,
334336
errors,
337+
extendedProfileFields,
335338
) => ({
336339
// Account data we need
337340
username: account.username,
@@ -374,5 +377,14 @@ export const profilePageSelector = createSelector(
374377
savePhotoState,
375378
isLoadingProfile,
376379
photoUploadError: errors.photo || null,
380+
381+
// Extended profile fields
382+
extendedProfileFields: extendedProfileFields.map((field) => ({
383+
...field,
384+
value: account.extendedProfile?.find(
385+
(extendedProfileField) => extendedProfileField.fieldName === field.name,
386+
)?.fieldValue,
387+
})),
377388
}),
389+
378390
);

src/profile/data/services.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ensureConfig, getConfig } from '@edx/frontend-platform';
2-
import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
2+
import { getAuthenticatedHttpClient, getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
33
import { logError } from '@edx/frontend-platform/logging';
44
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
55

@@ -147,3 +147,26 @@ export async function getCourseCertificates(username) {
147147
return [];
148148
}
149149
}
150+
151+
export async function getExtendedProfileFields(urlParams) {
152+
const requestConfig = {
153+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
154+
params: urlParams,
155+
isPublic: true,
156+
};
157+
158+
const { data } = await getAuthenticatedHttpClient()
159+
.get(
160+
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
161+
requestConfig,
162+
)
163+
.catch((e) => {
164+
throw (e);
165+
});
166+
167+
const extendedProfileFields = data.optionalFields.extended_profile
168+
.map((fieldName) => (data.optionalFields.fields[fieldName] ?? data.registrationFields.fields[fieldName]))
169+
.filter(Boolean);
170+
171+
return { fields: extendedProfileFields };
172+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { connect } from 'react-redux';
4+
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
5+
import get from 'lodash.get';
6+
7+
// Mock Data
8+
import mockData from '../data/mock_data';
9+
10+
// Components
11+
import EditableItemHeader from './elements/EditableItemHeader';
12+
import SwitchContent from './elements/SwitchContent';
13+
import SelectField from './elements/SelectField';
14+
import { editableFormSelector } from '../data/selectors';
15+
import TextField from './elements/TextField';
16+
17+
const ExtendedProfileFields = (props) => {
18+
const {
19+
extendedProfileFields, formId, changeHandler, submitHandler, closeHandler, openHandler, editMode,
20+
} = props;
21+
22+
// if (!learningGoal) {
23+
// learningGoal = mockData.learningGoal;
24+
// }
25+
26+
// if (!editMode || editMode === 'empty') { // editMode defaults to 'empty', not sure why yet
27+
// editMode = mockData.editMode;
28+
// }
29+
30+
// if (!visibilityLearningGoal) {
31+
// visibilityLearningGoal = mockData.visibilityLearningGoal;
32+
// }
33+
34+
return extendedProfileFields?.map((field) => (
35+
<SwitchContent
36+
className="mb-5"
37+
expression={field.type}
38+
cases={{
39+
checkbox: (
40+
<>
41+
<EditableItemHeader content={field.label} />
42+
<p data-hj-suppress className="lead">
43+
{field.default}
44+
</p>
45+
</>
46+
),
47+
text: (
48+
<TextField
49+
formId={formId}
50+
changeHandler={changeHandler}
51+
submitHandler={submitHandler}
52+
closeHandler={closeHandler}
53+
openHandler={openHandler}
54+
editMode={editMode}
55+
{...field}
56+
/>
57+
),
58+
select: (
59+
<SelectField
60+
formId={formId}
61+
changeHandler={changeHandler}
62+
submitHandler={submitHandler}
63+
closeHandler={closeHandler}
64+
openHandler={openHandler}
65+
editMode={editMode}
66+
{...field}
67+
/>
68+
),
69+
}}
70+
/>
71+
));
72+
};
73+
74+
ExtendedProfileFields.propTypes = {
75+
// From Selector
76+
formId: PropTypes.string.isRequired,
77+
extendedProfileFields: PropTypes.arrayOf(PropTypes.shape({
78+
name: PropTypes.string.isRequired,
79+
label: PropTypes.string.isRequired,
80+
default: PropTypes.unknown,
81+
placeholder: PropTypes.string,
82+
instructions: PropTypes.string,
83+
options: PropTypes.arrayOf(PropTypes.shape({
84+
value: PropTypes.string.isRequired,
85+
label: PropTypes.string.isRequired,
86+
})),
87+
error_message: PropTypes.shape({
88+
required: PropTypes.string,
89+
invalid: PropTypes.string,
90+
}),
91+
restrictions: PropTypes.shape({
92+
max_length: PropTypes.number,
93+
}),
94+
type: PropTypes.string.isRequired,
95+
})).isRequired,
96+
97+
// Actions
98+
changeHandler: PropTypes.func.isRequired,
99+
submitHandler: PropTypes.func.isRequired,
100+
closeHandler: PropTypes.func.isRequired,
101+
openHandler: PropTypes.func.isRequired,
102+
103+
// i18n
104+
intl: intlShape.isRequired,
105+
};
106+
107+
ExtendedProfileFields.defaultProps = {
108+
};
109+
110+
export default connect(editableFormSelector, {})(injectIntl(ExtendedProfileFields));

0 commit comments

Comments
 (0)