From 7c58442aaeac2b35f85068c42e660571d53ca844 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 19 Apr 2018 16:34:31 +0300 Subject: [PATCH] Form is responsible to change customErrorText FieldTextInput: touch flag couldn't be tracked reliably in responses with errors --- .../FieldTextInput/FieldTextInput.js | 6 +- .../ContactDetailsForm/ContactDetailsForm.js | 24 ++- .../PasswordChangeForm/PasswordChangeForm.js | 20 +- .../PasswordRecoveryForm.js | 181 ++++++++++-------- yarn.lock | 8 +- 5 files changed, 143 insertions(+), 96 deletions(-) diff --git a/src/components/FieldTextInput/FieldTextInput.js b/src/components/FieldTextInput/FieldTextInput.js index f0b4f6f6..9af3262c 100644 --- a/src/components/FieldTextInput/FieldTextInput.js +++ b/src/components/FieldTextInput/FieldTextInput.js @@ -41,9 +41,9 @@ class FieldTextInputComponent extends Component { // Error message and input error styles are only shown if the // field has been touched and the validation has failed. - const hasError = touched && invalid && errorText; + const hasError = !!customErrorText || !!(touched && invalid && error); - const fieldMeta = { touched, error: errorText }; + const fieldMeta = { touched: hasError, error: errorText }; const inputClasses = inputRootClass || @@ -116,6 +116,6 @@ class FieldTextInput extends Component { render() { return ; } -}; +} export default FieldTextInput; diff --git a/src/containers/ContactDetailsForm/ContactDetailsForm.js b/src/containers/ContactDetailsForm/ContactDetailsForm.js index b60b1dab..bdd84c8f 100644 --- a/src/containers/ContactDetailsForm/ContactDetailsForm.js +++ b/src/containers/ContactDetailsForm/ContactDetailsForm.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { compose } from 'redux'; import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import { Form as FinalForm } from 'react-final-form'; +import { isEqual } from 'lodash'; import classNames from 'classnames'; import { propTypes } from '../../util/types'; import * as validators from '../../util/validators'; @@ -24,6 +25,7 @@ class ContactDetailsFormComponent extends Component { this.state = { showVerificationEmailSentMessage: false }; this.emailSentTimeoutId = null; this.handleResendVerificationEmail = this.handleResendVerificationEmail.bind(this); + this.submittedValues = {}; } componentWillUnmount() { @@ -54,7 +56,6 @@ class ContactDetailsFormComponent extends Component { currentUser, formId, handleSubmit, - submitting, inProgress, intl, invalid, @@ -97,6 +98,7 @@ class ContactDetailsFormComponent extends Component { sendVerificationEmailError ); + const emailTouched = this.submittedValues.email !== values.email; const emailTakenErrorText = isChangeEmailTakenError(saveEmailError) ? intl.formatMessage({ id: 'ContactDetailsForm.emailTakenError' }) : null; @@ -221,6 +223,7 @@ class ContactDetailsFormComponent extends Component { const passwordFailedMessage = intl.formatMessage({ id: 'ContactDetailsForm.passwordFailed', }); + const passwordTouched = this.submittedValues.currentPassword !== values.currentPassword; const passwordErrorText = isChangeEmailWrongPassword(saveEmailError) ? passwordFailedMessage : null; @@ -255,11 +258,22 @@ class ContactDetailsFormComponent extends Component { } const classes = classNames(rootClassName || css.root, className); + const submittedOnce = Object.keys(this.submittedValues).length > 0; + const pristineSinceLastSubmit = submittedOnce && isEqual(values, this.submittedValues); const submitDisabled = - invalid || submitting || inProgress || !(emailChanged || phoneNumberChanged); + invalid || + pristineSinceLastSubmit || + inProgress || + !(emailChanged || phoneNumberChanged); return ( -
+ { + this.submittedValues = values; + handleSubmit(e); + }} + >
{emailVerifiedInfo}
diff --git a/src/containers/PasswordChangeForm/PasswordChangeForm.js b/src/containers/PasswordChangeForm/PasswordChangeForm.js index 55711e92..e9f1806d 100644 --- a/src/containers/PasswordChangeForm/PasswordChangeForm.js +++ b/src/containers/PasswordChangeForm/PasswordChangeForm.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { compose } from 'redux'; import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import { Form as FinalForm } from 'react-final-form'; +import { isEqual } from 'lodash'; import classNames from 'classnames'; import { propTypes } from '../../util/types'; import * as validators from '../../util/validators'; @@ -18,6 +19,7 @@ class PasswordChangeFormComponent extends Component { constructor(props) { super(props); this.resetTimeoutId = null; + this.submittedValues = {}; } componentWillUnmount() { window.clearTimeout(this.resetTimeoutId); @@ -34,13 +36,13 @@ class PasswordChangeFormComponent extends Component { changePasswordError, currentUser, handleSubmit, - submitting, inProgress, intl, invalid, pristine, ready, - reset, + form, + values, } = fieldRenderProps; const user = ensureCurrentUser(currentUser); @@ -103,6 +105,7 @@ class PasswordChangeFormComponent extends Component { const passwordFailedMessage = intl.formatMessage({ id: 'PasswordChangeForm.passwordFailed', }); + const passwordTouched = this.submittedValues.currentPassword !== values.currentPassword; const passwordErrorText = isChangePasswordWrongPassword(changePasswordError) ? passwordFailedMessage : null; @@ -118,16 +121,19 @@ class PasswordChangeFormComponent extends Component { ) : null; + const submittedOnce = Object.keys(this.submittedValues).length > 0; + const pristineSinceLastSubmit = submittedOnce && isEqual(values, this.submittedValues); const classes = classNames(rootClassName || css.root, className); - const submitDisabled = invalid || submitting || inProgress; + const submitDisabled = invalid || pristineSinceLastSubmit || inProgress; return ( { - handleSubmit(values) + onSubmit={e => { + this.submittedValues = values; + handleSubmit(e) .then(() => { - this.resetTimeoutId = window.setTimeout(reset, RESET_TIMEOUT); + this.resetTimeoutId = window.setTimeout(form.reset, RESET_TIMEOUT); }) .catch(() => { // Error is handled in duck file already. @@ -171,7 +177,7 @@ class PasswordChangeFormComponent extends Component { passwordMinLength, passwordMaxLength )} - customErrorText={passwordErrorText} + customErrorText={passwordTouched ? null : passwordErrorText} />
diff --git a/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.js b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.js index 0c31bb0c..5ecc62d6 100644 --- a/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.js +++ b/src/containers/PasswordRecoveryForm/PasswordRecoveryForm.js @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { compose } from 'redux'; import { FormattedMessage, injectIntl, intlShape } from 'react-intl'; import { Form as FinalForm } from 'react-final-form'; +import { isEqual } from 'lodash'; import classNames from 'classnames'; import { propTypes } from '../../util/types'; import * as validators from '../../util/validators'; @@ -11,90 +12,116 @@ import { Form, PrimaryButton, FieldTextInput, NamedLink } from '../../components import css from './PasswordRecoveryForm.css'; -const PasswordRecoveryFormComponent = props => ( - { - const { - rootClassName, - className, - formId, - handleSubmit, - pristine, - submitting, - initialValues, - intl, - inProgress, - recoveryError, - } = fieldRenderProps; +class PasswordRecoveryFormComponent extends Component { + constructor(props) { + super(props); + this.submittedValues = {}; + } - // email - const emailLabel = intl.formatMessage({ - id: 'PasswordRecoveryForm.emailLabel', - }); - const emailPlaceholder = intl.formatMessage({ - id: 'PasswordRecoveryForm.emailPlaceholder', - }); - const emailRequiredMessage = intl.formatMessage({ - id: 'PasswordRecoveryForm.emailRequired', - }); - const emailNotFoundMessage = intl.formatMessage({ - id: 'PasswordRecoveryForm.emailNotFound', - }); - const emailInvalidMessage = intl.formatMessage({ - id: 'PasswordRecoveryForm.emailInvalid', - }); + render() { + return ( + { + const { + rootClassName, + className, + formId, + handleSubmit, + pristine, + initialValues, + intl, + inProgress, + recoveryError, + values, + } = fieldRenderProps; - const emailRequired = validators.required(emailRequiredMessage); - const emailValid = validators.emailFormatValid(emailInvalidMessage); + // email + const emailLabel = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailLabel', + }); + const emailPlaceholder = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailPlaceholder', + }); + const emailRequiredMessage = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailRequired', + }); + const emailNotFoundMessage = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailNotFound', + }); + const emailInvalidMessage = intl.formatMessage({ + id: 'PasswordRecoveryForm.emailInvalid', + }); - // In case a given email is not found, pass a custom error message - // to be rendered with the input component - const customErrorText = isPasswordRecoveryEmailNotFoundError(recoveryError) - ? emailNotFoundMessage - : null; - const initialEmail = initialValues ? initialValues.email : null; + const emailRequired = validators.required(emailRequiredMessage); + const emailValid = validators.emailFormatValid(emailInvalidMessage); - const classes = classNames(rootClassName || css.root, className); - const submitInProgress = submitting || inProgress; - const submitDisabled = (pristine && !initialEmail) || submitInProgress; + // In case a given email is not found, pass a custom error message + // to be rendered with the input component + const customErrorText = isPasswordRecoveryEmailNotFoundError(recoveryError) + ? emailNotFoundMessage + : null; + const initialEmail = initialValues ? initialValues.email : null; + const emailTouched = values.email !== this.submittedValues.email; - const loginLink = ( - - - - ); + const classes = classNames(rootClassName || css.root, className); + const submitInProgress = inProgress; + const submittedOnce = Object.keys(this.submittedValues).length > 0; + const pristineSinceLastSubmit = submittedOnce && isEqual(values, this.submittedValues); + const submitDisabled = + (pristine && !initialEmail) || submitInProgress || pristineSinceLastSubmit; - return ( - - + const loginLink = ( + + + + ); -
-

- - - -

+ return ( + { + this.submittedValues = values; + handleSubmit(e); + }} + > + - - - -
- - ); - }} - /> -); +
+

+ + + +

+ + + + +
+ + ); + }} + /> + ); + } +} PasswordRecoveryFormComponent.defaultProps = { rootClassName: null, diff --git a/yarn.lock b/yarn.lock index c30b2d7d..483c43fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3193,8 +3193,8 @@ final-form-arrays@^1.0.4: resolved "https://registry.yarnpkg.com/final-form-arrays/-/final-form-arrays-1.0.4.tgz#74330cb4d64ce07bb1c4a9084d0b65afdac6bf72" final-form@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.4.0.tgz#3eaa1305d7b03d1e5698ace5ff361d4d20d44ac0" + version "4.5.2" + resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.5.2.tgz#f8a9518784bdc2f72e71548a42b04c640963ca49" finalhandler@1.1.1: version "1.1.1" @@ -6889,8 +6889,8 @@ react-final-form-arrays@^1.0.4: resolved "https://registry.yarnpkg.com/react-final-form-arrays/-/react-final-form-arrays-1.0.4.tgz#888b9b81a806844cf66d73f4e8b60e5f18fa2a11" react-final-form@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-3.1.5.tgz#6fa0e4c61e49b691b679ef4c8a5ad3fe92df0956" + version "3.3.1" + resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-3.3.1.tgz#dbe40eec853a9c451e5a8deb3b02d948a2fb7e3c" react-google-maps@^9.4.5: version "9.4.5"