diff --git a/src/CONST.js b/src/CONST.js index 0c6a817125a4..0bf8e55d57c7 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -758,6 +758,7 @@ const CONST = { // There's a limit of 60k characters in Auth - https://github.com/Expensify/Auth/blob/198d59547f71fdee8121325e8bc9241fc9c3236a/auth/lib/Request.h#L28 MAX_COMMENT_LENGTH: 60000, + FORM_CHARACTER_LIMIT: 50, AVATAR_CROP_MODAL: { // The next two constants control what is min and max value of the image crop scale. // Values define in how many times the image can be bigger than its container. @@ -791,6 +792,7 @@ const CONST = { INVITE: 'invite', LEAVE_ROOM: 'leaveRoom', }, + PROFILE_SETTINGS_FORM: 'profileSettingsForm', // These split the maximum decimal value of a signed 64-bit number (9,223,372,036,854,775,807) into parts where none of them are too big to fit into a 32-bit number, so that we can // generate them each with a random number generator with only 32-bits of precision. diff --git a/src/libs/ValidationUtils.js b/src/libs/ValidationUtils.js index a562df509c82..02b64dda758d 100644 --- a/src/libs/ValidationUtils.js +++ b/src/libs/ValidationUtils.js @@ -321,7 +321,19 @@ function isValidRoutingNumber(number) { * @returns {Boolean[]} */ function doesFailCharacterLimit(maxLength, valuesToBeValidated) { - return _.map(valuesToBeValidated, value => value.length > maxLength); + return _.map(valuesToBeValidated, value => value && value.length > maxLength); +} + +/** + * Checks if each string in array is of valid length and then returns true + * for each string which exceeds the limit. The function trims the passed values. + * + * @param {Number} maxLength + * @param {String[]} valuesToBeValidated + * @returns {Boolean[]} + */ +function doesFailCharacterLimitAfterTrim(maxLength, valuesToBeValidated) { + return _.map(valuesToBeValidated, value => value && value.trim().length > maxLength); } /** @@ -384,6 +396,7 @@ export { isValidSSNLastFour, isValidSSNFullNine, doesFailCharacterLimit, + doesFailCharacterLimitAfterTrim, isReservedRoomName, isExistingRoomName, isValidTaxID, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 55bc10f84cd6..2653398b046e 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -72,7 +72,7 @@ function getDisplayName(login, personalDetail) { * @returns {String} */ function getMaxCharacterError(isError) { - return isError ? Localize.translateLocal('personalDetails.error.characterLimit', {limit: 50}) : ''; + return isError ? Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}) : ''; } /** @@ -263,7 +263,6 @@ function setPersonalDetails(details, shouldGrowl) { } function updateProfile(firstName, lastName, pronouns, timezone) { - const myPersonalDetails = personalDetails[currentUserEmail]; API.write('UpdateProfile', { firstName, lastName, @@ -286,19 +285,6 @@ function updateProfile(firstName, lastName, pronouns, timezone) { }, }, }], - failureData: [{ - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS, - value: { - [currentUserEmail]: { - firstName: myPersonalDetails.firstName, - lastName: myPersonalDetails.lastName, - pronouns: myPersonalDetails.pronouns, - timezone: myPersonalDetails.timeZone, - displayName: myPersonalDetails.displayName, - }, - }, - }], }); } diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 458a17d1713a..e652cda0c86d 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -2,7 +2,7 @@ import lodashGet from 'lodash/get'; import React, {Component} from 'react'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; -import {View, ScrollView} from 'react-native'; +import {View} from 'react-native'; import Str from 'expensify-common/lib/str'; import moment from 'moment-timezone'; import _ from 'underscore'; @@ -17,16 +17,16 @@ import styles from '../../../styles/styles'; import Text from '../../../components/Text'; import LoginField from './LoginField'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; +import * as Localize from '../../../libs/Localize'; import compose from '../../../libs/compose'; -import Button from '../../../components/Button'; -import FixedFooter from '../../../components/FixedFooter'; import TextInput from '../../../components/TextInput'; import Picker from '../../../components/Picker'; -import FullNameInputRow from '../../../components/FullNameInputRow'; import CheckboxWithLabel from '../../../components/CheckboxWithLabel'; import AvatarWithImagePicker from '../../../components/AvatarWithImagePicker'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import * as ValidationUtils from '../../../libs/ValidationUtils'; +import * as ReportUtils from '../../../libs/ReportUtils'; +import Form from '../../../components/Form'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; const propTypes = { @@ -65,24 +65,19 @@ class ProfilePage extends Component { constructor(props) { super(props); - const currentUserDetails = this.props.currentUserPersonalDetails || {}; + this.defaultAvatar = ReportUtils.getDefaultAvatar(this.props.currentUserPersonalDetails.login); + this.avatar = {uri: lodashGet(this.props.currentUserPersonalDetails, 'avatar') || this.defaultAvatar}; + this.pronouns = props.currentUserPersonalDetails.pronouns; this.state = { - firstName: currentUserDetails.firstName || '', - hasFirstNameError: false, - lastName: currentUserDetails.lastName || '', - hasLastNameError: false, - pronouns: currentUserDetails.pronouns || '', - hasPronounError: false, - hasSelfSelectedPronouns: !_.isEmpty(currentUserDetails.pronouns) && !currentUserDetails.pronouns.startsWith(CONST.PRONOUNS.PREFIX), - selectedTimezone: lodashGet(currentUserDetails, 'timezone.selected', CONST.DEFAULT_TIME_ZONE.selected), - isAutomaticTimezone: lodashGet(currentUserDetails, 'timezone.automatic', CONST.DEFAULT_TIME_ZONE.automatic), logins: this.getLogins(props.loginList), + selectedTimezone: lodashGet(props.currentUserPersonalDetails.timezone, 'selected', CONST.DEFAULT_TIME_ZONE.selected), + isAutomaticTimezone: lodashGet(props.currentUserPersonalDetails.timezone, 'automatic', CONST.DEFAULT_TIME_ZONE.automatic), + hasSelfSelectedPronouns: !_.isEmpty(props.currentUserPersonalDetails.pronouns) && !props.currentUserPersonalDetails.pronouns.startsWith(CONST.PRONOUNS.PREFIX), }; this.getLogins = this.getLogins.bind(this); - this.setAutomaticTimezone = this.setAutomaticTimezone.bind(this); + this.validate = this.validate.bind(this); this.updatePersonalDetails = this.updatePersonalDetails.bind(this); - this.validateInputs = this.validateInputs.bind(this); } componentDidUpdate(prevProps) { @@ -101,18 +96,6 @@ class ProfilePage extends Component { this.setState(stateToUpdate); } - /** - * Set the form to use automatic timezone - * - * @param {Boolean} isAutomaticTimezone - */ - setAutomaticTimezone(isAutomaticTimezone) { - this.setState(({selectedTimezone}) => ({ - isAutomaticTimezone, - selectedTimezone: isAutomaticTimezone ? moment.tz.guess() : selectedTimezone, - })); - } - /** * Get the most validated login of each type * @@ -145,34 +128,65 @@ class ProfilePage extends Component { /** * Submit form to update personal details + * @param {Object} values + * @param {String} values.firstName + * @param {String} values.lastName + * @param {String} values.pronouns + * @param {Boolean} values.isAutomaticTimezone + * @param {String} values.timezone + * @param {String} values.selfSelectedPronoun */ - updatePersonalDetails() { - if (!this.validateInputs()) { - return; - } - + updatePersonalDetails(values) { PersonalDetails.updateProfile( - this.state.firstName.trim(), - this.state.lastName.trim(), - this.state.pronouns.trim(), + values.firstName.trim(), + values.lastName.trim(), + (this.state.hasSelfSelectedPronouns) ? values.selfSelectedPronoun.trim() : values.pronouns.trim(), { - automatic: this.state.isAutomaticTimezone, - selected: this.state.selectedTimezone, + automatic: values.isAutomaticTimezone, + selected: values.timezone, }, ); } - validateInputs() { - const [hasFirstNameError, hasLastNameError, hasPronounError] = ValidationUtils.doesFailCharacterLimit( - 50, - [this.state.firstName.trim(), this.state.lastName.trim(), this.state.pronouns.trim()], + /** + * @param {Object} values - An object containing the value of each inputID + * @param {String} values.firstName + * @param {String} values.lastName + * @param {String} values.pronouns + * @param {Boolean} values.isAutomaticTimezone + * @param {String} values.timezone + * @param {String} values.selfSelectedPronoun + * @returns {Object} - An object containing the errors for each inputID + */ + validate(values) { + const errors = {}; + + const [hasFirstNameError, hasLastNameError, hasPronounError] = ValidationUtils.doesFailCharacterLimitAfterTrim( + CONST.FORM_CHARACTER_LIMIT, + [values.firstName, values.lastName, values.pronouns], ); + + const hasSelfSelectedPronouns = values.pronouns === CONST.PRONOUNS.SELF_SELECT; + this.pronouns = hasSelfSelectedPronouns ? '' : values.pronouns; this.setState({ - hasFirstNameError, - hasLastNameError, - hasPronounError, + hasSelfSelectedPronouns, + isAutomaticTimezone: values.isAutomaticTimezone, + selectedTimezone: values.isAutomaticTimezone ? moment.tz.guess() : values.timezone, }); - return !hasFirstNameError && !hasLastNameError && !hasPronounError; + + if (hasFirstNameError) { + errors.firstName = Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}); + } + + if (hasLastNameError) { + errors.lastName = Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}); + } + + if (hasPronounError) { + errors.pronouns = Localize.translateLocal('personalDetails.error.characterLimit', {limit: CONST.FORM_CHARACTER_LIMIT}); + } + + return errors; } render() { @@ -180,16 +194,8 @@ class ProfilePage extends Component { label: value, value: `${CONST.PRONOUNS.PREFIX}${key}`, })); - - // Disables button if none of the form values have changed const currentUserDetails = this.props.currentUserPersonalDetails || {}; - const isButtonDisabled = (currentUserDetails.firstName === this.state.firstName.trim()) - && (currentUserDetails.lastName === this.state.lastName.trim()) - && (lodashGet(currentUserDetails, 'timezone.selected') === this.state.selectedTimezone) - && (lodashGet(currentUserDetails, 'timezone.automatic') === this.state.isAutomaticTimezone) - && (currentUserDetails.pronouns === this.state.pronouns.trim()); - - const pronounsPickerValue = this.state.hasSelfSelectedPronouns ? CONST.PRONOUNS.SELF_SELECT : this.state.pronouns; + const pronounsPickerValue = this.state.hasSelfSelectedPronouns ? CONST.PRONOUNS.SELF_SELECT : this.pronouns; return ( @@ -199,10 +205,16 @@ class ProfilePage extends Component { onBackButtonPress={() => Navigation.navigate(ROUTES.SETTINGS)} onCloseButtonPress={() => Navigation.dismissModal(true)} /> - +
@@ -213,45 +225,49 @@ class ProfilePage extends Component { onImageRemoved={PersonalDetails.deleteAvatar} anchorPosition={styles.createMenuPositionProfile} size={CONST.AVATAR_SIZE.LARGE} - /> {this.props.translate('profilePage.tellUsAboutYourself')} - this.setState({firstName})} - onChangeLastName={lastName => this.setState({lastName})} - style={[styles.mt4, styles.mb4]} - /> + + + + + + + + + { - const hasSelfSelectedPronouns = pronouns === CONST.PRONOUNS.SELF_SELECT; - this.setState({ - pronouns: hasSelfSelectedPronouns ? '' : pronouns, - hasSelfSelectedPronouns, - }); - }} items={pronounsList} placeholder={{ value: '', label: this.props.translate('profilePage.selectYourPronouns'), }} - value={pronounsPickerValue} + defaultValue={pronounsPickerValue} /> {this.state.hasSelfSelectedPronouns && ( this.setState({pronouns})} + inputID="selfSelectedPronoun" + defaultValue={this.pronouns} placeholder={this.props.translate('profilePage.selfSelectYourPronoun')} - errorText={PersonalDetails.getMaxCharacterError(this.state.hasPronounError)} /> )} @@ -260,37 +276,30 @@ class ProfilePage extends Component { label={this.props.translate('profilePage.emailAddress')} type="email" login={this.state.logins.email} + defaultValue={this.state.logins.email} /> this.setState({selectedTimezone})} items={timezones} isDisabled={this.state.isAutomaticTimezone} + defaultValue={this.state.selectedTimezone} value={this.state.selectedTimezone} /> - - -