diff --git a/config/webpack.dev.config.js b/config/webpack.dev.config.js index 80a08ce..9bd0370 100755 --- a/config/webpack.dev.config.js +++ b/config/webpack.dev.config.js @@ -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: { diff --git a/config/webpack.prod.config.js b/config/webpack.prod.config.js index 036551a..08d02c5 100755 --- a/config/webpack.prod.config.js +++ b/config/webpack.prod.config.js @@ -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', }, diff --git a/package-lock.json b/package-lock.json index 53ce2aa..2f882ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4008436..c6cb1c8 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/actions/ProfileActions.js b/src/actions/ProfileActions.js index afa4b23..b908dc0 100644 --- a/src/actions/ProfileActions.js +++ b/src/actions/ProfileActions.js @@ -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, diff --git a/src/actions/ProfileActions.test.js b/src/actions/ProfileActions.test.js index d373ff2..d77c530 100644 --- a/src/actions/ProfileActions.test.js +++ b/src/actions/ProfileActions.test.js @@ -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); }); }); diff --git a/src/analytics/analytics.js b/src/analytics/analytics.js index 43cde4f..6128db1 100755 --- a/src/analytics/analytics.js +++ b/src/analytics/analytics.js @@ -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, diff --git a/src/components/ProfilePage.jsx b/src/components/ProfilePage.jsx index 3a9dc43..96262af 100644 --- a/src/components/ProfilePage.jsx +++ b/src/components/ProfilePage.jsx @@ -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 { - - - - - + + + + + {this.props.requiresParentalConsent ? : null} - - + + @@ -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); diff --git a/src/components/ProfilePage/Bio.jsx b/src/components/ProfilePage/Bio.jsx index 8e5e347..4dcd38b 100644 --- a/src/components/ProfilePage/Bio.jsx +++ b/src/components/ProfilePage/Bio.jsx @@ -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} /> {error} @@ -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} /> -

{value}

+

{bio}

), empty: ( @@ -100,7 +100,7 @@ class Bio extends React.Component { static: ( -

{value}

+

{bio}

), }} @@ -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, }; diff --git a/src/components/ProfilePage/Certificates.jsx b/src/components/ProfilePage/Certificates.jsx index ee49e94..76f349d 100644 --- a/src/components/ProfilePage/Certificates.jsx +++ b/src/components/ProfilePage/Certificates.jsx @@ -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 { {this.renderCertificates()} @@ -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()} @@ -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, }; diff --git a/src/components/ProfilePage/Country.jsx b/src/components/ProfilePage/Country.jsx index 9c49f59..5b27f04 100644 --- a/src/components/ProfilePage/Country.jsx +++ b/src/components/ProfilePage/Country.jsx @@ -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 { {error} @@ -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} /> -
{ALL_COUNTRIES[value]}
+
{ALL_COUNTRIES[country]}
), empty: ( @@ -108,7 +108,7 @@ class Country extends React.Component { static: ( -
{ALL_COUNTRIES[value]}
+
{ALL_COUNTRIES[country]}
), }} @@ -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, }; diff --git a/src/components/ProfilePage/Education.jsx b/src/components/ProfilePage/Education.jsx index 148066c..d6f6054 100644 --- a/src/components/ProfilePage/Education.jsx +++ b/src/components/ProfilePage/Education.jsx @@ -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 { {error} @@ -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} /> -
{EDUCATION[value]}
+
{EDUCATION[education]}
), empty: ( @@ -112,7 +112,7 @@ class Education extends React.Component { static: ( -
{EDUCATION[value]}
+
{EDUCATION[education]}
), }} @@ -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, }; diff --git a/src/components/ProfilePage/Name.jsx b/src/components/ProfilePage/Name.jsx index 8e335dc..77b672b 100644 --- a/src/components/ProfilePage/Name.jsx +++ b/src/components/ProfilePage/Name.jsx @@ -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 {
- + {error} @@ -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} /> -
{value}
+
{name}
), empty: ( @@ -103,7 +103,7 @@ class Name extends React.Component { static: ( -
{value}
+
{name}
), }} @@ -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, }; diff --git a/src/components/ProfilePage/PreferredLanguage.jsx b/src/components/ProfilePage/PreferredLanguage.jsx index acc4575..4acb8c2 100644 --- a/src/components/ProfilePage/PreferredLanguage.jsx +++ b/src/components/ProfilePage/PreferredLanguage.jsx @@ -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 ( {error} @@ -112,8 +112,8 @@ class PreferredLanguage extends React.Component { )} showEditButton onClickEdit={this.handleOpen} - showVisibility={visibility !== null} - visibility={visibility} + showVisibility={visibilityLanguageProficiencies !== null} + visibility={visibilityLanguageProficiencies} />
{ALL_LANGUAGES[value]}
@@ -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, }; diff --git a/src/components/ProfilePage/SocialLinks.jsx b/src/components/ProfilePage/SocialLinks.jsx index a4a6294..6f18959 100644 --- a/src/components/ProfilePage/SocialLinks.jsx +++ b/src/components/ProfilePage/SocialLinks.jsx @@ -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: (
    - {values.map(({ platform }) => ( + {socialLinks.map(({ platform }) => ( - +
      - {values.map(({ platform, social_link: socialLink }) => ( + {socialLinks.map(({ platform, socialLink }) => (
        - {values.map(({ platform, social_link: socialLink }) => ( + {socialLinks.map(({ platform, socialLink }) => ( - + + {/* TODO: Replace this alert with per-field errors. Needs API update. */} + {error !== null ? {error} : null}
          - {values.map(({ platform, social_link: socialLink }) => ( + {socialLinks.map(({ platform, socialLink }) => ( ))}
        @@ -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 ? - : - Add {name}; + const linkDisplay = url ? ( + + ) : ( + Add {name} + ); return
      • {linkDisplay}
      • ; } @@ -247,13 +266,8 @@ EditableListItem.defaultProps = { onClickEmptyContent: null, }; - function EditingListItem({ - platform, - name, - value, - onChange, - error, + platform, name, value, onChange, error, }) { return (
      • @@ -261,7 +275,7 @@ function EditingListItem({ @@ -283,7 +297,6 @@ EditingListItem.defaultProps = { error: null, }; - function EmptyListItem({ onClick, name }) { return (
      • @@ -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 (
      • @@ -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, +}; diff --git a/src/components/ProfilePage/elements/FormControls.jsx b/src/components/ProfilePage/elements/FormControls.jsx index 4007496..a333f40 100644 --- a/src/components/ProfilePage/elements/FormControls.jsx +++ b/src/components/ProfilePage/elements/FormControls.jsx @@ -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 ( @@ -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({