Client/server data adapter and simpler data model. (#53)
* Client/server data adapter and simpler data passthroughs. * Parse error response and pipe to UI * Add top-of-form error display for social links * Remove save failed state from save button * Remove object deconstruction in catch * Fixing a few small bugs. * When opening and closing forms, remove drafts. * Tweak where we send account_privacy back to * Passing course cert visibility through. * Fixin’ up the tests. * Documenting weird social links behavior. * More comments.
This commit is contained in:
@@ -25,10 +25,6 @@ module.exports = Merge.smart(commonConfig, {
|
||||
test: /\.(js|jsx)$/,
|
||||
include: [
|
||||
path.resolve(__dirname, '../src'),
|
||||
path.resolve(__dirname, 'node_modules/camelcase-keys'),
|
||||
path.resolve(__dirname, 'node_modules/camelcase'),
|
||||
path.resolve(__dirname, 'node_modules/map-obj'),
|
||||
path.resolve(__dirname, 'node_modules/quick-lru'),
|
||||
],
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
|
||||
@@ -24,10 +24,6 @@ module.exports = Merge.smart(commonConfig, {
|
||||
test: /\.(js|jsx)$/,
|
||||
include: [
|
||||
path.resolve(__dirname, '../src'),
|
||||
path.resolve(__dirname, 'node_modules/camelcase-keys'),
|
||||
path.resolve(__dirname, 'node_modules/camelcase'),
|
||||
path.resolve(__dirname, 'node_modules/map-obj'),
|
||||
path.resolve(__dirname, 'node_modules/quick-lru'),
|
||||
],
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -2442,7 +2442,7 @@
|
||||
"integrity": "sha512-APBpZvdQrC1MJWMzk33V7FR2RhBRtnH2QPLqZzS+qia7PixwgWNlnX7UfHjhx+YWkM53GdsZKs40EBkSwADuMA=="
|
||||
},
|
||||
"@edx/edx-bootstrap": {
|
||||
"version": "git://github.com/edx/edx-bootstrap.git#e14bb7b678037a675e26c1b196f800e0f573f22e",
|
||||
"version": "git://github.com/edx/edx-bootstrap.git#71fd3272d235eb133cccefc1ce63f35a7696bf28",
|
||||
"from": "git://github.com/edx/edx-bootstrap.git#update-with-documentation-site",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.13",
|
||||
@@ -5347,28 +5347,6 @@
|
||||
"upper-case": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"camelcase": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz",
|
||||
"integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA=="
|
||||
},
|
||||
"camelcase-keys": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-5.0.0.tgz",
|
||||
"integrity": "sha512-RFdQsUUi4TS/xg4RoLwmpw9hwOp/M0OY+9g3Oa90sH+tueZ1Cd7vXl4fEaCbydiGp1xo+eUr+wq9jLjRPLjhtg==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"map-obj": "^3.0.0",
|
||||
"quick-lru": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"map-obj": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-3.0.0.tgz",
|
||||
"integrity": "sha512-Ot+2wruG8WqTbJngDxz0Ifm03y2pO4iL+brq/l+yEkGjUza03BnMQqX2XT//Jls8MOOl2VTHviAoLX+/nq/HXw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"caniuse-api": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz",
|
||||
@@ -12760,19 +12738,13 @@
|
||||
"lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=",
|
||||
"dev": true
|
||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"lodash.defaultsdeep": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz",
|
||||
"integrity": "sha1-vsECT4WxvZbL6kBbI8FK1kQ6b4E="
|
||||
},
|
||||
"lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
@@ -12863,10 +12835,10 @@
|
||||
"integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
|
||||
"lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40="
|
||||
},
|
||||
"lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bootstrap": "^4.2.1",
|
||||
"camelcase-keys": "^5.0.0",
|
||||
"classnames": "^2.2.5",
|
||||
"connected-react-router": "^5.0.1",
|
||||
"copy-webpack-plugin": "^4.6.0",
|
||||
@@ -47,8 +46,8 @@
|
||||
"history": "^4.7.2",
|
||||
"i18n-iso-countries": "^3.7.8",
|
||||
"iso-countries-languages": "^0.2.1",
|
||||
"lodash.defaultsdeep": "^4.6.0",
|
||||
"lodash.set": "^4.3.2",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.snakecase": "^4.1.1",
|
||||
"prop-types": "^15.5.10",
|
||||
"query-string": "^5.1.1",
|
||||
"react": "^16.8.3",
|
||||
@@ -65,7 +64,6 @@
|
||||
"redux-saga": "^1.0.1",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"reselect": "^4.0.0",
|
||||
"snakecase-keys": "^2.1.0",
|
||||
"whatwg-fetch": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -6,8 +6,7 @@ export const SAVE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_PROFILE_P
|
||||
export const DELETE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_PROFILE_PHOTO');
|
||||
export const OPEN_FORM = 'OPEN_FORM';
|
||||
export const CLOSE_FORM = 'CLOSE_FORM';
|
||||
export const UPDATE_ACCOUNT_DRAFT = 'UPDATE_ACCOUNT_DRAFT';
|
||||
export const UPDATE_VISIBILITY_DRAFT = 'UPDATE_VISIBILITY_DRAFT';
|
||||
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
|
||||
export const RESET_DRAFTS = 'RESET_DRAFTS';
|
||||
|
||||
// FETCH PROFILE ACTIONS
|
||||
@@ -21,11 +20,11 @@ export const fetchProfileBegin = () => ({
|
||||
type: FETCH_PROFILE.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchProfileSuccess = (account, preferences, certificates) => ({
|
||||
export const fetchProfileSuccess = (account, preferences, courseCertificates) => ({
|
||||
type: FETCH_PROFILE.SUCCESS,
|
||||
account,
|
||||
preferences,
|
||||
certificates,
|
||||
courseCertificates,
|
||||
});
|
||||
|
||||
export const fetchProfileFailure = error => ({
|
||||
@@ -62,9 +61,9 @@ export const saveProfileReset = () => ({
|
||||
type: SAVE_PROFILE.RESET,
|
||||
});
|
||||
|
||||
export const saveProfileFailure = error => ({
|
||||
export const saveProfileFailure = errors => ({
|
||||
type: SAVE_PROFILE.FAILURE,
|
||||
payload: { error },
|
||||
payload: { errors },
|
||||
});
|
||||
|
||||
// SAVE PROFILE PHOTO ACTIONS
|
||||
@@ -138,16 +137,8 @@ export const closeForm = formId => ({
|
||||
|
||||
// FORM STATE ACTIONS
|
||||
|
||||
export const updateAccountDraft = (name, value) => ({
|
||||
type: UPDATE_ACCOUNT_DRAFT,
|
||||
payload: {
|
||||
name,
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
export const updateVisibilityDraft = (name, value) => ({
|
||||
type: UPDATE_VISIBILITY_DRAFT,
|
||||
export const updateDraft = (name, value) => ({
|
||||
type: UPDATE_DRAFT,
|
||||
payload: {
|
||||
name,
|
||||
value,
|
||||
|
||||
@@ -84,12 +84,12 @@ describe('SAVE profile actions', () => {
|
||||
});
|
||||
|
||||
it('should create an action to signal user account save failure', () => {
|
||||
const error = 'Test failure';
|
||||
const errors = ['Test failure'];
|
||||
const expectedAction = {
|
||||
type: SAVE_PROFILE.FAILURE,
|
||||
payload: { error },
|
||||
payload: { errors },
|
||||
};
|
||||
expect(saveProfileFailure(error)).toEqual(expectedAction);
|
||||
expect(saveProfileFailure(errors)).toEqual(expectedAction);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import snakecaseKeys from 'snakecase-keys';
|
||||
import apiClient from '../config/apiClient';
|
||||
import { configuration } from '../config/environment';
|
||||
import { snakeCaseObject } from '../services/utils';
|
||||
|
||||
const eventLogApiBaseUrl = `${configuration.LMS_BASE_URL}/event`;
|
||||
|
||||
@@ -14,7 +14,7 @@ function handleTrackEvents(eventName, properties) {
|
||||
// Sends events to tracking log and downstream
|
||||
// TODO: Determine consistent naming for eventName vs eventType and properties v eventData.
|
||||
function logEvent(eventType, eventData) {
|
||||
snakecaseKeys(eventData, { deep: true });
|
||||
snakeCaseObject(eventData, { deep: true });
|
||||
const serverData = {
|
||||
event_type: eventType,
|
||||
event: eventData,
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
deleteProfilePhoto,
|
||||
openForm,
|
||||
closeForm,
|
||||
updateVisibilityDraft,
|
||||
updateAccountDraft,
|
||||
updateDraft,
|
||||
} from '../actions/ProfileActions';
|
||||
|
||||
// Components
|
||||
@@ -27,6 +26,7 @@ import Bio from './ProfilePage/Bio';
|
||||
import Certificates from './ProfilePage/Certificates';
|
||||
import AgeMessage from './ProfilePage/AgeMessage';
|
||||
import DateJoined from './ProfilePage/DateJoined';
|
||||
import { profilePageSelector } from '../selectors/ProfilePageSelector';
|
||||
|
||||
export class ProfilePage extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -67,17 +67,30 @@ export class ProfilePage extends React.Component {
|
||||
this.props.saveProfile(formId);
|
||||
}
|
||||
|
||||
handleChange(formId, name, value) {
|
||||
if (name === 'visibility') {
|
||||
this.props.updateVisibilityDraft(formId, value);
|
||||
} else {
|
||||
this.props.updateAccountDraft(formId, value);
|
||||
}
|
||||
handleChange(name, value) {
|
||||
this.props.updateDraft(name, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
profileImage, username, dateJoined, errors,
|
||||
profileImage,
|
||||
username,
|
||||
dateJoined,
|
||||
errors,
|
||||
name,
|
||||
visibilityName,
|
||||
country,
|
||||
visibilityCountry,
|
||||
education,
|
||||
visibilityEducation,
|
||||
socialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
visibilitySocialLinks,
|
||||
languageProficiencies,
|
||||
visibilityLanguageProficiencies,
|
||||
visibilityCourseCertificates,
|
||||
bio,
|
||||
visibilityBio,
|
||||
} = this.props;
|
||||
|
||||
const commonFormProps = {
|
||||
@@ -112,11 +125,37 @@ export class ProfilePage extends React.Component {
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={{ order: 2 }} md={{ size: 4, order: 1 }} lg={3} className="mt-md-4">
|
||||
<Name formId="name" {...commonFormProps} />
|
||||
<Country formId="country" {...commonFormProps} />
|
||||
<PreferredLanguage formId="languageProficiencies" {...commonFormProps} />
|
||||
<Education formId="education" {...commonFormProps} />
|
||||
<SocialLinks formId="socialLinks" {...commonFormProps} />
|
||||
<Name
|
||||
name={name}
|
||||
visibilityName={visibilityName}
|
||||
formId="name"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Country
|
||||
country={country}
|
||||
visibilityCountry={visibilityCountry}
|
||||
formId="country"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<PreferredLanguage
|
||||
languageProficiencies={languageProficiencies}
|
||||
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
|
||||
formId="languageProficiencies"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Education
|
||||
education={education}
|
||||
visibilityEducation={visibilityEducation}
|
||||
formId="education"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<SocialLinks
|
||||
socialLinks={socialLinks}
|
||||
draftSocialLinksByPlatform={draftSocialLinksByPlatform}
|
||||
visibilitySocialLinks={visibilitySocialLinks}
|
||||
formId="socialLinks"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
</Col>
|
||||
<Col
|
||||
xs={{ order: 1 }}
|
||||
@@ -125,8 +164,17 @@ export class ProfilePage extends React.Component {
|
||||
className="mt-4 mt-md-n5"
|
||||
>
|
||||
{this.props.requiresParentalConsent ? <AgeMessage accountURL="#account" /> : null}
|
||||
<Bio formId="bio" {...commonFormProps} />
|
||||
<Certificates formId="certificates" {...commonFormProps} />
|
||||
<Bio
|
||||
bio={bio}
|
||||
visibilityBio={visibilityBio}
|
||||
formId="bio"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
<Certificates
|
||||
visibilityCourseCertificates={visibilityCourseCertificates}
|
||||
formId="certificates"
|
||||
{...commonFormProps}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
@@ -136,30 +184,58 @@ export class ProfilePage extends React.Component {
|
||||
}
|
||||
|
||||
ProfilePage.propTypes = {
|
||||
// Page state helpers
|
||||
currentlyEditingField: PropTypes.string,
|
||||
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
isCurrentUserProfile: PropTypes.bool.isRequired,
|
||||
errors: PropTypes.objectOf(PropTypes.string),
|
||||
|
||||
// Profile data
|
||||
// Account data
|
||||
username: PropTypes.string,
|
||||
requiresParentalConsent: PropTypes.bool,
|
||||
dateJoined: PropTypes.string,
|
||||
profileImage: PropTypes.string,
|
||||
accountPrivacy: PropTypes.string,
|
||||
certificates: PropTypes.arrayOf(PropTypes.shape({
|
||||
isCurrentUserProfile: PropTypes.bool.isRequired,
|
||||
|
||||
// Bio form data
|
||||
bio: PropTypes.string,
|
||||
visibilityBio: PropTypes.string.isRequired,
|
||||
|
||||
// Certificates form data
|
||||
courseCertificates: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
})),
|
||||
visibilityCourseCertificates: PropTypes.string.isRequired,
|
||||
|
||||
// Profile data for form fields
|
||||
// Country form data
|
||||
country: PropTypes.string,
|
||||
visibilityCountry: PropTypes.string.isRequired,
|
||||
|
||||
// Education form data
|
||||
education: PropTypes.string,
|
||||
visibilityEducation: PropTypes.string.isRequired,
|
||||
|
||||
// Language proficiency form data
|
||||
languageProficiencies: PropTypes.arrayOf(PropTypes.shape({
|
||||
code: PropTypes.string.isRequired,
|
||||
})),
|
||||
visibilityLanguageProficiencies: PropTypes.string.isRequired,
|
||||
|
||||
// Name form data
|
||||
name: PropTypes.string,
|
||||
visibilityName: PropTypes.string.isRequired,
|
||||
|
||||
// Social links form data
|
||||
socialLinks: PropTypes.arrayOf(PropTypes.shape({
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
})),
|
||||
bio: PropTypes.string,
|
||||
visibility: PropTypes.objectOf(PropTypes.string),
|
||||
draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
})),
|
||||
visibilitySocialLinks: PropTypes.string.isRequired,
|
||||
|
||||
// Other data we need
|
||||
profileImage: PropTypes.string,
|
||||
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
|
||||
// Page state helpers
|
||||
errors: PropTypes.objectOf(PropTypes.string),
|
||||
|
||||
// Actions
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
@@ -168,8 +244,7 @@ ProfilePage.propTypes = {
|
||||
deleteProfilePhoto: PropTypes.func.isRequired,
|
||||
openForm: PropTypes.func.isRequired,
|
||||
closeForm: PropTypes.func.isRequired,
|
||||
updateVisibilityDraft: PropTypes.func.isRequired,
|
||||
updateAccountDraft: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
|
||||
// Router
|
||||
match: PropTypes.shape({
|
||||
@@ -177,57 +252,28 @@ ProfilePage.propTypes = {
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
yearOfBirth: PropTypes.number,
|
||||
requiresParentalConsent: PropTypes.bool,
|
||||
};
|
||||
|
||||
ProfilePage.defaultProps = {
|
||||
currentlyEditingField: null,
|
||||
saveState: null,
|
||||
savePhotoState: null,
|
||||
errors: {},
|
||||
profileImage: null,
|
||||
name: null,
|
||||
username: null,
|
||||
education: null,
|
||||
country: null,
|
||||
socialLinks: [],
|
||||
draftSocialLinksByPlatform: {},
|
||||
bio: null,
|
||||
certificates: null,
|
||||
accountPrivacy: null,
|
||||
visibility: {}, // eslint-disable-line
|
||||
yearOfBirth: null,
|
||||
languageProficiencies: [],
|
||||
courseCertificates: null,
|
||||
requiresParentalConsent: null,
|
||||
dateJoined: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const profileImage =
|
||||
state.profilePage.account.profileImage != null
|
||||
// TODO: This will change back to camelcase in the future
|
||||
? state.profilePage.account.profileImage.image_url_large
|
||||
: null;
|
||||
return {
|
||||
isCurrentUserProfile: state.userAccount.username === state.profilePage.account.username,
|
||||
currentlyEditingField: state.profilePage.currentlyEditingField,
|
||||
saveState: state.profilePage.saveState,
|
||||
savePhotoState: state.profilePage.savePhotoState,
|
||||
error: state.profilePage.error,
|
||||
profileImage,
|
||||
|
||||
username: state.profilePage.account.username,
|
||||
education: state.profilePage.account.levelOfEducation,
|
||||
socialLinks: state.profilePage.account.socialLinks,
|
||||
bio: state.profilePage.account.bio,
|
||||
certificates: state.profilePage.account.certificates,
|
||||
accountPrivacy: state.profilePage.preferences.accountPrivacy,
|
||||
visibility: state.profilePage.preferences.visibility || {},
|
||||
yearOfBirth: state.profilePage.account.yearOfBirth,
|
||||
requiresParentalConsent: state.profilePage.account.requiresParentalConsent,
|
||||
dateJoined: state.profilePage.account.dateJoined,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
profilePageSelector,
|
||||
{
|
||||
fetchProfile,
|
||||
saveProfilePhoto,
|
||||
@@ -235,7 +281,6 @@ export default connect(
|
||||
saveProfile,
|
||||
openForm,
|
||||
closeForm,
|
||||
updateVisibilityDraft,
|
||||
updateAccountDraft,
|
||||
updateDraft,
|
||||
},
|
||||
)(ProfilePage);
|
||||
|
||||
@@ -27,7 +27,7 @@ class Bio extends React.Component {
|
||||
|
||||
handleChange(e) {
|
||||
const { name, value } = e.target;
|
||||
this.props.changeHandler(this.props.formId, name, value);
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
@@ -45,7 +45,7 @@ class Bio extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, value, visibility, editMode, saveState, error, intl,
|
||||
formId, bio, visibilityBio, editMode, saveState, error, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -61,16 +61,16 @@ class Bio extends React.Component {
|
||||
type="textarea"
|
||||
id={formId}
|
||||
name={formId}
|
||||
value={value}
|
||||
value={bio || ''}
|
||||
invalid={error != null}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormFeedback>{error}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormControls
|
||||
formId={formId}
|
||||
visibilityId="visibilityBio"
|
||||
saveState={saveState}
|
||||
visibility={visibility}
|
||||
visibility={visibilityBio}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
@@ -82,10 +82,10 @@ class Bio extends React.Component {
|
||||
content={intl.formatMessage(messages['profile.bio.about.me'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibility !== null}
|
||||
visibility={visibility}
|
||||
showVisibility={visibilityBio !== null}
|
||||
visibility={visibilityBio}
|
||||
/>
|
||||
<p className="lead">{value}</p>
|
||||
<p className="lead">{bio}</p>
|
||||
</React.Fragment>
|
||||
),
|
||||
empty: (
|
||||
@@ -100,7 +100,7 @@ class Bio extends React.Component {
|
||||
static: (
|
||||
<React.Fragment>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.bio.about.me'])} />
|
||||
<p className="lead">{value}</p>
|
||||
<p className="lead">{bio}</p>
|
||||
</React.Fragment>
|
||||
),
|
||||
}}
|
||||
@@ -117,8 +117,8 @@ Bio.propTypes = {
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
value: PropTypes.string,
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
bio: PropTypes.string,
|
||||
visibilityBio: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
@@ -136,8 +136,8 @@ Bio.propTypes = {
|
||||
Bio.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
value: null,
|
||||
visibility: 'private',
|
||||
bio: null,
|
||||
visibilityBio: 'private',
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class Certificates extends React.Component {
|
||||
|
||||
handleChange(e) {
|
||||
const { name, value } = e.target;
|
||||
this.props.changeHandler(this.props.formId, name, value);
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
@@ -87,7 +87,7 @@ class Certificates extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, visibility, editMode, saveState, intl,
|
||||
visibilityCourseCertificates, editMode, saveState, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -100,9 +100,9 @@ class Certificates extends React.Component {
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.certificates.my.certificates'])} />
|
||||
{this.renderCertificates()}
|
||||
<FormControls
|
||||
formId={formId}
|
||||
visibilityId="visibilityCourseCertificates"
|
||||
saveState={saveState}
|
||||
visibility={visibility}
|
||||
visibility={visibilityCourseCertificates}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
@@ -114,8 +114,8 @@ class Certificates extends React.Component {
|
||||
content={intl.formatMessage(messages['profile.certificates.my.certificates'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibility !== null}
|
||||
visibility={visibility}
|
||||
showVisibility={visibilityCourseCertificates !== null}
|
||||
visibility={visibilityCourseCertificates}
|
||||
/>
|
||||
{this.renderCertificates()}
|
||||
</React.Fragment>
|
||||
@@ -152,7 +152,7 @@ Certificates.propTypes = {
|
||||
certificates: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
})),
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
visibilityCourseCertificates: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
|
||||
@@ -169,7 +169,7 @@ Certificates.propTypes = {
|
||||
Certificates.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
visibility: 'private',
|
||||
visibilityCourseCertificates: 'private',
|
||||
certificates: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class Country extends React.Component {
|
||||
name,
|
||||
value,
|
||||
} = e.target;
|
||||
this.props.changeHandler(this.props.formId, name, value);
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
@@ -49,7 +49,7 @@ class Country extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, value, visibility, editMode, saveState, error,
|
||||
formId, country, visibilityCountry, editMode, saveState, error,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -65,7 +65,7 @@ class Country extends React.Component {
|
||||
type="select"
|
||||
name={formId}
|
||||
className="w-100"
|
||||
value={value}
|
||||
value={country}
|
||||
invalid={error != null}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
@@ -76,9 +76,9 @@ class Country extends React.Component {
|
||||
<FormFeedback>{error}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormControls
|
||||
formId={formId}
|
||||
visibilityId="visibilityCountry"
|
||||
saveState={saveState}
|
||||
visibility={visibility}
|
||||
visibility={visibilityCountry}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
@@ -90,10 +90,10 @@ class Country extends React.Component {
|
||||
content="Location"
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibility !== null}
|
||||
visibility={visibility}
|
||||
showVisibility={visibilityCountry !== null}
|
||||
visibility={visibilityCountry}
|
||||
/>
|
||||
<h5>{ALL_COUNTRIES[value]}</h5>
|
||||
<h5>{ALL_COUNTRIES[country]}</h5>
|
||||
</React.Fragment>
|
||||
),
|
||||
empty: (
|
||||
@@ -108,7 +108,7 @@ class Country extends React.Component {
|
||||
static: (
|
||||
<React.Fragment>
|
||||
<EditableItemHeader content="Location" />
|
||||
<h5>{ALL_COUNTRIES[value]}</h5>
|
||||
<h5>{ALL_COUNTRIES[country]}</h5>
|
||||
</React.Fragment>
|
||||
),
|
||||
}}
|
||||
@@ -125,8 +125,8 @@ Country.propTypes = {
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
value: PropTypes.string,
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
country: PropTypes.string,
|
||||
visibilityCountry: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
@@ -141,8 +141,8 @@ Country.propTypes = {
|
||||
Country.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
value: null,
|
||||
visibility: 'private',
|
||||
country: null,
|
||||
visibilityCountry: 'private',
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class Education extends React.Component {
|
||||
name,
|
||||
value,
|
||||
} = e.target;
|
||||
this.props.changeHandler(this.props.formId, name, value);
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
@@ -51,7 +51,7 @@ class Education extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, value, visibility, editMode, saveState, error, intl,
|
||||
formId, education, visibilityEducation, editMode, saveState, error, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -69,7 +69,7 @@ class Education extends React.Component {
|
||||
type="select"
|
||||
name={formId}
|
||||
className="w-100"
|
||||
value={value}
|
||||
value={education}
|
||||
invalid={error != null}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
@@ -80,9 +80,9 @@ class Education extends React.Component {
|
||||
<FormFeedback>{error}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormControls
|
||||
formId={formId}
|
||||
visibilityId="visibilityEducation"
|
||||
saveState={saveState}
|
||||
visibility={visibility}
|
||||
visibility={visibilityEducation}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
@@ -94,10 +94,10 @@ class Education extends React.Component {
|
||||
content={intl.formatMessage(messages['profile.education.education'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibility !== null}
|
||||
visibility={visibility}
|
||||
showVisibility={visibilityEducation !== null}
|
||||
visibility={visibilityEducation}
|
||||
/>
|
||||
<h5>{EDUCATION[value]}</h5>
|
||||
<h5>{EDUCATION[education]}</h5>
|
||||
</React.Fragment>
|
||||
),
|
||||
empty: (
|
||||
@@ -112,7 +112,7 @@ class Education extends React.Component {
|
||||
static: (
|
||||
<React.Fragment>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.education.education'])} />
|
||||
<h5>{EDUCATION[value]}</h5>
|
||||
<h5>{EDUCATION[education]}</h5>
|
||||
</React.Fragment>
|
||||
),
|
||||
}}
|
||||
@@ -129,8 +129,8 @@ Education.propTypes = {
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
value: PropTypes.string,
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
education: PropTypes.string,
|
||||
visibilityEducation: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
@@ -148,8 +148,8 @@ Education.propTypes = {
|
||||
Education.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
value: null,
|
||||
visibility: 'private',
|
||||
education: null,
|
||||
visibilityEducation: 'private',
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class Name extends React.Component {
|
||||
name,
|
||||
value,
|
||||
} = e.target;
|
||||
this.props.changeHandler(this.props.formId, name, value);
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
@@ -48,7 +48,7 @@ class Name extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, value, visibility, editMode, saveState, error, intl,
|
||||
formId, name, visibilityName, editMode, saveState, error, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -60,7 +60,7 @@ class Name extends React.Component {
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormGroup>
|
||||
<Label for="name">Full Name</Label>
|
||||
<Input type="text" name={formId} value={value} invalid={error != null} onChange={this.handleChange} />
|
||||
<Input type="text" name={formId} value={name} invalid={error != null} onChange={this.handleChange} />
|
||||
<FormText>
|
||||
<FormattedMessage
|
||||
id="profile.name.details"
|
||||
@@ -71,9 +71,9 @@ class Name extends React.Component {
|
||||
<FormFeedback>{error}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormControls
|
||||
formId={formId}
|
||||
visibilityId="visibilityName"
|
||||
saveState={saveState}
|
||||
visibility={visibility}
|
||||
visibility={visibilityName}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
@@ -85,10 +85,10 @@ class Name extends React.Component {
|
||||
content={intl.formatMessage(messages['profile.name.full.name'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibility !== null}
|
||||
visibility={visibility}
|
||||
showVisibility={visibilityName !== null}
|
||||
visibility={visibilityName}
|
||||
/>
|
||||
<h5>{value}</h5>
|
||||
<h5>{name}</h5>
|
||||
</React.Fragment>
|
||||
),
|
||||
empty: (
|
||||
@@ -103,7 +103,7 @@ class Name extends React.Component {
|
||||
static: (
|
||||
<React.Fragment>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.name.full.name'])} />
|
||||
<h5>{value}</h5>
|
||||
<h5>{name}</h5>
|
||||
</React.Fragment>
|
||||
),
|
||||
}}
|
||||
@@ -120,8 +120,8 @@ Name.propTypes = {
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
value: PropTypes.string,
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
name: PropTypes.string,
|
||||
visibilityName: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
@@ -139,8 +139,8 @@ Name.propTypes = {
|
||||
Name.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
value: null,
|
||||
visibility: 'private',
|
||||
name: null,
|
||||
visibilityName: 'private',
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ class PreferredLanguage extends React.Component {
|
||||
value = [{ code: rawValue }];
|
||||
}
|
||||
|
||||
this.props.changeHandler(this.props.formId, name, value);
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
@@ -57,10 +57,10 @@ class PreferredLanguage extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, value: valueArr, visibility, editMode, saveState, error,
|
||||
formId, languageProficiencies, visibilityLanguageProficiencies, editMode, saveState, error,
|
||||
} = this.props;
|
||||
|
||||
const value = valueArr.length ? valueArr[0].code : '';
|
||||
const value = languageProficiencies.length ? languageProficiencies[0].code : '';
|
||||
|
||||
return (
|
||||
<SwitchContent
|
||||
@@ -92,9 +92,9 @@ class PreferredLanguage extends React.Component {
|
||||
<FormFeedback>{error}</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormControls
|
||||
formId={formId}
|
||||
visibilityId="visibilityLanguageProficiencies"
|
||||
saveState={saveState}
|
||||
visibility={visibility}
|
||||
visibility={visibilityLanguageProficiencies}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
@@ -112,8 +112,8 @@ class PreferredLanguage extends React.Component {
|
||||
)}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibility !== null}
|
||||
visibility={visibility}
|
||||
showVisibility={visibilityLanguageProficiencies !== null}
|
||||
visibility={visibilityLanguageProficiencies}
|
||||
/>
|
||||
<h5>{ALL_LANGUAGES[value]}</h5>
|
||||
</React.Fragment>
|
||||
@@ -155,13 +155,13 @@ PreferredLanguage.propTypes = {
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
value: PropTypes.oneOfType([
|
||||
languageProficiencies: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string })),
|
||||
// TODO: ProfilePageSelector should supply null values
|
||||
// instead of empty strings when no value exists
|
||||
PropTypes.oneOf(['']),
|
||||
]),
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
visibilityLanguageProficiencies: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
@@ -176,8 +176,8 @@ PreferredLanguage.propTypes = {
|
||||
PreferredLanguage.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
value: null,
|
||||
visibility: 'private',
|
||||
languageProficiencies: [],
|
||||
visibilityLanguageProficiencies: 'private',
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form, Input, FormFeedback } from 'reactstrap';
|
||||
import { Form, Input, FormFeedback, Alert } from 'reactstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons';
|
||||
@@ -43,24 +43,40 @@ class SocialLinks extends React.Component {
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
} = e.target;
|
||||
const { name, value } = e.target;
|
||||
|
||||
if (name !== 'visibility') {
|
||||
const updatedList = this.props.committedValue.map((socialLink) => {
|
||||
if (socialLink.platform === name) {
|
||||
return { platform: name, social_link: value };
|
||||
}
|
||||
return socialLink;
|
||||
});
|
||||
this.props.changeHandler(this.props.formId, 'socialLinks', updatedList);
|
||||
// The social links are a bit special. If we're updating them, we need to merge them
|
||||
// with any existing social link drafts, essentially sending a fresh copy of the whole
|
||||
// data structure back to the reducer. This helps the reducer stay simple and keeps
|
||||
// special cases out of it, concentrating them here, where they began.
|
||||
if (name !== 'visibilitySocialLinks') {
|
||||
this.props.changeHandler(
|
||||
'socialLinks',
|
||||
this.mergeWithDrafts({
|
||||
platform: name,
|
||||
// If it's an empty string, send it as null.
|
||||
// The empty string is just for the input. We want nulls.
|
||||
socialLink: value,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.props.changeHandler(this.props.formId, name, value);
|
||||
this.props.changeHandler(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
mergeWithDrafts(newSocialLink) {
|
||||
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
|
||||
const updated = [];
|
||||
knownPlatforms.forEach((platform) => {
|
||||
if (newSocialLink.platform === platform) {
|
||||
updated.push(newSocialLink);
|
||||
} else if (this.props.draftSocialLinksByPlatform[platform] !== undefined) {
|
||||
updated.push(this.props.draftSocialLinksByPlatform[platform]);
|
||||
}
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.submitHandler(this.props.formId);
|
||||
@@ -76,7 +92,7 @@ class SocialLinks extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
formId, value: values, visibility, editMode, saveState, error, intl,
|
||||
socialLinks, visibilitySocialLinks, editMode, saveState, error, intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -86,7 +102,7 @@ class SocialLinks extends React.Component {
|
||||
cases={{
|
||||
empty: (
|
||||
<ul className="list-unstyled">
|
||||
{values.map(({ platform }) => (
|
||||
{socialLinks.map(({ platform }) => (
|
||||
<EmptyListItem
|
||||
key={platform}
|
||||
onClick={this.handleOpen}
|
||||
@@ -97,9 +113,11 @@ class SocialLinks extends React.Component {
|
||||
),
|
||||
static: (
|
||||
<React.Fragment>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.sociallinks.social.links'])} />
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
|
||||
/>
|
||||
<ul className="list-unstyled">
|
||||
{values.map(({ platform, social_link: socialLink }) => (
|
||||
{socialLinks.map(({ platform, socialLink }) => (
|
||||
<StaticListItem
|
||||
key={platform}
|
||||
name={platformDisplayInfo[platform].name}
|
||||
@@ -116,11 +134,11 @@ class SocialLinks extends React.Component {
|
||||
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
|
||||
showEditButton
|
||||
onClickEdit={this.handleOpen}
|
||||
showVisibility={visibility !== null}
|
||||
visibility={visibility}
|
||||
showVisibility={visibilitySocialLinks !== null}
|
||||
visibility={visibilitySocialLinks}
|
||||
/>
|
||||
<ul className="list-unstyled">
|
||||
{values.map(({ platform, social_link: socialLink }) => (
|
||||
{socialLinks.map(({ platform, socialLink }) => (
|
||||
<EditableListItem
|
||||
key={platform}
|
||||
platform={platform}
|
||||
@@ -134,23 +152,27 @@ class SocialLinks extends React.Component {
|
||||
),
|
||||
editing: (
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<EditableItemHeader content={intl.formatMessage(messages['profile.sociallinks.social.links'])} />
|
||||
<EditableItemHeader
|
||||
content={intl.formatMessage(messages['profile.sociallinks.social.links'])}
|
||||
/>
|
||||
{/* TODO: Replace this alert with per-field errors. Needs API update. */}
|
||||
{error !== null ? <Alert color="danger">{error}</Alert> : null}
|
||||
<ul className="list-unstyled">
|
||||
{values.map(({ platform, social_link: socialLink }) => (
|
||||
{socialLinks.map(({ platform, socialLink }) => (
|
||||
<EditingListItem
|
||||
key={platform}
|
||||
name={platformDisplayInfo[platform].name}
|
||||
platform={platform}
|
||||
value={socialLink}
|
||||
error={error !== null ? error[platform] : null}
|
||||
/* TODO: Per-field errors: error={error !== null ? error[platform] : null} */
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<FormControls
|
||||
formId={formId}
|
||||
visibilityId="visibilitySocialLinks"
|
||||
saveState={saveState}
|
||||
visibility={visibility}
|
||||
visibility={visibilitySocialLinks}
|
||||
cancelHandler={this.handleClose}
|
||||
changeHandler={this.handleChange}
|
||||
/>
|
||||
@@ -170,15 +192,15 @@ SocialLinks.propTypes = {
|
||||
formId: PropTypes.string.isRequired,
|
||||
|
||||
// From Selector
|
||||
value: PropTypes.arrayOf(PropTypes.shape({
|
||||
socialLinks: PropTypes.arrayOf(PropTypes.shape({
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
})).isRequired,
|
||||
draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
})),
|
||||
committedValue: PropTypes.arrayOf(PropTypes.shape({
|
||||
platform: PropTypes.string,
|
||||
socialLink: PropTypes.string,
|
||||
})),
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
visibilitySocialLinks: PropTypes.oneOf(['private', 'all_users']),
|
||||
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
|
||||
saveState: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
@@ -196,9 +218,8 @@ SocialLinks.propTypes = {
|
||||
SocialLinks.defaultProps = {
|
||||
editMode: 'static',
|
||||
saveState: null,
|
||||
value: [],
|
||||
committedValue: [],
|
||||
visibility: 'private',
|
||||
draftSocialLinksByPlatform: {},
|
||||
visibilitySocialLinks: 'private',
|
||||
error: null,
|
||||
};
|
||||
|
||||
@@ -222,16 +243,14 @@ SocialLink.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
function EditableListItem({
|
||||
url,
|
||||
platform,
|
||||
onClickEmptyContent,
|
||||
name,
|
||||
url, platform, onClickEmptyContent, name,
|
||||
}) {
|
||||
const linkDisplay = url ?
|
||||
<SocialLink name={name} url={url} platform={platform} /> :
|
||||
<EmptyContent onClick={onClickEmptyContent}>Add {name}</EmptyContent>;
|
||||
const linkDisplay = url ? (
|
||||
<SocialLink name={name} url={url} platform={platform} />
|
||||
) : (
|
||||
<EmptyContent onClick={onClickEmptyContent}>Add {name}</EmptyContent>
|
||||
);
|
||||
|
||||
return <li className="form-group">{linkDisplay}</li>;
|
||||
}
|
||||
@@ -247,13 +266,8 @@ EditableListItem.defaultProps = {
|
||||
onClickEmptyContent: null,
|
||||
};
|
||||
|
||||
|
||||
function EditingListItem({
|
||||
platform,
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
error,
|
||||
platform, name, value, onChange, error,
|
||||
}) {
|
||||
return (
|
||||
<li className="form-group">
|
||||
@@ -261,7 +275,7 @@ function EditingListItem({
|
||||
<Input
|
||||
type="text"
|
||||
name={platform}
|
||||
value={value}
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
invalid={error != null}
|
||||
/>
|
||||
@@ -283,7 +297,6 @@ EditingListItem.defaultProps = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
|
||||
function EmptyListItem({ onClick, name }) {
|
||||
return (
|
||||
<li className="mb-4">
|
||||
@@ -292,7 +305,7 @@ function EmptyListItem({ onClick, name }) {
|
||||
id="profile.sociallinks.add"
|
||||
defaultMessage="Add {network}"
|
||||
values={{
|
||||
network: { name },
|
||||
network: name,
|
||||
}}
|
||||
description="{network} is the name of a social network such as Facebook or Twitter"
|
||||
/>
|
||||
@@ -306,7 +319,6 @@ EmptyListItem.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
function StaticListItem({ name, url, platform }) {
|
||||
return (
|
||||
<li className="mb-2">
|
||||
@@ -317,6 +329,10 @@ function StaticListItem({ name, url, platform }) {
|
||||
|
||||
StaticListItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
url: PropTypes.string,
|
||||
platform: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
StaticListItem.defaultProps = {
|
||||
url: null,
|
||||
};
|
||||
|
||||
@@ -9,9 +9,11 @@ import AsyncActionButton from './AsyncActionButton';
|
||||
import { VisibilitySelect } from './Visibility';
|
||||
|
||||
function FormControls({
|
||||
formId, cancelHandler, changeHandler, visibility, saveState, intl,
|
||||
cancelHandler, changeHandler, visibility, visibilityId, saveState, intl,
|
||||
}) {
|
||||
const visibilityId = `${formId}-visibility`;
|
||||
// Eliminate error/failed state for save button
|
||||
const buttonState = saveState === 'error' ? null : saveState;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<FormGroup className="mb-4">
|
||||
@@ -22,7 +24,7 @@ function FormControls({
|
||||
id={visibilityId}
|
||||
className="w-auto"
|
||||
type="select"
|
||||
name="visibility"
|
||||
name={visibilityId}
|
||||
value={visibility}
|
||||
onChange={changeHandler}
|
||||
/>
|
||||
@@ -30,12 +32,11 @@ function FormControls({
|
||||
<FormGroup>
|
||||
<AsyncActionButton
|
||||
type="submit"
|
||||
variant={saveState}
|
||||
variant={buttonState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['profile.formcontrols.button.save']),
|
||||
pending: intl.formatMessage(messages['profile.formcontrols.button.saving']),
|
||||
complete: intl.formatMessage(messages['profile.formcontrols.button.saved']),
|
||||
error: intl.formatMessage(messages['profile.formcontrols.button.save.failed']),
|
||||
}}
|
||||
/>
|
||||
<Button color="link" onClick={cancelHandler}>
|
||||
@@ -49,9 +50,9 @@ function FormControls({
|
||||
export default injectIntl(FormControls);
|
||||
|
||||
FormControls.propTypes = {
|
||||
formId: PropTypes.string.isRequired,
|
||||
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
|
||||
visibility: PropTypes.oneOf(['private', 'all_users']),
|
||||
visibilityId: PropTypes.string.isRequired,
|
||||
cancelHandler: PropTypes.func.isRequired,
|
||||
changeHandler: PropTypes.func.isRequired,
|
||||
|
||||
|
||||
@@ -26,11 +26,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Saved',
|
||||
description: 'A button label',
|
||||
},
|
||||
'profile.formcontrols.button.save.failed': {
|
||||
id: 'profile.formcontrols.button.save.failed',
|
||||
defaultMessage: 'Save Failed',
|
||||
description: 'A button label',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
CLOSE_FORM,
|
||||
OPEN_FORM,
|
||||
FETCH_PROFILE,
|
||||
UPDATE_ACCOUNT_DRAFT,
|
||||
UPDATE_VISIBILITY_DRAFT,
|
||||
UPDATE_DRAFT,
|
||||
RESET_DRAFTS,
|
||||
} from '../actions/ProfileActions';
|
||||
|
||||
@@ -18,12 +17,9 @@ export const initialState = {
|
||||
account: {
|
||||
socialLinks: [],
|
||||
},
|
||||
preferences: {
|
||||
visibility: {},
|
||||
},
|
||||
certificates: [],
|
||||
accountDrafts: {},
|
||||
visibilityDrafts: {},
|
||||
preferences: {},
|
||||
courseCertificates: [],
|
||||
drafts: {},
|
||||
};
|
||||
|
||||
const profilePage = (state = initialState, action) => {
|
||||
@@ -33,7 +29,7 @@ const profilePage = (state = initialState, action) => {
|
||||
...state,
|
||||
account: action.account,
|
||||
preferences: action.preferences,
|
||||
certificates: action.certificates,
|
||||
courseCertificates: action.courseCertificates,
|
||||
};
|
||||
case SAVE_PROFILE.BEGIN:
|
||||
return {
|
||||
@@ -49,13 +45,7 @@ const profilePage = (state = initialState, action) => {
|
||||
// Account is always replaced completely.
|
||||
account: action.payload.account !== null ? action.payload.account : state.account,
|
||||
// Preferences changes get merged in.
|
||||
preferences: Object.assign({}, state.preferences, {
|
||||
visibility: Object.assign(
|
||||
{},
|
||||
state.preferences.visibility,
|
||||
action.payload.preferences.visibility,
|
||||
),
|
||||
}),
|
||||
preferences: Object.assign({}, state.preferences, action.payload.preferences),
|
||||
};
|
||||
case SAVE_PROFILE.FAILURE:
|
||||
return {
|
||||
@@ -120,18 +110,10 @@ const profilePage = (state = initialState, action) => {
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case UPDATE_ACCOUNT_DRAFT:
|
||||
case UPDATE_DRAFT:
|
||||
return {
|
||||
...state,
|
||||
accountDrafts: Object.assign({}, state.accountDrafts, {
|
||||
[action.payload.name]: action.payload.value,
|
||||
}),
|
||||
};
|
||||
|
||||
case UPDATE_VISIBILITY_DRAFT:
|
||||
return {
|
||||
...state,
|
||||
visibilityDrafts: Object.assign({}, state.visibilityDrafts, {
|
||||
drafts: Object.assign({}, state.drafts, {
|
||||
[action.payload.name]: action.payload.value,
|
||||
}),
|
||||
};
|
||||
@@ -139,13 +121,13 @@ const profilePage = (state = initialState, action) => {
|
||||
case RESET_DRAFTS:
|
||||
return {
|
||||
...state,
|
||||
accountDrafts: {},
|
||||
visibilityDrafts: {},
|
||||
drafts: {},
|
||||
};
|
||||
case OPEN_FORM:
|
||||
return {
|
||||
...state,
|
||||
currentlyEditingField: action.payload.formId,
|
||||
drafts: {},
|
||||
};
|
||||
case CLOSE_FORM:
|
||||
// Only close if the field to close is undefined or matches the field that is currently open
|
||||
@@ -153,6 +135,7 @@ const profilePage = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
currentlyEditingField: null,
|
||||
drafts: {},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
||||
@@ -33,6 +33,9 @@ import { handleSaveProfileSelector } from '../selectors/ProfilePageSelector';
|
||||
// Services
|
||||
import * as ProfileApiService from '../services/ProfileApiService';
|
||||
|
||||
// Utils
|
||||
import { keepKeys } from '../services/utils';
|
||||
|
||||
export function* handleFetchProfile(action) {
|
||||
const { username } = action.payload;
|
||||
const currentUsername = yield select(state => state.authentication.username); // eslint-disable-line
|
||||
@@ -52,11 +55,11 @@ export function* handleFetchProfile(action) {
|
||||
const result = yield all(calls);
|
||||
|
||||
if (result.length > 2) {
|
||||
const [account, certificates, preferences] = result;
|
||||
yield put(fetchProfileSuccess(account, preferences, certificates));
|
||||
const [account, courseCertificates, preferences] = result;
|
||||
yield put(fetchProfileSuccess(account, preferences, courseCertificates));
|
||||
} else {
|
||||
const [account, certificates] = result;
|
||||
yield put(fetchProfileSuccess(account, { visibility: {} }, certificates));
|
||||
const [account, courseCertificates] = result;
|
||||
yield put(fetchProfileSuccess(account, {}, courseCertificates));
|
||||
}
|
||||
|
||||
yield put(fetchProfileReset());
|
||||
@@ -66,35 +69,62 @@ export function* handleFetchProfile(action) {
|
||||
}
|
||||
|
||||
export function* handleSaveProfile(action) {
|
||||
const { username, accountDrafts, visibilityDrafts } = yield select(handleSaveProfileSelector);
|
||||
|
||||
try {
|
||||
const { username, drafts, preferences } = yield select(handleSaveProfileSelector);
|
||||
|
||||
const accountDrafts = keepKeys(drafts, [
|
||||
'bio',
|
||||
'courseCertificates',
|
||||
'country',
|
||||
'education',
|
||||
'languageProficiencies',
|
||||
'name',
|
||||
'socialLinks',
|
||||
]);
|
||||
|
||||
const preferencesDrafts = keepKeys(drafts, [
|
||||
'visibilityBio',
|
||||
'visibilityCourseCertificates',
|
||||
'visibilityCountry',
|
||||
'visibilityEducation',
|
||||
'visibilityLanguageProficiencies',
|
||||
'visibilityName',
|
||||
'visibilitySocialLinks',
|
||||
]);
|
||||
|
||||
if (Object.keys(preferencesDrafts).length > 0) {
|
||||
preferencesDrafts.accountPrivacy = 'custom';
|
||||
}
|
||||
|
||||
yield put(saveProfileBegin());
|
||||
let accountResult = null;
|
||||
// Build the visibility drafts into a structure the API expects.
|
||||
const preferences = {
|
||||
visibility: visibilityDrafts,
|
||||
};
|
||||
|
||||
if (Object.keys(accountDrafts).length > 0) {
|
||||
accountResult = yield call(ProfileApiService.patchProfile, username, accountDrafts);
|
||||
}
|
||||
|
||||
if (Object.keys(visibilityDrafts).length > 0) {
|
||||
yield call(ProfileApiService.patchPreferences, username, preferences);
|
||||
let preferencesResult = preferences; // assume it hasn't changed.
|
||||
if (Object.keys(preferencesDrafts).length > 0) {
|
||||
yield call(ProfileApiService.patchPreferences, username, preferencesDrafts);
|
||||
// TODO: Temporary deoptimization since the patchPreferences call doesn't return anything.
|
||||
// Remove this second call once we can get a result from the one above.
|
||||
preferencesResult = yield call(ProfileApiService.getPreferences, username);
|
||||
}
|
||||
|
||||
// The account result is returned from the server.
|
||||
// The preferences draft is valid if the server didn't complain, so
|
||||
// pass it through directly.
|
||||
yield put(saveProfileSuccess(accountResult, preferences));
|
||||
yield put(saveProfileSuccess(accountResult, preferencesResult));
|
||||
yield delay(300);
|
||||
yield put(closeForm(action.payload.formId));
|
||||
yield delay(300);
|
||||
yield put(saveProfileReset());
|
||||
yield put(resetDrafts());
|
||||
} catch (e) {
|
||||
yield put(saveProfileFailure(e.message));
|
||||
// TODO: If this is any other kind of exception than a known validation error from the server,
|
||||
// this code will fail gracelessly when it can't find fieldErrors on the error.
|
||||
yield put(saveProfileFailure(e.fieldErrors));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,14 @@ describe('RootSaga', () => {
|
||||
it('should pass actions to the correct sagas', () => {
|
||||
const gen = rootSaga();
|
||||
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile)); // eslint-disable-line
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.SAVE_PROFILE.BASE, handleSaveProfile)); // eslint-disable-line
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto)); // eslint-disable-line
|
||||
expect(gen.next().value).toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto)); // eslint-disable-line
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile));
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.SAVE_PROFILE.BASE, handleSaveProfile));
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto));
|
||||
expect(gen.next().value)
|
||||
.toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto));
|
||||
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
@@ -39,12 +43,12 @@ describe('RootSaga', () => {
|
||||
describe('handleSaveProfile', () => {
|
||||
const selectorData = {
|
||||
username: 'my username',
|
||||
accountDrafts: {
|
||||
drafts: {
|
||||
name: 'Full Name',
|
||||
},
|
||||
visibilityDrafts: {},
|
||||
preferences: {},
|
||||
};
|
||||
beforeEach(() => {});
|
||||
|
||||
it('should successfully process a saveProfile request if there are no exceptions', () => {
|
||||
const action = profileActions.saveProfile('ze form id');
|
||||
const gen = handleSaveProfile(action);
|
||||
@@ -59,9 +63,7 @@ describe('RootSaga', () => {
|
||||
}));
|
||||
// The library would supply the result of the above call
|
||||
// as the parameter to the NEXT yield. Here:
|
||||
expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess(profile, {
|
||||
visibility: {},
|
||||
})));
|
||||
expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess(profile, {})));
|
||||
expect(gen.next().value).toEqual(delay(300));
|
||||
expect(gen.next().value).toEqual(put(profileActions.closeForm('ze form id')));
|
||||
expect(gen.next().value).toEqual(delay(300));
|
||||
@@ -72,6 +74,9 @@ describe('RootSaga', () => {
|
||||
|
||||
it('should successfully publish a failure action on exception', () => {
|
||||
const error = new Error('uhoh');
|
||||
error.fieldErrors = {
|
||||
uhoh: 'not good',
|
||||
};
|
||||
const action = profileActions.saveProfile(
|
||||
'my username',
|
||||
{
|
||||
@@ -85,7 +90,7 @@ describe('RootSaga', () => {
|
||||
expect(gen.next().value).toEqual(select(handleSaveProfileSelector));
|
||||
expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin()));
|
||||
const result = gen.throw(error);
|
||||
expect(result.value).toEqual(put(profileActions.saveProfileFailure('uhoh')));
|
||||
expect(result.value).toEqual(put(profileActions.saveProfileFailure({ uhoh: 'not good' })));
|
||||
expect(gen.next().value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,14 @@ import { createSelector } from 'reselect';
|
||||
export const formIdSelector = (state, props) => props.formId;
|
||||
export const authenticationUsernameSelector = state => state.authentication.username;
|
||||
export const profileAccountSelector = state => state.profilePage.account;
|
||||
export const profileCertificatesSelector = state => state.profilePage.certificates;
|
||||
export const profileDraftsSelector = state => state.profilePage.drafts;
|
||||
export const accountPrivacySelector = state => state.profilePage.preferences.accountPrivacy;
|
||||
export const profilePreferencesSelector = state => state.profilePage.preferences;
|
||||
export const profileCourseCertificatesSelector = state => state.profilePage.courseCertificates;
|
||||
export const profileAccountDraftsSelector = state => state.profilePage.accountDrafts;
|
||||
export const profileVisibilityDraftsSelector = state => state.profilePage.visibilityDrafts;
|
||||
export const profileVisibilitySelector = state => state.profilePage.preferences.visibility;
|
||||
export const saveStateSelector = state => state.profilePage.saveState;
|
||||
export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
|
||||
export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField;
|
||||
export const accountErrorsSelector = state => state.profilePage.errors;
|
||||
|
||||
@@ -19,7 +22,7 @@ export const isCurrentUserProfileSelector = createSelector(
|
||||
|
||||
export const editableFormModeSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
profileCertificatesSelector,
|
||||
profileCourseCertificatesSelector,
|
||||
formIdSelector,
|
||||
isCurrentUserProfileSelector,
|
||||
currentlyEditingFieldSelector,
|
||||
@@ -51,8 +54,8 @@ export const editableFormModeSelector = createSelector(
|
||||
|
||||
export const accountDraftsFieldSelector = createSelector(
|
||||
formIdSelector,
|
||||
profileAccountDraftsSelector,
|
||||
(formId, accountDrafts) => accountDrafts[formId],
|
||||
profileDraftsSelector,
|
||||
(formId, drafts) => drafts[formId],
|
||||
);
|
||||
|
||||
export const visibilityDraftsFieldSelector = createSelector(
|
||||
@@ -69,26 +72,9 @@ export const formErrorSelector = createSelector(
|
||||
|
||||
export const editableFormSelector = createSelector(
|
||||
editableFormModeSelector,
|
||||
profileAccountSelector,
|
||||
profileVisibilitySelector,
|
||||
formIdSelector,
|
||||
formErrorSelector,
|
||||
saveStateSelector,
|
||||
accountDraftsFieldSelector,
|
||||
visibilityDraftsFieldSelector,
|
||||
(
|
||||
editMode,
|
||||
account,
|
||||
visibility,
|
||||
formId,
|
||||
error,
|
||||
saveState,
|
||||
accountDraftsField,
|
||||
visibilityDraftsField,
|
||||
) => ({
|
||||
value: accountDraftsField || account[formId] || '',
|
||||
committedValue: account[formId] || '',
|
||||
visibility: visibilityDraftsField || visibility[formId] || 'private',
|
||||
(editMode, error, saveState) => ({
|
||||
editMode,
|
||||
error,
|
||||
saveState,
|
||||
@@ -97,7 +83,7 @@ export const editableFormSelector = createSelector(
|
||||
|
||||
export const certificatesSelector = createSelector(
|
||||
editableFormSelector,
|
||||
profileCertificatesSelector,
|
||||
profileCourseCertificatesSelector,
|
||||
(editableForm, certificates) => ({
|
||||
...editableForm,
|
||||
certificates,
|
||||
@@ -105,16 +91,223 @@ export const certificatesSelector = createSelector(
|
||||
}),
|
||||
);
|
||||
|
||||
export const profileImageSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
account => (account.profileImage != null ? account.profileImage.imageUrlLarge : null),
|
||||
);
|
||||
|
||||
/**
|
||||
* This is used by a saga to pull out data to process.
|
||||
*/
|
||||
export const handleSaveProfileSelector = createSelector(
|
||||
authenticationUsernameSelector,
|
||||
profileAccountDraftsSelector,
|
||||
profileVisibilityDraftsSelector,
|
||||
(username, accountDrafts, visibilityDrafts) => ({
|
||||
profileDraftsSelector,
|
||||
profilePreferencesSelector,
|
||||
(username, drafts, preferences) => ({
|
||||
username,
|
||||
accountDrafts,
|
||||
visibilityDrafts,
|
||||
drafts,
|
||||
preferences,
|
||||
}),
|
||||
);
|
||||
|
||||
// Reformats the social links in a platform-keyed hash.
|
||||
const socialLinksByPlatformSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
(account) => {
|
||||
const linksByPlatform = {};
|
||||
if (account.socialLinks !== undefined) {
|
||||
account.socialLinks.forEach((socialLink) => {
|
||||
linksByPlatform[socialLink.platform] = socialLink;
|
||||
});
|
||||
}
|
||||
return linksByPlatform;
|
||||
},
|
||||
);
|
||||
|
||||
const draftSocialLinksByPlatformSelector = createSelector(
|
||||
profileDraftsSelector,
|
||||
(drafts) => {
|
||||
const linksByPlatform = {};
|
||||
if (drafts.socialLinks !== undefined) {
|
||||
drafts.socialLinks.forEach((socialLink) => {
|
||||
linksByPlatform[socialLink.platform] = socialLink;
|
||||
});
|
||||
}
|
||||
return linksByPlatform;
|
||||
},
|
||||
);
|
||||
|
||||
// Fleshes out our list of existing social links with all the other ones the user can set.
|
||||
export const formSocialLinksSelector = createSelector(
|
||||
socialLinksByPlatformSelector,
|
||||
draftSocialLinksByPlatformSelector,
|
||||
(linksByPlatform, draftLinksByPlatform) => {
|
||||
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
|
||||
const socialLinks = [];
|
||||
// For each known platform
|
||||
knownPlatforms.forEach((platform) => {
|
||||
// If the link is in our drafts.
|
||||
if (draftLinksByPlatform[platform] !== undefined) {
|
||||
// Use the draft one.
|
||||
socialLinks.push(draftLinksByPlatform[platform]);
|
||||
} else if (linksByPlatform[platform] !== undefined) {
|
||||
// Otherwise use the real one.
|
||||
socialLinks.push(linksByPlatform[platform]);
|
||||
} else {
|
||||
// And if it's not in either, use a stub.
|
||||
socialLinks.push({
|
||||
platform,
|
||||
socialLink: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
return socialLinks;
|
||||
},
|
||||
);
|
||||
|
||||
export const visibilitiesSelector = createSelector(
|
||||
profilePreferencesSelector,
|
||||
accountPrivacySelector,
|
||||
(preferences, accountPrivacy) => {
|
||||
switch (accountPrivacy) {
|
||||
case 'all_users':
|
||||
return {
|
||||
visibilityBio: 'all_users',
|
||||
visibilityCourseCertificates: 'all_users',
|
||||
visibilityCountry: 'all_users',
|
||||
visibilityEducation: 'all_users',
|
||||
visibilityLanguageProficiencies: 'all_users',
|
||||
visibilityName: 'all_users',
|
||||
visibilitySocialLinks: 'all_users',
|
||||
};
|
||||
case 'custom':
|
||||
return {
|
||||
visibilityBio: preferences.visibilityBio || 'private',
|
||||
visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'private',
|
||||
visibilityCountry: preferences.visibilityCountry || 'private',
|
||||
visibilityEducation: preferences.visibilityEducation || 'private',
|
||||
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'private',
|
||||
visibilityName: preferences.visibilityName || 'private',
|
||||
visibilitySocialLinks: preferences.visibilitySocialLinks || 'private',
|
||||
};
|
||||
case 'private':
|
||||
default:
|
||||
// If there is some other value for accountPrivacy set, we're going to assume
|
||||
// it's private, since that's safest.
|
||||
return {
|
||||
visibilityBio: 'private',
|
||||
visibilityCourseCertificates: 'private',
|
||||
visibilityCountry: 'private',
|
||||
visibilityEducation: 'private',
|
||||
visibilityLanguageProficiencies: 'private',
|
||||
visibilityName: 'private',
|
||||
visibilitySocialLinks: 'private',
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* If there's no draft present at all (undefined), use the original committed value.
|
||||
*/
|
||||
function chooseFormValue(draft, committed) {
|
||||
return draft !== undefined ? draft : committed;
|
||||
}
|
||||
|
||||
export const formValuesSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
visibilitiesSelector,
|
||||
profileDraftsSelector,
|
||||
profileCourseCertificatesSelector,
|
||||
formSocialLinksSelector,
|
||||
(account, visibilities, drafts, courseCertificates, socialLinks) => ({
|
||||
bio: chooseFormValue(drafts.bio, account.bio),
|
||||
visibilityBio: chooseFormValue(drafts.visibilityBio, visibilities.visibilityBio),
|
||||
courseCertificates,
|
||||
visibilityCourseCertificates: chooseFormValue(
|
||||
drafts.visibilityCourseCertificates,
|
||||
visibilities.visibilityCourseCertificates,
|
||||
),
|
||||
country: chooseFormValue(drafts.country, account.country),
|
||||
visibilityCountry: chooseFormValue(drafts.visibilityCountry, visibilities.visibilityCountry),
|
||||
education: chooseFormValue(drafts.education, account.education),
|
||||
visibilityEducation: chooseFormValue(
|
||||
drafts.visibilityEducation,
|
||||
visibilities.visibilityEducation,
|
||||
),
|
||||
languageProficiencies: chooseFormValue(
|
||||
drafts.languageProficiencies,
|
||||
account.languageProficiencies,
|
||||
),
|
||||
visibilityLanguageProficiencies: chooseFormValue(
|
||||
drafts.visibilityLanguageProficiencies,
|
||||
visibilities.visibilityLanguageProficiencies,
|
||||
),
|
||||
name: chooseFormValue(drafts.name, account.name),
|
||||
visibilityName: chooseFormValue(drafts.visibilityName, visibilities.visibilityName),
|
||||
socialLinks, // Social links is calculated in its own selector, since it's complicated.
|
||||
visibilitySocialLinks: chooseFormValue(
|
||||
drafts.visibilitySocialLinks,
|
||||
visibilities.visibilitySocialLinks,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
export const profilePageSelector = createSelector(
|
||||
profileAccountSelector,
|
||||
formValuesSelector,
|
||||
profileImageSelector,
|
||||
saveStateSelector,
|
||||
savePhotoStateSelector,
|
||||
isCurrentUserProfileSelector,
|
||||
draftSocialLinksByPlatformSelector,
|
||||
(
|
||||
account,
|
||||
formValues,
|
||||
profileImage,
|
||||
saveState,
|
||||
savePhotoState,
|
||||
isCurrentUserProfile,
|
||||
draftSocialLinksByPlatform,
|
||||
) => ({
|
||||
// Account data we need
|
||||
username: account.username,
|
||||
profileImage,
|
||||
requiresParentalConsent: account.requiresParentalConsent,
|
||||
dateJoined: account.dateJoined,
|
||||
|
||||
// Bio form data
|
||||
bio: formValues.bio,
|
||||
visibilityBio: formValues.visibilityBio,
|
||||
|
||||
// Certificates form data
|
||||
courseCertificates: formValues.courseCertificates,
|
||||
visibilityCourseCertificates: formValues.visibilityCourseCertificates,
|
||||
|
||||
// Country form data
|
||||
country: formValues.country,
|
||||
visibilityCountry: formValues.visibilityCountry,
|
||||
|
||||
// Education form data
|
||||
education: formValues.education,
|
||||
visibilityEducation: formValues.visibilityEducation,
|
||||
|
||||
// Language proficiency form data
|
||||
languageProficiencies: formValues.languageProficiencies,
|
||||
visibilityLanguageProficiencies: formValues.visibilityLanguageProficiencies,
|
||||
|
||||
// Name form data
|
||||
name: formValues.name,
|
||||
visibilityName: formValues.visibilityName,
|
||||
|
||||
// Social links form data
|
||||
socialLinks: formValues.socialLinks,
|
||||
visibilitySocialLinks: formValues.visibilitySocialLinks,
|
||||
draftSocialLinksByPlatform,
|
||||
|
||||
// Other data we need
|
||||
saveState,
|
||||
savePhotoState,
|
||||
isCurrentUserProfile,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,38 +1,121 @@
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
import apiClient from '../config/apiClient';
|
||||
import CERTIFICATE_TYPES from '../constants/certificates';
|
||||
import { configuration } from '../config/environment';
|
||||
import { unflattenAndTransformKeys, flattenAndTransformKeys } from './utils';
|
||||
import {
|
||||
camelCaseObject,
|
||||
convertKeyNames,
|
||||
snakeCaseObject,
|
||||
} from './utils';
|
||||
|
||||
const clientToServerKeyMap = {
|
||||
socialLinks: 'social_links',
|
||||
education: 'level_of_education',
|
||||
profileImage: 'profile_image',
|
||||
dateJoined: 'date_joined',
|
||||
languageProficiencies: 'language_proficiencies',
|
||||
accountPrivacy: 'account_privacy',
|
||||
yearOfBirth: 'year_of_birth',
|
||||
requiresParentalConsent: 'requires_parental_consent',
|
||||
};
|
||||
const serverToClientKeyMap = Object.entries(clientToServerKeyMap).reduce((acc, [key, value]) => {
|
||||
acc[value] = key;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export function mapServerKey(key) {
|
||||
return serverToClientKeyMap[key] || key;
|
||||
function processAccountData(data) {
|
||||
const result = camelCaseObject(data);
|
||||
return convertKeyNames(result, {
|
||||
levelOfEducation: 'education',
|
||||
});
|
||||
}
|
||||
|
||||
export function mapClientKey(key) {
|
||||
return clientToServerKeyMap[key] || key;
|
||||
// GET ACCOUNT
|
||||
export async function getAccount(username) {
|
||||
const { data } = await apiClient.get(`${configuration.ACCOUNTS_API_BASE_URL}/${username}`);
|
||||
|
||||
// Process response data
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
// PATCH PROFILE
|
||||
export async function patchProfile(username, params) {
|
||||
let processedParams = snakeCaseObject(params);
|
||||
processedParams = convertKeyNames(processedParams, {
|
||||
education: 'level_of_education',
|
||||
});
|
||||
|
||||
const { data } = await apiClient.patch(
|
||||
`${configuration.ACCOUNTS_API_BASE_URL}/${username}`,
|
||||
processedParams,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/merge-patch+json',
|
||||
},
|
||||
},
|
||||
).catch((error) => {
|
||||
const processedError = Object.create(error);
|
||||
const fieldErrors = Object.entries(processAccountData(error.response.data.field_errors))
|
||||
.reduce((acc, [fieldKey, messages]) => {
|
||||
acc[fieldKey] = messages.userMessage;
|
||||
return acc;
|
||||
}, {});
|
||||
processedError.fieldErrors = fieldErrors;
|
||||
throw processedError;
|
||||
});
|
||||
|
||||
// Process response data
|
||||
return processAccountData(data);
|
||||
}
|
||||
|
||||
// POST PROFILE PHOTO
|
||||
|
||||
export async function postProfilePhoto(username, formData) {
|
||||
const { data } = await apiClient.post(
|
||||
`${configuration.ACCOUNTS_API_BASE_URL}/${username}/image`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
// DELETE PROFILE PHOTO
|
||||
|
||||
export async function deleteProfilePhoto(username) {
|
||||
const { data } = await apiClient.delete(`${configuration.ACCOUNTS_API_BASE_URL}/${username}/image`);
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
// GET PREFERENCES
|
||||
export async function getPreferences(username) {
|
||||
const { data } = await apiClient.get(`${configuration.PREFERENCES_API_BASE_URL}/${username}`);
|
||||
|
||||
const result = camelCaseObject(data);
|
||||
return convertKeyNames(result, {
|
||||
visibilityLevelOfEducation: 'visibilityEducation',
|
||||
});
|
||||
}
|
||||
|
||||
// PATCH PREFERENCES
|
||||
export async function patchPreferences(username, params) {
|
||||
let processedParams = snakeCaseObject(params);
|
||||
processedParams = convertKeyNames(processedParams, {
|
||||
visibility_bio: 'visibility.bio',
|
||||
visibility_course_certificates: 'visibility.course_certificates',
|
||||
visibility_country: 'visibility.country',
|
||||
visibility_date_joined: 'visibility.date_joined',
|
||||
visibility_education: 'visibility.level_of_education',
|
||||
visibility_language_proficiencies: 'visibility.language_proficiencies',
|
||||
visibility_name: 'visibility.name',
|
||||
visibility_social_links: 'visibility.social_links',
|
||||
visibility_time_zone: 'visibility.time_zone',
|
||||
});
|
||||
|
||||
await apiClient.patch(
|
||||
`${configuration.PREFERENCES_API_BASE_URL}/${username}`,
|
||||
processedParams,
|
||||
{ headers: { 'Content-Type': 'application/merge-patch+json' } },
|
||||
);
|
||||
|
||||
return params; // TODO: Once the server returns the updated preferences object, return that.
|
||||
}
|
||||
|
||||
// GET COURSE CERTIFICATES
|
||||
|
||||
function transformCertificateData(data) {
|
||||
const transformedData = [];
|
||||
data.forEach((cert) => {
|
||||
transformedData.push({
|
||||
...camelcaseKeys(cert),
|
||||
...camelCaseObject(cert),
|
||||
certificateType: CERTIFICATE_TYPES[cert.certificate_type],
|
||||
downloadUrl: `${configuration.LMS_BASE_URL}${cert.download_url}`,
|
||||
});
|
||||
@@ -40,130 +123,10 @@ function transformCertificateData(data) {
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
export function getAccount(username) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient
|
||||
.get(`${configuration.ACCOUNTS_API_BASE_URL}/${username}`)
|
||||
.then((response) => {
|
||||
resolve(unflattenAndTransformKeys(response.data, key => mapServerKey(key)));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const mapSaveProfileRequestData = (props) => {
|
||||
const PROFILE_REQUEST_DATA_MAP = {
|
||||
education: 'levelOfEducation',
|
||||
socialLinks: socialLinks =>
|
||||
Object.entries(socialLinks)
|
||||
.filter(([platform, value]) => value !== null) // eslint-disable-line no-unused-vars
|
||||
.reduce((acc, [platform, value]) => {
|
||||
acc.push({ socialLink: value, platform });
|
||||
return acc;
|
||||
}, []),
|
||||
};
|
||||
const state = {};
|
||||
|
||||
Object.keys(props).forEach((prop) => {
|
||||
const propModifier = PROFILE_REQUEST_DATA_MAP[prop] || prop;
|
||||
if (typeof propModifier === 'function') {
|
||||
state[prop] = propModifier(props[prop]);
|
||||
} else {
|
||||
state[propModifier] = props[prop];
|
||||
}
|
||||
});
|
||||
return state;
|
||||
};
|
||||
|
||||
export function patchProfile(username, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient
|
||||
.patch(
|
||||
`${configuration.ACCOUNTS_API_BASE_URL}/${username}`,
|
||||
flattenAndTransformKeys(data, key => mapClientKey(key)),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/merge-patch+json',
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((response) => {
|
||||
resolve(unflattenAndTransformKeys(response.data, key => mapServerKey(key)));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function postProfilePhoto(username, formData) {
|
||||
return apiClient.post(`${configuration.ACCOUNTS_API_BASE_URL}/${username}/image`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteProfilePhoto(username) {
|
||||
return apiClient.delete(`${configuration.ACCOUNTS_API_BASE_URL}/${username}/image`);
|
||||
}
|
||||
|
||||
export function getPreferences(username) {
|
||||
const url = `${configuration.PREFERENCES_API_BASE_URL}/${username}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient
|
||||
.get(url)
|
||||
.then(({ data }) => {
|
||||
// Unflatten server response
|
||||
// visibility.social_links: 'value' becomes { visibility: { socialLinks: 'value' }}
|
||||
const preferences = unflattenAndTransformKeys(data, key => mapServerKey(key));
|
||||
if (preferences.visibility === undefined) {
|
||||
preferences.visibility = {};
|
||||
}
|
||||
resolve(preferences);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function patchPreferences(username, preferences) {
|
||||
const url = `${configuration.PREFERENCES_API_BASE_URL}/${username}`;
|
||||
|
||||
// Flatten object for server
|
||||
// { visibility: { socialLinks: 'value' }} becomes visibility.social_links: 'value'
|
||||
const data = flattenAndTransformKeys(preferences, key => mapClientKey(key));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient
|
||||
.patch(url, data, { headers: { 'Content-Type': 'application/merge-patch+json' } })
|
||||
.then(() => {
|
||||
// eslint-disable-line no-unused-vars
|
||||
// Server response is blank on success
|
||||
// resolve(response.data);
|
||||
resolve(preferences);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getCourseCertificates(username) {
|
||||
export async function getCourseCertificates(username) {
|
||||
const url = `${configuration.CERTIFICATES_API_BASE_URL}/${username}/`;
|
||||
const { data } = await apiClient.get(url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
apiClient
|
||||
.get(url)
|
||||
.then(({ data }) => {
|
||||
resolve(transformCertificateData(data));
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
return transformCertificateData(data);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { mapSaveProfileRequestData } from './ProfileApiService';
|
||||
|
||||
describe('mapSaveProfileRequestData', () => {
|
||||
it('should modify props according to prop modifier strings and functions', () => {
|
||||
const props = {
|
||||
favoriteColor: 'red',
|
||||
age: 30,
|
||||
petName: 'Donkey',
|
||||
name: 'Donkey McWafflebatter',
|
||||
country: 'US',
|
||||
education: 'BS',
|
||||
socialLinks: {
|
||||
twitter: null,
|
||||
facebook: 'https://www.facebook.com',
|
||||
},
|
||||
};
|
||||
const result = mapSaveProfileRequestData(props);
|
||||
expect(result).toEqual({
|
||||
favoriteColor: 'red',
|
||||
age: 30,
|
||||
petName: 'Donkey',
|
||||
name: 'Donkey McWafflebatter',
|
||||
country: 'US',
|
||||
levelOfEducation: 'BS',
|
||||
socialLinks: [
|
||||
{
|
||||
platform: 'facebook',
|
||||
socialLink: 'https://www.facebook.com',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,30 +1,48 @@
|
||||
import set from 'lodash.set';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export function unflattenAndTransformKeys(obj, transformer) {
|
||||
const newObj = {};
|
||||
export function modifyObjectKeys(object, modify) {
|
||||
// If the passed in object is not an object, return it.
|
||||
if (
|
||||
object === undefined ||
|
||||
object === null ||
|
||||
(typeof object !== 'object' && !Array.isArray(object))
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
set(newObj, key.split('.').map(transformer), value);
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(value => modifyObjectKeys(value, modify));
|
||||
}
|
||||
|
||||
// Otherwise, process all its keys.
|
||||
const result = {};
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
result[modify(key)] = modifyObjectKeys(value, modify);
|
||||
});
|
||||
|
||||
return newObj;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function flattenAndTransformKeys(srcObj, transformer = key => key) {
|
||||
const flatten = (obj, prevKeys = []) => (Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
const tKey = transformer(key);
|
||||
const keys = prevKeys.concat(tKey);
|
||||
export function camelCaseObject(object) {
|
||||
return modifyObjectKeys(object, camelCase);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
acc[keys.join('.')] = value;
|
||||
} else if (value && typeof value === 'object') {
|
||||
Object.assign(acc, flatten(value, keys));
|
||||
} else {
|
||||
acc[keys.join('.')] = value;
|
||||
export function snakeCaseObject(object) {
|
||||
return modifyObjectKeys(object, snakeCase);
|
||||
}
|
||||
|
||||
export function convertKeyNames(object, nameMap) {
|
||||
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
|
||||
|
||||
return modifyObjectKeys(object, transformer);
|
||||
}
|
||||
|
||||
export function keepKeys(data, whitelist) {
|
||||
const result = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (whitelist.indexOf(key) > -1) {
|
||||
result[key] = data[key];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}));
|
||||
|
||||
return flatten(srcObj);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,47 +1,105 @@
|
||||
import { flattenAndTransformKeys, unflattenAndTransformKeys } from './utils';
|
||||
import { modifyObjectKeys, camelCaseObject, snakeCaseObject, convertKeyNames, keepKeys } from './utils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
it('should use the provided modify function to change all keys in and object and its children', () => {
|
||||
function meowKeys(key) {
|
||||
return `${key}Meow`;
|
||||
}
|
||||
|
||||
describe('unflattenAndTransformKeys', () => {
|
||||
it('should unflatten objects and transform keys', () => {
|
||||
const sourceObject = {
|
||||
country: 'US',
|
||||
'visibility.sociallinks': 'private',
|
||||
'visibility.education': 'private',
|
||||
'visibility.bio': 'private',
|
||||
};
|
||||
|
||||
const result = unflattenAndTransformKeys(sourceObject, key => key.toUpperCase());
|
||||
const result = modifyObjectKeys(
|
||||
{
|
||||
one: undefined,
|
||||
two: null,
|
||||
three: '',
|
||||
four: 0,
|
||||
five: NaN,
|
||||
six: [1, 2, { seven: 'woof' }],
|
||||
eight: { nine: { ten: 'bark' }, eleven: true },
|
||||
},
|
||||
meowKeys,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
COUNTRY: 'US',
|
||||
VISIBILITY: {
|
||||
SOCIALLINKS: 'private',
|
||||
EDUCATION: 'private',
|
||||
BIO: 'private',
|
||||
},
|
||||
oneMeow: undefined,
|
||||
twoMeow: null,
|
||||
threeMeow: '',
|
||||
fourMeow: 0,
|
||||
fiveMeow: NaN,
|
||||
sixMeow: [1, 2, { sevenMeow: 'woof' }],
|
||||
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('flattenAndTransformKeys', () => {
|
||||
it('should flatten objects and transform keys', () => {
|
||||
const sourceObject = {
|
||||
COUNTRY: 'US',
|
||||
VISIBILITY: {
|
||||
SOCIALLINKS: 'private',
|
||||
EDUCATION: 'private',
|
||||
BIO: 'private',
|
||||
},
|
||||
};
|
||||
|
||||
const result = flattenAndTransformKeys(sourceObject, key => key.toLowerCase());
|
||||
describe('camelCaseObject', () => {
|
||||
it('should make everything camelCase', () => {
|
||||
const result = camelCaseObject({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
country: 'US',
|
||||
'visibility.sociallinks': 'private',
|
||||
'visibility.education': 'private',
|
||||
'visibility.bio': 'private',
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
dotDotDot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snakeCaseObject', () => {
|
||||
it('should make everything snake_case', () => {
|
||||
const result = snakeCaseObject({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
dot_dot_dot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertKeyNames', () => {
|
||||
it('should replace the specified keynames', () => {
|
||||
const result = convertKeyNames(
|
||||
{
|
||||
one: { two: { three: 'four' } },
|
||||
five: 'six',
|
||||
},
|
||||
{
|
||||
two: 'blue',
|
||||
five: 'alive',
|
||||
seven: 'heaven',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: { blue: { three: 'four' } },
|
||||
alive: 'six',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('keepKeys', () => {
|
||||
it('should keep the specified keys only', () => {
|
||||
const result = keepKeys({
|
||||
one: 123,
|
||||
two: { three: 'skip me' },
|
||||
four: 'five',
|
||||
six: null,
|
||||
8: 'sneaky',
|
||||
}, [
|
||||
'one', 'three', 'six', 'seven', '8', // yup, the 8 integer will be converted to a string.
|
||||
]);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: 123,
|
||||
six: null,
|
||||
8: 'sneaky',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user