Skip to content

Commit 6bcf9ed

Browse files
committed
feat: add extended profile fields functionality to profile page
1 parent 9593641 commit 6bcf9ed

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
});
@@ -291,6 +294,13 @@ class ProfilePage extends React.Component {
291294
)}
292295
</div>
293296
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
297+
{this.props.extendedProfileFields.length > 0 && (
298+
<ExtendedProfileFields
299+
extendedProfileFields={this.props.extendedProfileFields}
300+
formId="extendedProfile"
301+
{...commonFormProps}
302+
/>
303+
)}
294304
{!this.isYOBDisabled() && this.renderAgeMessage()}
295305
{isBioBlockVisible && (
296306
<Bio
@@ -368,6 +378,28 @@ ProfilePage.propTypes = {
368378
name: PropTypes.string,
369379
visibilityName: PropTypes.string.isRequired,
370380

381+
// Extra profile fields
382+
extendedProfileFields: PropTypes.arrayOf(PropTypes.shape({
383+
name: PropTypes.string.isRequired,
384+
label: PropTypes.string.isRequired,
385+
default: PropTypes.unknown,
386+
placeholder: PropTypes.string,
387+
instructions: PropTypes.string,
388+
options: PropTypes.arrayOf(PropTypes.shape({
389+
value: PropTypes.string.isRequired,
390+
label: PropTypes.string.isRequired,
391+
})),
392+
error_message: PropTypes.shape({
393+
required: PropTypes.string,
394+
invalid: PropTypes.string,
395+
}),
396+
restrictions: PropTypes.shape({
397+
max_length: PropTypes.number,
398+
}),
399+
type: PropTypes.string.isRequired,
400+
value: PropTypes.unknown,
401+
})),
402+
371403
// Social links form data
372404
socialLinks: PropTypes.arrayOf(PropTypes.shape({
373405
platform: PropTypes.string,
@@ -404,6 +436,7 @@ ProfilePage.propTypes = {
404436
closeForm: PropTypes.func.isRequired,
405437
updateDraft: PropTypes.func.isRequired,
406438
navigate: PropTypes.func.isRequired,
439+
fetchExtraFieldsInfo: PropTypes.func.isRequired,
407440

408441
// Router
409442
params: PropTypes.shape({
@@ -432,6 +465,7 @@ ProfilePage.defaultProps = {
432465
courseCertificates: null,
433466
requiresParentalConsent: null,
434467
dateJoined: null,
468+
extendedProfileFields: [],
435469
};
436470

437471
export default connect(
@@ -444,5 +478,6 @@ export default connect(
444478
openForm,
445479
closeForm,
446480
updateDraft,
481+
fetchExtraFieldsInfo,
447482
},
448483
)(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 = {}) => {
@@ -155,6 +157,14 @@ const profilePage = (state = initialState, action = {}) => {
155157
};
156158
}
157159
return state;
160+
case EXTENDED_PROFILE_FIELDS.BEGIN:
161+
case EXTENDED_PROFILE_FIELDS.FAILURE:
162+
case EXTENDED_PROFILE_FIELDS.SUCCESS:
163+
if (!action.payload) { return state; }
164+
return {
165+
...state,
166+
extendedProfileFields: action.payload,
167+
};
158168
default:
159169
return state;
160170
}

src/profile/data/sagas.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import {
2929
saveProfileSuccess,
3030
SAVE_PROFILE,
3131
SAVE_PROFILE_PHOTO,
32+
EXTENDED_PROFILE_FIELDS,
33+
getExtendedProfileFieldsBegin,
34+
getExtendedProfileFieldsSuccess,
35+
getExtendedProfileFieldsFailure,
3236
} from './actions';
3337
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
3438
import * as ProfileApiService from './services';
@@ -117,6 +121,7 @@ export function* handleSaveProfile(action) {
117121
'languageProficiencies',
118122
'name',
119123
'socialLinks',
124+
'extendedProfile',
120125
]);
121126

122127
const preferencesDrafts = pick(drafts, [
@@ -204,9 +209,24 @@ export function* handleDeleteProfilePhoto(action) {
204209
}
205210
}
206211

212+
export function* fetchThirdPartyAuthContext(action) {
213+
try {
214+
yield put(getExtendedProfileFieldsBegin());
215+
const {
216+
fields,
217+
} = yield call(ProfileApiService.getExtendedProfileFields, action.payload.urlParams);
218+
219+
yield put(getExtendedProfileFieldsSuccess(fields));
220+
} catch (e) {
221+
yield put(getExtendedProfileFieldsFailure());
222+
throw e;
223+
}
224+
}
225+
207226
export default function* profileSaga() {
208227
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
209228
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
210229
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
211230
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
231+
yield takeEvery(EXTENDED_PROFILE_FIELDS.BASE, fetchThirdPartyAuthContext);
212232
}

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)