Merge pull request #797 from sharetribe/react-update

React 16 update
This commit is contained in:
Vesa Luusua 2018-04-20 15:45:22 +03:00 committed by GitHub
commit e2fd352dcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 4018 additions and 3072 deletions

View file

@ -12,12 +12,15 @@
"classnames": "^2.2.5",
"compression": "^1.7.2",
"cookie-parser": "^1.4.3",
"core-js": "^2.5.0",
"decimal.js": "7.2.4",
"dotenv": "4.0.0",
"dotenv-expand": "4.0.1",
"express": "^4.16.3",
"express-enforces-ssl": "^1.1.0",
"express-sitemap": "^1.8.0",
"final-form": "^4.4.0",
"final-form-arrays": "^1.0.4",
"helmet": "^3.12.0",
"lodash": "^4.17.5",
"moment": "^2.20.1",
@ -26,21 +29,23 @@
"path-to-regexp": "^2.2.0",
"prop-types": "^15.6.1",
"query-string": "^5.1.1",
"raf": "3.4.0",
"raven": "^2.4.2",
"raven-js": "^3.24.0",
"react": "^15.6.2",
"react-addons-shallow-compare": "^15.6.2",
"react": "^16.3.1",
"react-dates": "^16.0.0",
"react-dom": "^15.6.2",
"react-dom": "^16.3.1",
"react-final-form": "^3.1.5",
"react-final-form-arrays": "^1.0.4",
"react-google-maps": "^9.4.5",
"react-helmet": "^5.2.0",
"react-intl": "^2.4.0",
"react-moment-proptypes": "^1.5.0",
"react-redux": "^5.0.6",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-sortable-hoc": "^0.6.8",
"redux": "^3.7.2",
"redux-form": "^6.8.0",
"redux-form": "^7.3.0",
"redux-thunk": "^2.2.0",
"sanitize.css": "^5.0.0",
"sharetribe-scripts": "1.1.2",
@ -51,14 +56,12 @@
},
"devDependencies": {
"enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.3",
"nodemon": "^1.17.2",
"nsp": "^3.2.1",
"nsp-preprocessor-yarn": "^1.0.1",
"prettier": "^1.11.1",
"react-addons-test-utils": "^15.6.2",
"react-test-renderer": "^15.6.2"
"prettier": "^1.11.1"
},
"scripts": {
"clean": "rm -rf build/*",

View file

@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { toPairs } from 'lodash';
import { ensureListing } from '../../util/data';
import { EditListingFeaturesForm } from '../../containers';
@ -38,15 +37,8 @@ const EditListingFeaturesPanel = props => {
<FormattedMessage id="EditListingFeaturesPanel.createListingTitle" />
);
const currentFeaturesArray = publicData && publicData.amenities;
const currentFeatures =
currentFeaturesArray &&
currentFeaturesArray.reduce((map, key) => {
map[key] = true;
return map;
}, {});
const initialValues = { [FEATURES_NAME]: currentFeatures };
const amenities = publicData && publicData.amenities;
const initialValues = { amenities };
return (
<div className={classes}>
@ -56,9 +48,7 @@ const EditListingFeaturesPanel = props => {
name={FEATURES_NAME}
initialValues={initialValues}
onSubmit={values => {
const entries = values[FEATURES_NAME] ? toPairs(values[FEATURES_NAME]) : [];
const amenities = entries.filter(entry => entry[1] === true).map(entry => entry[0]);
const { amenities = [] } = values;
const updatedValues = {
publicData: { amenities },

View file

@ -40,10 +40,12 @@ const EditListingPoliciesPanel = props => {
<EditListingPoliciesForm
className={css.form}
publicData={publicData}
initialValues={{ rules: publicData.rules }}
onSubmit={values => {
const { rules = '' } = values;
const updateValues = {
publicData: {
...values,
rules,
},
};
onSubmit(updateValues);

View file

@ -1,37 +1,51 @@
/* eslint-disable no-console */
import React from 'react';
import { reduxForm } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import * as validators from '../../util/validators';
import FieldBirthdayInput from './FieldBirthdayInput';
const formName = 'Styleguide.BirthdayInput.Form';
const FormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const { handleSubmit, onChange, values } = fieldRenderProps;
const required = validators.required('A valid date is required');
const minAge = 18;
const minAgeRequired = validators.ageAtLeast(`Age should be at least ${minAge}`, minAge);
const FormComponent = () => {
const required = validators.required('A valid date is required');
const minAge = 18;
const minAgeRequired = validators.ageAtLeast(`Age should be at least ${minAge}`, minAge);
return (
<form>
<FieldBirthdayInput
id={`${formName}.birthday`}
name="birthday"
label="Date of birth"
format={null}
validate={[required, minAgeRequired]}
/>
</form>
);
};
const Form = reduxForm({
form: formName,
})(FormComponent);
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldBirthdayInput
id={`birthday`}
name="birthday"
label="Date of birth"
format={null}
valueFromForm={values.birthDate}
validate={validators.composeValidators(required, minAgeRequired)}
/>
</form>
);
}}
/>
);
export const Empty = {
component: Form,
component: FormComponent,
props: {
onChange: ({ birthday }) => {
console.log('birthday changed to:', birthday ? birthday.toUTCString() : birthday);
onChange: formState => {
const birthday = formState.values.birthday;
if (birthday) {
console.log('birthday changed to:', birthday.toUTCString());
}
},
onSubmit: values => {
console.log('BirthdayInput.Form submitted values:', values);
},
},
group: 'custom inputs',

View file

@ -1,6 +1,10 @@
/**
* NOTE this component is part of react-final-form instead of Redux Form.
*/
import React, { Component } from 'react';
import { func, instanceOf, object, node, string, bool } from 'prop-types';
import { Field } from 'redux-form';
import { Field } from 'react-final-form';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import { range } from 'lodash';
@ -8,7 +12,7 @@ import { ValidationError } from '../../components';
import css from './FieldBirthdayInput.css';
// Since redux-form tracks the onBlur event for marking the field as
// Since final-form tracks the onBlur event for marking the field as
// touched (which triggers possible error validation rendering), only
// trigger the event asynchronously when no other input within this
// component has received focus.
@ -88,14 +92,14 @@ class BirthdayInputComponent extends Component {
this.handleSelectChange = this.handleSelectChange.bind(this);
}
componentWillMount() {
const value = this.props.value;
const value = this.props.valueFromForm;
if (value instanceof Date) {
this.setState({ selected: selectedFromDate(value) });
}
}
componentWillReceiveProps(newProps) {
const oldValue = this.props.value;
const newValue = newProps.value;
const oldValue = this.props.valueFromForm;
const newValue = newProps.valueFromForm;
const valueChanged = oldValue !== newValue;
if (valueChanged && newValue instanceof Date) {
this.setState({ selected: selectedFromDate(newValue) });
@ -219,7 +223,7 @@ BirthdayInputComponent.defaultProps = {
dateLabel: null,
monthLabel: null,
yearLabel: null,
value: null,
valueFromForm: null,
disabled: false,
};
@ -231,7 +235,7 @@ BirthdayInputComponent.propTypes = {
dateLabel: node,
monthLabel: node,
yearLabel: node,
value: instanceOf(Date),
valueFromForm: instanceOf(Date),
onChange: func.isRequired,
onFocus: func.isRequired,
onBlur: func.isRequired,

View file

@ -1,42 +1,50 @@
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import * as validators from '../../util/validators';
import { Button } from '../../components';
import FieldBoolean from './FieldBoolean';
const formName = 'Styleguide.FieldBoolean.Form';
const FormComponent = props => {
const { form, handleSubmit, invalid, pristine, submitting } = props;
const required = validators.requiredBoolean('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form onSubmit={handleSubmit}>
<FieldBoolean
id={`${form}.boolOption`}
name="boolOption"
label="Boolean option"
placeholder="Choose yes/no"
validate={required}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
};
FormComponent.propTypes = formPropTypes;
const Form = reduxForm({
form: formName,
})(FormComponent);
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = props;
const required = validators.requiredBoolean('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldBoolean
id={`${form}.boolOption`}
name="boolOption"
label="Boolean option"
placeholder="Choose yes/no"
validate={required}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);
export const YesNo = {
component: Form,
component: FormComponent,
props: {
onChange(values) {
console.log('onChange:', values);
onChange: formState => {
if (Object.keys(formState.values).length > 0) {
console.log('form values changed to:', formState.values);
}
},
onSubmit(values) {
console.log('onSubmit:', values);

View file

@ -1,6 +1,6 @@
import React from 'react';
import { injectIntl, intlShape } from 'react-intl';
import { SelectField } from '../../components';
import { FieldSelect } from '../../components';
const FieldBoolean = props => {
const { placeholder, intl, ...rest } = props;
@ -30,11 +30,11 @@ const FieldBoolean = props => {
},
};
return (
<SelectField {...selectProps}>
<FieldSelect {...selectProps}>
<option value="">{placeholder}</option>
<option value="true">{trueLabel}</option>
<option value="false">{falseLabel}</option>
</SelectField>
</FieldSelect>
);
};

View file

@ -1,43 +1,47 @@
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import { Button } from '../../components';
import FieldCheckbox from './FieldCheckbox';
import { required } from '../../util/validators';
const formName = 'Styleguide.FieldCheckbox.Form';
const FormComponent = props => {
const { form, handleSubmit, invalid, pristine, submitting } = props;
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;
const submitDisabled = invalid || pristine || submitting;
const submitDisabled = invalid || pristine || submitting;
return (
<form onSubmit={handleSubmit}>
<FieldCheckbox id="checkbox-id" name="checkbox-name" label="Check here, optional" />
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldCheckbox id="checkbox-id1" name="checkbox-group" label="option 1" value="option1" />
<FieldCheckbox id="checkbox-id2" name="checkbox-group" label="option 2" value="option2" />
<FieldCheckbox
id="checkbox-required-id"
name="checkbox-required-name"
label="Check here, required"
validate={required('This field is required')}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
};
FormComponent.propTypes = formPropTypes;
const Form = reduxForm({
form: formName,
})(FormComponent);
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);
export const Checkbox = {
component: Form,
component: FormComponent,
props: {
onChange: formState => {
if (Object.keys(formState.values).length > 0) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('Submit values of FieldCheckbox: ', values);
},

View file

@ -1,8 +1,11 @@
/**
* NOTE this component is part of react-final-form instead of Redux Form.
*/
import React from 'react';
import { any, node, string, object, shape } from 'prop-types';
import { node, string } from 'prop-types';
import classNames from 'classnames';
import { Field } from 'redux-form';
import { ValidationError } from '../../components';
import { Field } from 'react-final-form';
import css from './FieldCheckbox.css';
@ -35,32 +38,26 @@ IconCheckbox.defaultProps = { className: null };
IconCheckbox.propTypes = { className: string };
const FieldCheckboxComponent = props => {
const { rootClassName, className, svgClassName, id, label, input, meta, ...rest } = props;
const { rootClassName, className, svgClassName, id, label, ...rest } = props;
const classes = classNames(rootClassName || css.root, className);
const { value, ...inputProps } = input;
const checked = value === true;
const checkboxProps = {
id,
className: css.input,
component: 'input',
type: 'checkbox',
checked,
...inputProps,
...rest,
};
return (
<span className={classes}>
<input {...checkboxProps} />
<Field {...checkboxProps} />
<label htmlFor={id} className={css.label}>
<span className={css.checkboxWrapper}>
<IconCheckbox className={svgClassName} />
</span>
<span className={css.text}>{label}</span>
</label>
<ValidationError fieldMeta={meta} />
</span>
);
};
@ -76,16 +73,16 @@ FieldCheckboxComponent.propTypes = {
className: string,
rootClassName: string,
svgClassName: string,
// Id is needed to connect the label with input.
id: string.isRequired,
label: node,
// redux-form Field params
input: shape({ value: any }).isRequired,
meta: object.isRequired,
// Name groups several checkboxes to an array of selected values
name: string.isRequired,
// Checkbox needs a value that is passed forward when user checks the checkbox
value: string.isRequired,
};
const FieldCheckbox = props => {
return <Field component={FieldCheckboxComponent} {...props} />;
};
export default FieldCheckbox;
export default FieldCheckboxComponent;

View file

@ -0,0 +1,28 @@
@import '../../marketplace.css';
.root {
margin: 0;
padding: 0;
border: none;
}
.list {
margin: 0;
}
.twoColumns {
@media (--viewportMedium) {
columns: 2;
}
}
.item {
padding: 2px 0;
/* Fix broken multi-column layout in Chrome */
page-break-inside: avoid;
@media (--viewportMedium) {
padding: 4px 0;
}
}

View file

@ -0,0 +1,151 @@
import React from 'react';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
import { Button } from '../../components';
import FieldCheckboxGroup from './FieldCheckboxGroup';
import { requiredFieldArrayCheckbox } from '../../util/validators';
const formName = 'Styleguide.FieldCheckboxGroup';
const formNameRequired = 'Styleguide.FieldCheckboxGroupRequired';
const label = <h3>Amenities</h3>;
const commonProps = {
label: label,
options: [
{
key: 'towels',
label: 'Towels',
},
{
key: 'bathroom',
label: 'Bathroom',
},
{
key: 'swimming_pool',
label: 'Swimming pool',
},
{
key: 'own_drinks',
label: 'Own drinks allowed',
},
{
key: 'jacuzzi',
label: 'Jacuzzi',
},
{
key: 'audiovisual_entertainment',
label: 'Audiovisual entertainment',
},
{
key: 'barbeque',
label: 'Barbeque',
},
{
key: 'own_food_allowed',
label: 'Own food allowed',
},
],
twoColumns: true,
};
const optionalProps = {
name: 'amenities-optional',
id: 'amenities-optional',
...commonProps,
};
const requiredProps = {
name: 'amenities-required',
id: `${formNameRequired}.amenities-required`,
...commonProps,
validate: requiredFieldArrayCheckbox('this is required'),
};
const tosProps = {
name: 'terms-of-service',
id: `${formNameRequired}.tos-accepted`,
options: [
{
key: 'tos',
label: 'Terms of Service',
},
],
validate: requiredFieldArrayCheckbox('You need to accept Terms of Service'),
};
const formComponent = country => props => (
<FinalForm
{...props}
mutators={{ ...arrayMutators }}
render={fieldRenderProps => {
const { handleSubmit, invalid, submitting, componentProps, onChange } = fieldRenderProps;
const submitDisabled = invalid || submitting;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldCheckboxGroup {...componentProps} />
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);
export const Optional = {
component: formComponent(formName),
props: {
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('Submit values: ', values);
},
initialValues: { [optionalProps.name]: ['jacuzzi', 'towels'] },
componentProps: optionalProps,
},
group: 'inputs',
};
export const Required = {
component: formComponent(formNameRequired),
props: {
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('Submit values: ', values);
},
componentProps: requiredProps,
},
group: 'inputs',
};
export const ToSAccepted = {
component: formComponent(formNameRequired),
props: {
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('Submit values: ', values);
},
componentProps: tosProps,
},
group: 'inputs',
};

View file

@ -0,0 +1,96 @@
/**
* NOTE this component is part of react-final-form instead of Redux Form.
*/
/*
* Renders a group of checkboxes that can be used to select
* multiple values from a set of options.
*
* The corresponding component when rendering the selected
* values is PropertyGroup.
*
*/
import React, { Component } from 'react';
import { arrayOf, bool, node, shape, string } from 'prop-types';
import classNames from 'classnames';
import { FieldArray } from 'react-final-form-arrays';
import { FieldCheckbox, ValidationError } from '../../components';
import css from './FieldCheckboxGroup.css';
class FieldCheckboxRenderer extends Component {
constructor(props) {
super(props);
this.state = { touched: false };
}
componentWillReceiveProps(nextProps) {
// FieldArray doesn't have touched prop, so we'll keep track of it
if (!this.state.touched) {
this.setState({ touched: nextProps.meta.dirty });
}
}
render() {
const { className, rootClassName, label, twoColumns, id, fields, options, meta } = this.props;
const touched = this.state.touched;
const classes = classNames(rootClassName || css.root, className);
const listClasses = twoColumns ? classNames(css.list, css.twoColumns) : css.list;
return (
<fieldset className={classes}>
{label ? <legend>{label}</legend> : null}
<ul className={listClasses}>
{options.map((option, index) => {
const fieldId = `${id}.${option.key}`;
return (
<li key={fieldId} className={css.item}>
<FieldCheckbox
id={fieldId}
name={fields.name}
label={option.label}
value={option.key}
/>
</li>
);
})}
</ul>
<ValidationError fieldMeta={{ ...meta, touched }} />
</fieldset>
);
}
}
FieldCheckboxRenderer.defaultProps = {
rootClassName: null,
className: null,
label: null,
twoColumns: false,
};
FieldCheckboxRenderer.propTypes = {
rootClassName: string,
className: string,
id: string.isRequired,
label: node,
options: arrayOf(
shape({
key: string.isRequired,
label: node.isRequired,
})
).isRequired,
twoColumns: bool,
};
const FieldCheckboxGroup = props => <FieldArray component={FieldCheckboxRenderer} {...props} />;
// Name and component are required fields for FieldArray.
// Component-prop we define in this file, name needs to be passed in
FieldCheckboxGroup.propTypes = {
name: string.isRequired,
};
export default FieldCheckboxGroup;

View file

@ -2,7 +2,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IntlProvider, addLocaleData } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import en from 'react-intl/locale-data/en';
import fi from 'react-intl/locale-data/fi';
import { currencyConfig } from '../../util/test-data';
@ -81,36 +81,46 @@ export const defaultValueWithFiEUR = {
group: 'custom inputs',
};
const formName = 'Styleguide.FieldCurrencyInput.Form';
const FormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const { handleSubmit, onChange } = fieldRenderProps;
const required = validators.required('This field is required');
const FormComponent = props => {
const { form } = props;
const required = validators.required('This field is required');
return (
<form>
<FieldCurrencyInput
name="price"
id={`${form}.price`}
label="Set price:"
placeholder="Type in amount in EUR..."
currencyConfig={currencyConfigEUR}
validate={required}
/>
</form>
);
};
FormComponent.propTypes = formPropTypes;
const Form = reduxForm({
form: formName,
})(FormComponent);
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldCurrencyInput
id="FieldCurrencyInput.price"
name="price"
label="Set price:"
placeholder="Type in amount in EUR..."
currencyConfig={currencyConfigEUR}
validate={required}
/>
</form>
);
}}
/>
);
export const FieldInForm = {
component: Form,
component: FormComponent,
props: {
onChange: values => {
console.log('form values changed to:', values);
onChange: formState => {
if (formState.values && formState.values.price) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('FieldInForm submitted values:', values);
return false;
},
},
group: 'custom inputs',

View file

@ -1,3 +1,7 @@
/**
* NOTE this component is part of react-final-form instead of Redux Form.
*/
/**
* CurrencyInput renders an input field that format it's value according to currency formatting rules
* onFocus: renders given value in unformatted manner: "9999,99"
@ -6,7 +10,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import { Field } from 'redux-form';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import Decimal from 'decimal.js';
import { ValidationError } from '../../components';
@ -279,7 +283,7 @@ FieldCurrencyInputComponent.propTypes = {
id: string,
label: string,
// Generated by redux-form's Field component
// Generated by final-form's Field component
input: object.isRequired,
meta: object.isRequired,
};

View file

@ -46,6 +46,8 @@ const defaultProps = {
horizontalMargin: 0,
withPortal: false,
withFullScreenPortal: false,
appendToBody: false,
disableScroll: false,
initialVisibleMonth: null,
firstDayOfWeek: config.i18n.firstDayOfWeek,
numberOfMonths: 1,
@ -137,12 +139,12 @@ class DateInputComponent extends Component {
onBlur,
onChange,
onFocus,
onDragStart,
onDrop,
phrases,
screenReaderInputMessage,
useMobileMargins,
value,
children,
render,
...datePickerProps
} = this.props;
/* eslint-enable no-unused-vars */
@ -194,6 +196,7 @@ DateInputComponent.defaultProps = {
DateInputComponent.propTypes = {
className: string,
id: string,
focused: bool,
initialDate: instanceOf(Date),
intl: intlShape.isRequired,
@ -202,8 +205,6 @@ DateInputComponent.propTypes = {
onChange: func.isRequired,
onBlur: func.isRequired,
onFocus: func.isRequired,
onDragStart: func.isRequired,
onDrop: func.isRequired,
phrases: shape({
closeDatePicker: string,
clearDate: string,

View file

@ -1,34 +1,51 @@
/* eslint-disable no-console */
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import moment from 'moment';
import { Button } from '../../components';
import { required, bookingDateRequired } from '../../util/validators';
import { required, bookingDateRequired, composeValidators } from '../../util/validators';
import FieldDateInput from './FieldDateInput';
const FormComponent = props => {
const { form, handleSubmit, pristine, submitting, dateInputProps } = props;
const submitDisabled = pristine || submitting;
return (
<form onSubmit={handleSubmit}>
<FieldDateInput {...dateInputProps} />
<Button type="submit" disabled={submitDisabled} style={{ marginTop: '24px' }}>
Select
</Button>
</form>
);
};
const FormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
form,
handleSubmit,
onChange,
pristine,
submitting,
dateInputProps,
values,
} = fieldRenderProps;
const submitDisabled = pristine || submitting;
if (values && values.bookingDates) {
onChange(values.bookingDates);
}
FormComponent.propTypes = formPropTypes;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldDateInput {...dateInputProps} />
<Button type="submit" disabled={submitDisabled} style={{ marginTop: '24px' }}>
Select
</Button>
</form>
);
}}
/>
);
const defaultFormName = 'Styleguide.DateInput.Form';
const Form = reduxForm({
form: defaultFormName,
})(FormComponent);
const defaultFormName = 'FieldDateInputExampleForm';
export const Empty = {
component: Form,
component: FormComponent,
props: {
dateInputProps: {
name: 'bookingDate',
@ -37,13 +54,15 @@ export const Empty = {
label: 'Date',
placeholderText: moment().format('ddd, MMMM D'),
format: null,
validate: [required('Required'), bookingDateRequired('Date is not valid')],
validate: composeValidators(required('Required'), bookingDateRequired('Date is not valid')),
onBlur: () => console.log('onBlur called from DateInput props.'),
onFocus: () => console.log('onFocus called from DateInput props.'),
},
onChange: values => {
const { date } = values;
console.log('Changed to', moment(date).format('L'));
onChange: formState => {
const { date } = formState.values;
if (date) {
console.log('Changed to', moment(date).format('L'));
}
},
onSubmit: values => {
console.log('Submitting a form with values:', values);

View file

@ -1,12 +1,12 @@
/**
* Provides a date picker for Redux Forms (using https://github.com/airbnb/react-dates)
* Provides a date picker for Final Forms (using https://github.com/airbnb/react-dates)
*
* NOTE: If you are using this component inside BookingDatesForm,
* you should convert value.date to start date and end date before submitting it to API
*/
import React, { Component } from 'react';
import { bool, object, string } from 'prop-types';
import { Field } from 'redux-form';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { ValidationError } from '../../components';
@ -49,6 +49,7 @@ class FieldDateInputComponent extends Component {
onBlur: input.onBlur,
onFocus: input.onFocus,
useMobileMargins,
id,
...restOfInput,
...rest,
};

View file

@ -16,8 +16,6 @@ describe('DateInput', () => {
onBlur: noop,
onChange: noop,
onFocus: noop,
onDragStart: noop,
onDrop: noop,
id: 'bookingDate',
placeholderText: 'today',
};

View file

@ -21,7 +21,7 @@ import css from './DateRangeInput.css';
export const HORIZONTAL_ORIENTATION = 'horizontal';
export const ANCHOR_LEFT = 'left';
// Since redux-form tracks the onBlur event for marking the field as
// Since final-form tracks the onBlur event for marking the field as
// touched (which triggers possible error validation rendering), only
// trigger the event asynchronously when no other input within this
// component has received focus.
@ -89,6 +89,8 @@ const defaultProps = {
horizontalMargin: 0,
withPortal: false,
withFullScreenPortal: false,
appendToBody: false,
disableScroll: false,
daySize: 38,
isRTL: false,
initialVisibleMonth: null,
@ -202,12 +204,12 @@ class DateRangeInputComponent extends Component {
onBlur,
onChange,
onFocus,
onDragStart,
onDrop,
phrases,
screenReaderInputMessage,
useMobileMargins,
value,
children,
render,
...datePickerProps
} = this.props;
/* eslint-enable no-unused-vars */
@ -268,6 +270,8 @@ DateRangeInputComponent.defaultProps = {
DateRangeInputComponent.propTypes = {
className: string,
startDateId: string,
endDateId: string,
unitType: propTypes.bookingUnitType.isRequired,
focusedInput: oneOf([START_DATE, END_DATE]),
initialDates: instanceOf(Date),
@ -277,8 +281,6 @@ DateRangeInputComponent.propTypes = {
onChange: func.isRequired,
onBlur: func.isRequired,
onFocus: func.isRequired,
onDragStart: func.isRequired,
onDrop: func.isRequired,
phrases: shape({
closeDatePicker: string,
clearDate: string,

View file

@ -1,35 +1,48 @@
/* eslint-disable no-console */
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import moment from 'moment';
import { Button } from '../../components';
import { required, bookingDatesRequired } from '../../util/validators';
import { required, bookingDatesRequired, composeValidators } from '../../util/validators';
import { LINE_ITEM_NIGHT } from '../../util/types';
import FieldDateRangeInput from './FieldDateRangeInput';
const FormComponent = props => {
const { form, handleSubmit, pristine, submitting, dateInputProps } = props;
const submitDisabled = pristine || submitting;
return (
<form onSubmit={handleSubmit}>
<FieldDateRangeInput {...dateInputProps} />
<Button type="submit" disabled={submitDisabled} style={{ marginTop: '24px' }}>
Select
</Button>
</form>
);
};
const FormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
form,
handleSubmit,
onChange,
pristine,
submitting,
dateInputProps,
} = fieldRenderProps;
const submitDisabled = pristine || submitting;
FormComponent.propTypes = formPropTypes;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldDateRangeInput {...dateInputProps} />
<Button type="submit" disabled={submitDisabled} style={{ marginTop: '24px' }}>
Select
</Button>
</form>
);
}}
/>
);
const defaultFormName = 'Styleguide.DateRangeInput.Form';
const Form = reduxForm({
form: defaultFormName,
})(FormComponent);
const defaultFormName = 'FieldDateRangeInputExampleForm';
export const Empty = {
component: Form,
component: FormComponent,
props: {
dateInputProps: {
name: 'bookingDates',
@ -43,16 +56,18 @@ export const Empty = {
.add(1, 'days')
.format('ddd, MMMM D'),
format: null,
validate: [
validate: composeValidators(
required('Required'),
bookingDatesRequired('Start date is not valid', 'End date is not valid'),
],
bookingDatesRequired('Start date is not valid', 'End date is not valid')
),
onBlur: () => console.log('onBlur called from DateRangeInput props.'),
onFocus: () => console.log('onFocus called from DateRangeInput props.'),
},
onChange: values => {
const { startDate, endDate } = values;
console.log('Changed to', moment(startDate).format('L'), moment(endDate).format('L'));
onChange: formState => {
const { startDate, endDate } = formState.values;
if (startDate || endDate) {
console.log('Changed to', moment(startDate).format('L'), moment(endDate).format('L'));
}
},
onSubmit: values => {
console.log('Submitting a form with values:', values);

View file

@ -1,6 +1,13 @@
/**
* Provides a date picker for Final Forms (using https://github.com/airbnb/react-dates)
*
* NOTE: If you are using this component inside BookingDatesForm,
* you should convert value.date to start date and end date before submitting it to API
*/
import React, { Component } from 'react';
import { bool, func, object, oneOf, string } from 'prop-types';
import { Field } from 'redux-form';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { START_DATE, END_DATE } from '../../util/dates';
import { propTypes } from '../../util/types';
@ -117,6 +124,8 @@ class FieldDateRangeInputComponent extends Component {
...restOfInput,
...rest,
focusedInput: this.state.focusedInput,
startDateId,
endDateId,
};
const classes = classNames(rootClassName || css.fieldRoot, className);
const errorClasses = classNames({ [css.mobileMargins]: useMobileMargins });

View file

@ -18,8 +18,6 @@ describe('DateRangeInput', () => {
onBlur: noop,
onChange: noop,
onFocus: noop,
onDragStart: noop,
onDrop: noop,
startDateId: 'bookingStartDate',
startDatePlaceholderText: 'today',
endDateId: 'bookingEndDate',

View file

@ -0,0 +1,68 @@
@import '../../marketplace.css';
.root {
position: relative;
}
.input {
position: absolute;
opacity: 0;
height: 0;
width: 0;
/* Highlight the borders if the checkbox is hovered, focused or checked */
&:hover + label .box,
&:focus + label .box,
&:checked + label .box {
stroke: var(--marketplaceColor);
}
/* Display the "check" when checked */
&:checked + label .checked {
display: inline;
stroke: var(--marketplaceColor);
stroke-width: 1px;
}
/* Hightlight the text on checked, hover and focus */
&:focus + label .text,
&:hover + label .text,
&:checked + label .text {
color: var(--matterColorDark);
}
}
.label {
display: flex;
align-items: center;
padding: 0;
}
.checkboxWrapper {
/* This should follow line-height */
height: 32px;
margin-top: -1px;
margin-right: 12px;
align-self: baseline;
display: inline-flex;
align-items: center;
cursor: pointer;
}
.checked {
display: none;
fill: var(--marketplaceColor);
}
.box {
stroke: var(--matterColorAnti);
}
.text {
@apply --marketplaceListingAttributeFontStyles;
color: var(--matterColor);
margin-top: -1px;
margin-bottom: 1px;
cursor: pointer;
}

View file

@ -0,0 +1,91 @@
import React from 'react';
import { any, node, string, object, shape } from 'prop-types';
import classNames from 'classnames';
import { Field } from 'redux-form';
import { ValidationError } from '../../components';
import css from './FieldCheckbox.css';
const IconCheckbox = props => {
return (
<svg className={props.className} width="14" height="14" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<g transform="translate(2 2)">
<path
className={css.checked}
d="M9.9992985 1.5048549l-.0194517 6.9993137C9.977549 9.3309651 9.3066522 10 8.4798526 10H1.5001008c-.8284271 0-1.5-.6715729-1.5-1.5l-.000121-7c0-.8284271.6715728-1.5 1.5-1.5h.000121l6.9993246.0006862c.8284272.000067 1.4999458.671694 1.499879 1.5001211a1.5002208 1.5002208 0 0 1-.0000059.0040476z"
/>
<path
className={css.box}
strokeWidth="2"
d="M10.9992947 1.507634l-.0194518 6.9993137C10.9760133 9.8849417 9.8578519 11 8.4798526 11H1.5001008c-1.3807119 0-2.5-1.1192881-2.5-2.4999827L-1.0000202 1.5c0-1.3807119 1.119288-2.5 2.500098-2.5l6.9994284.0006862c1.3807118.0001115 2.4999096 1.11949 2.4997981 2.5002019-.0000018.003373-.0000018.003373-.0000096.0067458z"
/>
</g>
<path
d="M5.636621 10.7824771L3.3573694 8.6447948c-.4764924-.4739011-.4764924-1.2418639 0-1.7181952.4777142-.473901 1.251098-.473901 1.7288122 0l1.260291 1.1254782 2.8256927-4.5462307c.3934117-.5431636 1.1545778-.6695372 1.7055985-.278265.5473554.3912721.6731983 1.150729.2797866 1.6951077l-3.6650524 5.709111c-.2199195.306213-.5803433.5067097-.9920816.5067097-.3225487 0-.6328797-.1263736-.8637952-.3560334z"
fill="#FFF"
/>
</g>
</svg>
);
};
IconCheckbox.defaultProps = { className: null };
IconCheckbox.propTypes = { className: string };
const FieldCheckboxComponent = props => {
const { rootClassName, className, svgClassName, id, label, input, meta, ...rest } = props;
const classes = classNames(rootClassName || css.root, className);
const { value, ...inputProps } = input;
const checked = value === true;
const checkboxProps = {
id,
className: css.input,
type: 'checkbox',
checked,
...inputProps,
...rest,
};
return (
<span className={classes}>
<input {...checkboxProps} />
<label htmlFor={id} className={css.label}>
<span className={css.checkboxWrapper}>
<IconCheckbox className={svgClassName} />
</span>
<span className={css.text}>{label}</span>
</label>
<ValidationError fieldMeta={meta} />
</span>
);
};
FieldCheckboxComponent.defaultProps = {
className: null,
rootClassName: null,
svgClassName: null,
label: null,
};
FieldCheckboxComponent.propTypes = {
className: string,
rootClassName: string,
svgClassName: string,
id: string.isRequired,
label: node,
// redux-form Field params
input: shape({ value: any }).isRequired,
meta: object.isRequired,
};
const FieldCheckbox = props => {
return <Field component={FieldCheckboxComponent} {...props} />;
};
export default FieldCheckbox;

View file

@ -11,8 +11,9 @@ import React, { Component } from 'react';
import { arrayOf, bool, node, shape, string } from 'prop-types';
import classNames from 'classnames';
import { FieldArray } from 'redux-form';
import { FieldCheckbox, ValidationError } from '../../components';
import { ValidationError } from '../../components';
import FieldCheckbox from './FieldCheckbox';
import css from './FieldGroupCheckbox.css';
class FieldCheckboxRenderer extends Component {
@ -29,8 +30,7 @@ class FieldCheckboxRenderer extends Component {
}
render() {
const { className, rootClassName, label, twoColumns, id, options, fields, meta } = this.props;
const name = fields.name;
const { className, rootClassName, label, twoColumns, id, options, meta, name } = this.props;
const touched = this.state.touched;
@ -77,7 +77,11 @@ FieldCheckboxRenderer.propTypes = {
twoColumns: bool,
};
const FieldGroupCheckbox = props => <FieldArray component={FieldCheckboxRenderer} {...props} />;
// Redux Form: the name of FieldArray must be unique
// https://github.com/erikras/redux-form/issues/2740
const FieldGroupCheckbox = props => (
<FieldArray component={FieldCheckboxRenderer} name={`${props.name}.FieldArray`} props={props} />
);
// Name and component are required fields for FieldArray.
// Component-prop we define in this file, name needs to be passed in

View file

@ -1,40 +1,52 @@
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import * as validators from '../../util/validators';
import { Button } from '../../components';
import FieldPhoneNumberInput from './FieldPhoneNumberInput';
const formName = 'Styleguide.FieldPhoneNumberInput.Form';
const FormComponent = props => {
const { form, handleSubmit, invalid, pristine, submitting } = props;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form onSubmit={handleSubmit}>
<FieldPhoneNumberInput
id={`${form}.phoneNumber`}
name="phoneNumber"
label="Phone number"
placeholder="Phone number"
validate={required}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
};
FormComponent.propTypes = formPropTypes;
const Form = reduxForm({
form: formName,
})(FormComponent);
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
render={fieldRenderProps => {
const { formId, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldPhoneNumberInput
id={`${formId}.phoneNumber`}
name="phoneNumber"
label="Phone number"
placeholder="Phone number"
validate={required}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);
export const PhoneNumber = {
component: Form,
component: FormComponent,
props: {
formId: 'PhoneNumberExample',
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit(values) {
console.log('onSubmit:', values);
},

View file

@ -1,3 +1,7 @@
/**
* DEPRECATED: this component is part of Redux Form - we are migrating to react-final-form.
*/
/**
* A text field with phone number formatting. By default uses formatting
* rules defined in the fiFormatter.js file. To change the formatting
@ -6,7 +10,7 @@
*/
import React from 'react';
import { TextInputField } from '../../components';
import { FieldTextInput } from '../../components';
import { format, parse } from './fiFormatter';
const FieldPhoneNumberInput = props => {
@ -17,7 +21,7 @@ const FieldPhoneNumberInput = props => {
parse: parse,
};
return <TextInputField {...inputProps} />;
return <FieldTextInput {...inputProps} />;
};
export default FieldPhoneNumberInput;

View file

@ -1,41 +1,52 @@
/* eslint-disable no-console */
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import * as validators from '../../util/validators';
import { Button } from '../../components';
import FieldReviewRating from './FieldReviewRating';
const formName = 'Styleguide.FieldReviewRating.Form';
const FormComponent = props => {
const { form, handleSubmit, invalid, pristine, submitting } = props;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
const FormComponent = props => (
<FinalForm
{...props}
form={formName}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form onSubmit={handleSubmit}>
<FieldReviewRating
id={`${form}.rate1`}
name="rating"
label="Rate your experience"
validate={required}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
};
FormComponent.propTypes = formPropTypes;
const Form = reduxForm({
form: formName,
})(FormComponent);
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldReviewRating
id={`${form}.rate1`}
name="rating"
label="Rate your experience"
validate={required}
/>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);
export const StarRating = {
component: Form,
component: FormComponent,
props: {
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('submit values:', values);
},

View file

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import { Field } from 'redux-form';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { IconReviewStar, ValidationError } from '../../components';
@ -13,12 +13,6 @@ class FieldReviewRatingComponent extends Component {
this.handleChange = this.handleChange.bind(this);
}
componentWillUnmount() {
if (this.props.clearOnUnmount) {
this.props.input.onChange('');
}
}
handleChange(event) {
this.props.input.onChange(event.target.value);
}
@ -29,7 +23,6 @@ class FieldReviewRatingComponent extends Component {
rootClassName,
className,
inputRootClass,
clearOnUnmount,
customErrorText,
id,
intl,
@ -105,17 +98,15 @@ class FieldReviewRatingComponent extends Component {
FieldReviewRatingComponent.defaultProps = {
rootClassName: null,
className: null,
clearOnUnmount: false,
customErrorText: null,
label: null,
};
const { string, bool, shape, func, object } = PropTypes;
const { string, shape, func, object } = PropTypes;
FieldReviewRatingComponent.propTypes = {
rootClassName: string,
className: string,
clearOnUnmount: bool,
id: string.isRequired,
label: string,

View file

@ -0,0 +1,18 @@
@import '../../marketplace.css';
.root {
}
.select {
color: var(--matterColorAnti);
border-bottom-color: var(--attentionColor);
}
.selectSuccess {
color: var(--matterColor);
border-bottom-color: var(--successColor);
}
.selectError {
border-bottom-color: var(--failColor);
}

View file

@ -0,0 +1,51 @@
/* eslint-disable no-console */
import React from 'react';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import * as validators from '../../util/validators';
import { Button } from '../../components';
import FieldSelect from './FieldSelect';
const FormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const { form, handleSubmit, onChange, invalid, pristine, submitting } = fieldRenderProps;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldSelect id="select1" name="select1" label="Choose an option:" validate={required}>
<option value="">Pick something...</option>
<option value="first">First option</option>
<option value="second">Second option</option>
</FieldSelect>
<Button style={{ marginTop: 24 }} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);
export const Select = {
component: FormComponent,
props: {
onChange: formState => {
if (formState.values.select1) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('submit values:', values);
return false;
},
},
group: 'inputs',
};

View file

@ -0,0 +1,72 @@
/**
* NOTE this component is part of react-final-form instead of Redux Form.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { ValidationError } from '../../components';
import css from './FieldSelect.css';
const FieldSelectComponent = props => {
const { rootClassName, className, id, label, input, meta, children, ...rest } = props;
if (label && !id) {
throw new Error('id required when a label is given');
}
const { valid, invalid, touched, error } = meta;
// Error message and input error styles are only shown if the
// field has been touched and the validation has failed.
const hasError = touched && invalid && error;
const selectClasses = classNames(css.select, {
[css.selectSuccess]: valid,
[css.selectError]: hasError,
});
const selectProps = { className: selectClasses, id, ...input, ...rest };
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
{label ? <label htmlFor={id}>{label}</label> : null}
<select {...selectProps}>{children}</select>
<ValidationError fieldMeta={meta} />
</div>
);
};
FieldSelectComponent.defaultProps = {
rootClassName: null,
className: null,
id: null,
label: null,
children: null,
};
const { string, object, node } = PropTypes;
FieldSelectComponent.propTypes = {
rootClassName: string,
className: string,
// Label is optional, but if it is given, an id is also required so
// the label can reference the input in the `for` attribute
id: string,
label: string,
// Generated by final-form's Field component
input: object.isRequired,
meta: object.isRequired,
children: node,
};
const FieldSelect = props => {
return <Field component={FieldSelectComponent} {...props} />;
};
export default FieldSelect;

View file

@ -0,0 +1,19 @@
@import '../../marketplace.css';
.root {
}
.input {
border-bottom-color: var(--attentionColor);
}
.inputSuccess {
border-bottom-color: var(--successColor);
}
.inputError {
border-bottom-color: var(--failColor);
}
.textarea {
}

View file

@ -0,0 +1,7 @@
.field {
margin-top: 24px;
}
.submit {
margin-top: 24px;
}

View file

@ -0,0 +1,90 @@
/* eslint-disable no-console */
import React from 'react';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import * as validators from '../../util/validators';
import { Button } from '../../components';
import FieldTextInput from './FieldTextInput';
import css from './FieldTextInput.example.css';
const FormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const { handleSubmit, onChange, invalid, pristine, submitting, formName } = fieldRenderProps;
const required = validators.required('This field is required');
const submitDisabled = invalid || pristine || submitting;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<FieldTextInput
className={css.field}
type="text"
id={`${formName}.input1`}
name="input1"
label="Input that requires a value:"
validate={required}
/>
<FieldTextInput
className={css.field}
type="text"
id={`${formName}.input2`}
name="input2"
label="Input that does not require a value:"
/>
<FieldTextInput
className={css.field}
type="text"
name="input3"
placeholder="Input without label..."
/>
<FieldTextInput
className={css.field}
type="textarea"
id={`${formName}.textarea1`}
name="textarea1"
label="Textarea that requires a value:"
validate={required}
/>
<FieldTextInput
className={css.field}
type="textarea"
id={`${formName}.textarea2`}
name="textarea2"
label="Textarea that does not require a value:"
/>
<FieldTextInput
className={css.field}
type="textarea"
name="textarea3"
placeholder="Textarea without label..."
/>
<Button className={css.submit} type="submit" disabled={submitDisabled}>
Submit
</Button>
</form>
);
}}
/>
);
export const Inputs = {
component: FormComponent,
props: {
formName: 'Inputs',
onChange: formState => {
if (Object.keys(formState.values).length > 0) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('submit values:', values);
},
},
group: 'inputs',
};

View file

@ -0,0 +1,121 @@
/**
* NOTE this component is part of react-final-form instead of Redux Form.
*/
import React, { Component } from 'react';
import { func, object, shape, string } from 'prop-types';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { ValidationError, ExpandingTextarea } from '../../components';
import css from './FieldTextInput.css';
const CONTENT_MAX_LENGTH = 5000;
class FieldTextInputComponent extends Component {
render() {
/* eslint-disable no-unused-vars */
const {
rootClassName,
className,
inputRootClass,
customErrorText,
id,
label,
type,
input,
meta,
onUnmount,
...rest
} = this.props;
/* eslint-enable no-unused-vars */
if (label && !id) {
throw new Error('id required when a label is given');
}
const { valid, invalid, touched, error } = meta;
const isTextarea = type === 'textarea';
const errorText = customErrorText || error;
// Error message and input error styles are only shown if the
// field has been touched and the validation has failed.
const hasError = !!customErrorText || !!(touched && invalid && error);
const fieldMeta = { touched: hasError, error: errorText };
const inputClasses =
inputRootClass ||
classNames(css.input, {
[css.inputSuccess]: valid,
[css.inputError]: hasError,
[css.textarea]: isTextarea,
});
const inputProps = isTextarea
? { className: inputClasses, id, rows: 1, maxLength: CONTENT_MAX_LENGTH, ...input, ...rest }
: { className: inputClasses, id, type, ...input, ...rest };
const classes = classNames(rootClassName || css.root, className);
return (
<div className={classes}>
{label ? <label htmlFor={id}>{label}</label> : null}
{isTextarea ? <ExpandingTextarea {...inputProps} /> : <input {...inputProps} />}
<ValidationError fieldMeta={fieldMeta} />
</div>
);
}
}
FieldTextInputComponent.defaultProps = {
rootClassName: null,
className: null,
inputRootClass: null,
onUnmount: null,
customErrorText: null,
id: null,
label: null,
};
FieldTextInputComponent.propTypes = {
rootClassName: string,
className: string,
inputRootClass: string,
onUnmount: func,
// Error message that can be manually passed to input field,
// overrides default validation message
customErrorText: string,
// Label is optional, but if it is given, an id is also required so
// the label can reference the input in the `for` attribute
id: string,
label: string,
// Either 'textarea' or something that is passed to the input element
type: string.isRequired,
// Generated by redux-form's Field component
input: shape({
onChange: func.isRequired,
}).isRequired,
meta: object.isRequired,
};
class FieldTextInput extends Component {
componentWillUnmount() {
// Unmounting happens too late if it is done inside Field component
// (Then Form has already registered its (new) fields and
// changing the value without corresponding field is prohibited in Final Form
if (this.props.onUnmount) {
this.props.onUnmount();
}
}
render() {
return <Field component={FieldTextInputComponent} {...this.props} />;
}
}
export default FieldTextInput;

View file

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Field } from 'redux-form';
import { Field } from 'react-final-form';
import { debounce } from 'lodash';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
@ -494,18 +494,14 @@ LocationAutocompleteInput.propTypes = {
export default LocationAutocompleteInput;
class LocationAutocompleteInputFieldComponent extends Component {
componentWillUnmount() {
if (this.props.clearOnUnmount) {
this.props.input.onChange('');
}
}
render() {
/* eslint-disable no-unused-vars */
const { rootClassName, labelClassName, clearOnUnmount, ...restProps } = this.props;
const { input, label, meta, ...otherProps } = restProps;
const { rootClassName, labelClassName, ...restProps } = this.props;
const { input, label, meta, valueFromForm, ...otherProps } = restProps;
/* eslint-enable no-unused-vars */
const value = typeof valueFromForm !== 'undefined' ? valueFromForm : input.value;
const locationAutocompleteProps = { label, meta, ...otherProps, input: { ...input, value } };
const labelInfo = label ? (
<label className={labelClassName} htmlFor={input.name}>
{label}
@ -515,7 +511,7 @@ class LocationAutocompleteInputFieldComponent extends Component {
return (
<div className={rootClassName}>
{labelInfo}
<LocationAutocompleteInput {...restProps} />
<LocationAutocompleteInput {...locationAutocompleteProps} />
<ValidationError fieldMeta={meta} />
</div>
);
@ -527,7 +523,6 @@ LocationAutocompleteInputFieldComponent.defaultProps = {
labelClassName: null,
type: null,
label: null,
clearOnUnmount: false,
};
LocationAutocompleteInputFieldComponent.propTypes = {
@ -539,7 +534,6 @@ LocationAutocompleteInputFieldComponent.propTypes = {
}).isRequired,
label: string,
meta: object.isRequired,
clearOnUnmount: bool,
};
export const LocationAutocompleteInputField = props => {

View file

@ -1,3 +1,7 @@
/**
* DEPRECATED: this component is part of Redux Form - we are migrating to react-final-form.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Field } from 'redux-form';

View file

@ -49,12 +49,7 @@ const SelectMultipleFilterFormComponent = props => {
contentRef={contentRef}
style={style}
>
<FieldGroupCheckbox
className={css.fieldGroup}
name={name}
id={`${form}.${name}`}
options={options}
/>
<FieldGroupCheckbox className={css.fieldGroup} name={name} id={`${form}`} options={options} />
<div className={css.buttonsWrapper}>
<button className={css.clearButton} type="button" onClick={handleClear}>
{clear}

View file

@ -1,45 +1,51 @@
/* eslint-disable no-console */
import React from 'react';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import { Button } from '../../components';
import { stripeCountryConfigs } from './StripeBankAccountTokenInputField.util';
import StripeBankAccountTokenInputField from './StripeBankAccountTokenInputField';
import * as validators from '../../util/validators';
const formComponent = country => {
const FormComponent = props => {
const { form, handleSubmit } = props;
const currency = stripeCountryConfigs(country).currency;
return (
<form onSubmit={handleSubmit}>
<StripeBankAccountTokenInputField
id={`${form}.token`}
name="token"
country={country}
currency={currency}
formName={form}
validate={validators.required(' ')}
/>
<Button style={{ marginTop: 24 }} type="submit">
Submit
</Button>
</form>
);
};
FormComponent.propTypes = formPropTypes;
return FormComponent;
};
const formComponent = country => props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const { formName, handleSubmit, onChange } = fieldRenderProps;
const currency = stripeCountryConfigs(country).currency;
return (
<form
onSubmit={e => {
e.preventDefault();
handleSubmit(e);
}}
>
<FormSpy onChange={onChange} />
<StripeBankAccountTokenInputField
id={`${formName}.token`}
name="token"
country={country}
currency={currency}
formName={formName}
validate={validators.required(' ')}
/>
<Button style={{ marginTop: 24 }} type="submit">
Submit
</Button>
</form>
);
}}
/>
);
// DE
const FormComponentEur = formComponent('DE');
const formNameEur = 'Styleguide.StripeBankAccountTokenInputField.formEur';
const FormEur = reduxForm({ form: formNameEur })(FormComponentEur);
export const DE_EUR = {
component: FormEur,
component: formComponent('DE'),
props: {
onChange: values => {
console.log('values changed to:', values);
formName: 'DE_EUR',
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('values submitted:', values);
@ -49,15 +55,14 @@ export const DE_EUR = {
};
// US
const FormComponentUsd = formComponent('US');
const formNameUsd = 'Styleguide.StripeBankAccountTokenInputField.formUsd';
const FormUsd = reduxForm({ form: formNameUsd })(FormComponentUsd);
export const US_USD = {
component: FormUsd,
component: formComponent('US'),
props: {
onChange: values => {
console.log('values changed to:', values);
formName: 'US_USD',
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('values submitted:', values);
@ -67,15 +72,14 @@ export const US_USD = {
};
// GB
const FormComponentGbGbp = formComponent('GB');
const formNameGbp = 'Styleguide.StripeBankAccountTokenInputField.formGbGbp';
const formGbGbp = reduxForm({ form: formNameGbp })(FormComponentGbGbp);
export const GB_GBP = {
component: formGbGbp,
component: formComponent('GB'),
props: {
onChange: values => {
console.log('values changed to:', values);
formName: 'GB_GBP',
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('values submitted:', values);
@ -85,15 +89,14 @@ export const GB_GBP = {
};
// AU
const FormComponentAuAud = formComponent('AU');
const formNameAuAud = 'Styleguide.StripeBankAccountTokenInputField.formAuAud';
const formAuAud = reduxForm({ form: formNameAuAud })(FormComponentAuAud);
export const AU_AUD = {
component: formAuAud,
component: formComponent('AU'),
props: {
onChange: values => {
console.log('values changed to:', values);
formName: 'AU_AUD',
onChange: formState => {
if (formState.dirty) {
console.log('form values changed to:', formState.values);
}
},
onSubmit: values => {
console.log('values submitted:', values);

View file

@ -1,8 +1,12 @@
/**
* NOTE this component is part of react-final-form instead of Redux Form.
*/
/* eslint-disable no-underscore-dangle */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import { Field } from 'redux-form';
import { Field } from 'react-final-form';
import classNames from 'classnames';
import { debounce } from 'lodash';
import config from '../../config';

View file

@ -1,3 +1,7 @@
/**
* DEPRECATED: this component is part of Redux Form - we are migrating to react-final-form.
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Field } from 'redux-form';

View file

@ -32,12 +32,15 @@ export { default as ExternalLink } from './ExternalLink/ExternalLink';
export { default as FieldBirthdayInput } from './FieldBirthdayInput/FieldBirthdayInput';
export { default as FieldBoolean } from './FieldBoolean/FieldBoolean';
export { default as FieldCheckbox } from './FieldCheckbox/FieldCheckbox';
export { default as FieldCheckboxGroup } from './FieldCheckboxGroup/FieldCheckboxGroup';
export { default as FieldCurrencyInput } from './FieldCurrencyInput/FieldCurrencyInput';
export { default as FieldDateInput } from './FieldDateInput/FieldDateInput';
export { default as FieldDateRangeInput } from './FieldDateRangeInput/FieldDateRangeInput';
export { default as FieldGroupCheckbox } from './FieldGroupCheckbox/FieldGroupCheckbox';
export { default as FieldPhoneNumberInput } from './FieldPhoneNumberInput/FieldPhoneNumberInput';
export { default as FieldReviewRating } from './FieldReviewRating/FieldReviewRating';
export { default as FieldSelect } from './FieldSelect/FieldSelect';
export { default as FieldTextInput } from './FieldTextInput/FieldTextInput';
export { default as Footer } from './Footer/Footer';
export { default as Form } from './Form/Form';
export { default as IconBannedUser } from './IconBannedUser/IconBannedUser';

View file

@ -79,7 +79,7 @@ exports[`AuthenticationPageComponent matches snapshot 1`] = `
]
}
/>
<ReduxForm
<LoginForm
inProgress={false}
onSubmit={[Function]}
/>

View file

@ -1,12 +1,11 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { string, bool } from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reduxForm, formValueSelector, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import moment from 'moment';
import { required, bookingDatesRequired } from '../../util/validators';
import { required, bookingDatesRequired, composeValidators } from '../../util/validators';
import { START_DATE, END_DATE } from '../../util/dates';
import { propTypes } from '../../util/types';
import config from '../../config';
@ -34,7 +33,7 @@ export class BookingDatesFormComponent extends Component {
// focus on that input, otherwise continue with the
// default handleSubmit function.
handleFormSubmit(e) {
const { startDate, endDate } = this.props.bookingDates;
const { startDate, endDate } = e.bookingDates || {};
if (!startDate) {
e.preventDefault();
this.setState({ focusedInput: START_DATE });
@ -42,28 +41,13 @@ export class BookingDatesFormComponent extends Component {
e.preventDefault();
this.setState({ focusedInput: END_DATE });
} else {
this.props.handleSubmit(e);
this.props.onSubmit(e);
}
}
render() {
const {
rootClassName,
className,
submitButtonWrapperClassName,
unitType,
bookingDates,
form,
price: unitPrice,
intl,
startDatePlaceholder,
endDatePlaceholder,
isOwnListing,
} = this.props;
const { startDate, endDate } = bookingDates;
const { rootClassName, className, price: unitPrice, ...rest } = this.props;
const classes = classNames(rootClassName || css.root, className);
const submitButtonClasses = classNames(css.submitButtonWrapper, submitButtonWrapperClassName);
if (!unitPrice) {
return (
@ -84,91 +68,124 @@ export class BookingDatesFormComponent extends Component {
);
}
const bookingStartLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingStartTitle' });
const bookingEndLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingEndTitle' });
const requiredMessage = intl.formatMessage({ id: 'BookingDatesForm.requiredDate' });
const startDateErrorMessage = intl.formatMessage({
id: 'FieldDateRangeInput.invalidStartDate',
});
const endDateErrorMessage = intl.formatMessage({ id: 'FieldDateRangeInput.invalidEndDate' });
// This is the place to collect breakdown estimation data. See the
// EstimatedBreakdownMaybe component to change the calculations
// for customised payment processes.
const bookingData =
startDate && endDate
? {
unitType,
unitPrice,
startDate,
endDate,
// NOTE: If unitType is `line-item/units`, a new picker
// for the quantity should be added to the form.
quantity: 1,
}
: null;
const bookingInfo = bookingData ? (
<div className={css.priceBreakdownContainer}>
<h3 className={css.priceBreakdownTitle}>
<FormattedMessage id="BookingDatesForm.priceBreakdownTitle" />
</h3>
<EstimatedBreakdownMaybe bookingData={bookingData} />
</div>
) : null;
const dateFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
};
const now = moment();
const today = now.startOf('day').toDate();
const tomorrow = now
.startOf('day')
.add(1, 'days')
.toDate();
const startDatePlaceholderText =
startDatePlaceholder || intl.formatDate(today, dateFormatOptions);
const endDatePlaceholderText =
endDatePlaceholder || intl.formatDate(tomorrow, dateFormatOptions);
return (
<Form className={className} onSubmit={this.handleFormSubmit}>
<FieldDateRangeInput
className={css.bookingDates}
name="bookingDates"
unitType={unitType}
startDateId={`${form}.bookingStartDate`}
startDateLabel={bookingStartLabel}
startDatePlaceholderText={startDatePlaceholderText}
endDateId={`${form}.bookingEndDate`}
endDateLabel={bookingEndLabel}
endDatePlaceholderText={endDatePlaceholderText}
focusedInput={this.state.focusedInput}
onFocusedInputChange={this.onFocusedInputChange}
format={null}
useMobileMargins
validate={[
required(requiredMessage),
bookingDatesRequired(startDateErrorMessage, endDateErrorMessage),
]}
/>
{bookingInfo}
<p className={css.smallPrint}>
<FormattedMessage
id={
isOwnListing ? 'BookingDatesForm.ownListing' : 'BookingDatesForm.youWontBeChargedInfo'
}
/>
</p>
<div className={submitButtonClasses}>
<PrimaryButton type="submit">
<FormattedMessage id="BookingDatesForm.requestToBook" />
</PrimaryButton>
</div>
</Form>
<FinalForm
{...rest}
unitPrice={unitPrice}
onSubmit={this.handleFormSubmit}
render={fieldRenderProps => {
const {
endDatePlaceholder,
startDatePlaceholder,
form,
handleSubmit,
intl,
isOwnListing,
submitButtonWrapperClassName,
unitPrice,
unitType,
values,
} = fieldRenderProps;
const { startDate, endDate } = values && values.bookingDates ? values.bookingDates : {};
const bookingStartLabel = intl.formatMessage({
id: 'BookingDatesForm.bookingStartTitle',
});
const bookingEndLabel = intl.formatMessage({ id: 'BookingDatesForm.bookingEndTitle' });
const requiredMessage = intl.formatMessage({ id: 'BookingDatesForm.requiredDate' });
const startDateErrorMessage = intl.formatMessage({
id: 'FieldDateRangeInput.invalidStartDate',
});
const endDateErrorMessage = intl.formatMessage({
id: 'FieldDateRangeInput.invalidEndDate',
});
// This is the place to collect breakdown estimation data. See the
// EstimatedBreakdownMaybe component to change the calculations
// for customised payment processes.
const bookingData =
startDate && endDate
? {
unitType,
unitPrice,
startDate,
endDate,
// NOTE: If unitType is `line-item/units`, a new picker
// for the quantity should be added to the form.
quantity: 1,
}
: null;
const bookingInfo = bookingData ? (
<div className={css.priceBreakdownContainer}>
<h3 className={css.priceBreakdownTitle}>
<FormattedMessage id="BookingDatesForm.priceBreakdownTitle" />
</h3>
<EstimatedBreakdownMaybe bookingData={bookingData} />
</div>
) : null;
const dateFormatOptions = {
weekday: 'short',
month: 'short',
day: 'numeric',
};
const now = moment();
const today = now.startOf('day').toDate();
const tomorrow = now
.startOf('day')
.add(1, 'days')
.toDate();
const startDatePlaceholderText =
startDatePlaceholder || intl.formatDate(today, dateFormatOptions);
const endDatePlaceholderText =
endDatePlaceholder || intl.formatDate(tomorrow, dateFormatOptions);
const submitButtonClasses = classNames(
css.submitButtonWrapper,
submitButtonWrapperClassName
);
return (
<Form onSubmit={handleSubmit} className={classes}>
<FieldDateRangeInput
className={css.bookingDates}
name="bookingDates"
unitType={unitType}
startDateId={`${form}.bookingStartDate`}
startDateLabel={bookingStartLabel}
startDatePlaceholderText={startDatePlaceholderText}
endDateId={`${form}.bookingEndDate`}
endDateLabel={bookingEndLabel}
endDatePlaceholderText={endDatePlaceholderText}
focusedInput={this.state.focusedInput}
onFocusedInputChange={this.onFocusedInputChange}
format={null}
useMobileMargins
validate={composeValidators(
required(requiredMessage),
bookingDatesRequired(startDateErrorMessage, endDateErrorMessage)
)}
/>
{bookingInfo}
<p className={css.smallPrint}>
<FormattedMessage
id={
isOwnListing
? 'BookingDatesForm.ownListing'
: 'BookingDatesForm.youWontBeChargedInfo'
}
/>
</p>
<div className={submitButtonClasses}>
<PrimaryButton type="submit">
<FormattedMessage id="BookingDatesForm.requestToBook" />
</PrimaryButton>
</div>
</Form>
);
}}
/>
);
}
}
@ -183,11 +200,7 @@ BookingDatesFormComponent.defaultProps = {
endDatePlaceholder: null,
};
const { instanceOf, shape, string, bool } = PropTypes;
BookingDatesFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
submitButtonWrapperClassName: string,
@ -196,12 +209,6 @@ BookingDatesFormComponent.propTypes = {
price: propTypes.money,
isOwnListing: bool,
// from formValueSelector
bookingDates: shape({
startDate: instanceOf(Date),
endDate: instanceOf(Date),
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
@ -210,21 +217,7 @@ BookingDatesFormComponent.propTypes = {
endDatePlaceholder: string,
};
const formName = 'BookingDates';
// When a field depends on the value of another field, we must connect
// to the store and select the required values to inject to the
// component.
//
// See: http://redux-form.com/6.6.1/examples/selectingFormValues/
const selector = formValueSelector(formName);
const mapStateToProps = state => ({ bookingDates: selector(state, 'bookingDates') || {} });
const BookingDatesForm = compose(
connect(mapStateToProps),
reduxForm({ form: formName }),
injectIntl
)(BookingDatesFormComponent);
const BookingDatesForm = compose(injectIntl)(BookingDatesFormComponent);
BookingDatesForm.displayName = 'BookingDatesForm';
export default BookingDatesForm;

View file

@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
import Decimal from 'decimal.js';
import { types as sdkTypes } from '../../util/sdkLoader';
import { renderShallow, renderDeep } from '../../util/test-helpers';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { fakeIntl } from '../../util/test-data';
import { LINE_ITEM_NIGHT, TRANSITION_REQUEST } from '../../util/types';
import { dateFromAPIToLocalNoon } from '../../util/dates';
import { BookingBreakdown } from '../../components';
@ -18,7 +18,6 @@ describe('BookingDatesForm', () => {
it('matches snapshot without selected dates', () => {
const tree = renderShallow(
<BookingDatesFormComponent
{...fakeFormProps}
unitType={LINE_ITEM_NIGHT}
intl={fakeIntl}
dispatch={noop}

View file

@ -1,48 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BookingDatesForm matches snapshot without selected dates 1`] = `
<Form
className={null}
contentRef={null}
onSubmit={[Function]}
>
<FieldDateRangeInput
endDateId="fakeTestForm.bookingEndDate"
endDateLabel="BookingDatesForm.bookingEndTitle"
endDatePlaceholderText="tomorrow"
focusedInput={null}
format={null}
name="bookingDates"
onFocusedInputChange={[Function]}
startDateId="fakeTestForm.bookingStartDate"
startDateLabel="BookingDatesForm.bookingStartTitle"
startDatePlaceholderText="today"
unitType="line-item/night"
useMobileMargins={true}
validate={
Array [
[Function],
[Function],
]
<ReactFinalForm
bookingDates={Object {}}
dispatch={[Function]}
endDatePlaceholder="tomorrow"
intl={
Object {
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"now": [Function],
}
/>
<p>
<FormattedMessage
id="BookingDatesForm.youWontBeChargedInfo"
values={Object {}}
/>
</p>
<div
className=""
>
<PrimaryButton
type="submit"
>
<FormattedMessage
id="BookingDatesForm.requestToBook"
values={Object {}}
/>
</PrimaryButton>
</div>
</Form>
}
isOwnListing={false}
onSubmit={[Function]}
render={[Function]}
startDatePlaceholder="today"
submitButtonWrapperClassName={null}
unitPrice={
Money {
"amount": 1099,
"currency": "USD",
}
}
unitType="line-item/night"
/>
`;

View file

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { formValueSelector, reduxForm, propTypes as formPropTypes } from 'redux-form';
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';
@ -13,7 +13,7 @@ import {
isChangeEmailWrongPassword,
isTooManyEmailVerificationRequestsError,
} from '../../util/errors';
import { FieldPhoneNumberInput, Form, PrimaryButton, TextInputField } from '../../components';
import { FieldPhoneNumberInput, Form, PrimaryButton, FieldTextInput } from '../../components';
import css from './ContactDetailsForm.css';
@ -25,6 +25,7 @@ class ContactDetailsFormComponent extends Component {
this.state = { showVerificationEmailSentMessage: false };
this.emailSentTimeoutId = null;
this.handleResendVerificationEmail = this.handleResendVerificationEmail.bind(this);
this.submittedValues = {};
}
componentWillUnmount() {
@ -43,262 +44,292 @@ class ContactDetailsFormComponent extends Component {
}
render() {
const {
rootClassName,
className,
saveEmailError,
savePhoneNumberError,
currentUser,
form,
handleSubmit,
submitting,
inProgress,
intl,
invalid,
ready,
sendVerificationEmailError,
sendVerificationEmailInProgress,
email,
phoneNumber,
} = this.props;
const user = ensureCurrentUser(currentUser);
if (!user.id) {
return null;
}
const { email: currentEmail, emailVerified, pendingEmail, profile } = user.attributes;
// email
// has the email changed
const emailChanged = currentEmail !== email;
const emailLabel = intl.formatMessage({
id: 'ContactDetailsForm.emailLabel',
});
const emailPlaceholder = currentEmail || '';
const emailRequiredMessage = intl.formatMessage({
id: 'ContactDetailsForm.emailRequired',
});
const emailRequired = validators.required(emailRequiredMessage);
const emailInvalidMessage = intl.formatMessage({
id: 'ContactDetailsForm.emailInvalid',
});
const emailValid = validators.emailFormatValid(emailInvalidMessage);
const tooManyVerificationRequests = isTooManyEmailVerificationRequestsError(
sendVerificationEmailError
);
const emailTakenErrorText = isChangeEmailTakenError(saveEmailError)
? intl.formatMessage({ id: 'ContactDetailsForm.emailTakenError' })
: null;
let resendEmailMessage = null;
if (tooManyVerificationRequests) {
resendEmailMessage = (
<span className={css.tooMany}>
<FormattedMessage id="ContactDetailsForm.tooManyVerificationRequests" />
</span>
);
} else if (sendVerificationEmailInProgress || this.state.showVerificationEmailSentMessage) {
resendEmailMessage = (
<span className={css.emailSent}>
<FormattedMessage id="ContactDetailsForm.emailSent" />
</span>
);
} else {
/* eslint-disable jsx-a11y/no-static-element-interactions */
resendEmailMessage = (
<span className={css.helperLink} onClick={this.handleResendVerificationEmail} role="button">
<FormattedMessage id="ContactDetailsForm.resendEmailVerificationText" />
</span>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
// Email status info: unverified, verified and pending email (aka changed unverified email)
let emailVerifiedInfo = null;
if (emailVerified && !pendingEmail && !emailChanged) {
// Current email is verified and there's no pending unverified email
emailVerifiedInfo = (
<span className={css.emailVerified}>
<FormattedMessage id="ContactDetailsForm.emailVerified" />
</span>
);
} else if (!emailVerified && !pendingEmail) {
// Current email is unverified. This is the email given in sign up form
emailVerifiedInfo = (
<span className={css.emailUnverified}>
<FormattedMessage
id="ContactDetailsForm.emailUnverified"
values={{ resendEmailMessage }}
/>
</span>
);
} else if (pendingEmail) {
// Current email has been tried to change, but the new address is not yet verified
const pendingEmailStyled = <span className={css.emailStyle}>{pendingEmail}</span>;
const pendingEmailCheckInbox = (
<span className={css.checkInbox}>
<FormattedMessage
id="ContactDetailsForm.pendingEmailCheckInbox"
values={{ pendingEmail: pendingEmailStyled }}
/>
</span>
);
emailVerifiedInfo = (
<span className={css.pendingEmailUnverified}>
<FormattedMessage
id="ContactDetailsForm.pendingEmailUnverified"
values={{ pendingEmailCheckInbox, resendEmailMessage }}
/>
</span>
);
}
// phone
const protectedData = profile.protectedData || {};
const currentPhoneNumber = protectedData.phoneNumber;
// has the phone number changed
const phoneNumberChanged = currentPhoneNumber !== phoneNumber;
const phonePlaceholder = intl.formatMessage({ id: 'ContactDetailsForm.phonePlaceholder' });
const phoneLabel = intl.formatMessage({ id: 'ContactDetailsForm.phoneLabel' });
// password
const passwordLabel = intl.formatMessage({
id: 'ContactDetailsForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'ContactDetailsForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'ContactDetailsForm.passwordRequired',
});
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'ContactDetailsForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordValidators = emailChanged ? [passwordRequired, passwordMinLength] : null;
const passwordFailedMessage = intl.formatMessage({
id: 'ContactDetailsForm.passwordFailed',
});
const passwordErrorText = isChangeEmailWrongPassword(saveEmailError)
? passwordFailedMessage
: null;
const confirmClasses = classNames(css.confirmChangesSection, {
[css.confirmChangesSectionVisible]: emailChanged,
});
// generic error
const isGenericEmailError = saveEmailError && !(emailTakenErrorText || passwordErrorText);
let genericError = null;
if (isGenericEmailError && savePhoneNumberError) {
genericError = (
<span className={css.error}>
<FormattedMessage id="ContactDetailsForm.genericFailure" />
</span>
);
} else if (isGenericEmailError) {
genericError = (
<span className={css.error}>
<FormattedMessage id="ContactDetailsForm.genericEmailFailure" />
</span>
);
} else if (savePhoneNumberError) {
genericError = (
<span className={css.error}>
<FormattedMessage id="ContactDetailsForm.genericPhoneNumberFailure" />
</span>
);
}
const classes = classNames(rootClassName || css.root, className);
const submitDisabled =
invalid || submitting || inProgress || !(emailChanged || phoneNumberChanged);
return (
<Form className={classes} onSubmit={handleSubmit}>
<div className={css.contactDetailsSection}>
<TextInputField
type="email"
name="email"
id={`${form}.email`}
label={emailLabel}
placeholder={emailPlaceholder}
validate={[emailRequired, emailValid]}
customErrorText={emailTakenErrorText}
/>
{emailVerifiedInfo}
<FieldPhoneNumberInput
className={css.phone}
name="phoneNumber"
id={`${form}.phoneNumber`}
label={phoneLabel}
placeholder={phonePlaceholder}
/>
</div>
<FinalForm
{...this.props}
render={fieldRenderProps => {
const {
rootClassName,
className,
saveEmailError,
savePhoneNumberError,
currentUser,
formId,
handleSubmit,
inProgress,
intl,
invalid,
ready,
sendVerificationEmailError,
sendVerificationEmailInProgress,
values,
} = fieldRenderProps;
const { email, phoneNumber } = values;
<div className={confirmClasses}>
<h3 className={css.confirmChangesTitle}>
<FormattedMessage id="ContactDetailsForm.confirmChangesTitle" />
</h3>
<p className={css.confirmChangesInfo}>
<FormattedMessage id="ContactDetailsForm.confirmChangesInfo" />
</p>
const user = ensureCurrentUser(currentUser);
<TextInputField
className={css.password}
type="password"
name="currentPassword"
id={`${form}.currentPassword`}
autoComplete="current-password"
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordValidators}
customErrorText={passwordErrorText}
/>
</div>
<div className={css.bottomWrapper}>
{genericError}
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={inProgress}
ready={ready}
disabled={submitDisabled}
>
<FormattedMessage id="ContactDetailsForm.saveChanges" />
</PrimaryButton>
</div>
</Form>
if (!user.id) {
return null;
}
const { email: currentEmail, emailVerified, pendingEmail, profile } = user.attributes;
// email
// has the email changed
const emailChanged = currentEmail !== email;
const emailLabel = intl.formatMessage({
id: 'ContactDetailsForm.emailLabel',
});
const emailPlaceholder = currentEmail || '';
const emailRequiredMessage = intl.formatMessage({
id: 'ContactDetailsForm.emailRequired',
});
const emailRequired = validators.required(emailRequiredMessage);
const emailInvalidMessage = intl.formatMessage({
id: 'ContactDetailsForm.emailInvalid',
});
const emailValid = validators.emailFormatValid(emailInvalidMessage);
const tooManyVerificationRequests = isTooManyEmailVerificationRequestsError(
sendVerificationEmailError
);
const emailTouched = this.submittedValues.email !== values.email;
const emailTakenErrorText = isChangeEmailTakenError(saveEmailError)
? intl.formatMessage({ id: 'ContactDetailsForm.emailTakenError' })
: null;
let resendEmailMessage = null;
if (tooManyVerificationRequests) {
resendEmailMessage = (
<span className={css.tooMany}>
<FormattedMessage id="ContactDetailsForm.tooManyVerificationRequests" />
</span>
);
} else if (
sendVerificationEmailInProgress ||
this.state.showVerificationEmailSentMessage
) {
resendEmailMessage = (
<span className={css.emailSent}>
<FormattedMessage id="ContactDetailsForm.emailSent" />
</span>
);
} else {
/* eslint-disable jsx-a11y/no-static-element-interactions */
resendEmailMessage = (
<span
className={css.helperLink}
onClick={this.handleResendVerificationEmail}
role="button"
>
<FormattedMessage id="ContactDetailsForm.resendEmailVerificationText" />
</span>
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
}
// Email status info: unverified, verified and pending email (aka changed unverified email)
let emailVerifiedInfo = null;
if (emailVerified && !pendingEmail && !emailChanged) {
// Current email is verified and there's no pending unverified email
emailVerifiedInfo = (
<span className={css.emailVerified}>
<FormattedMessage id="ContactDetailsForm.emailVerified" />
</span>
);
} else if (!emailVerified && !pendingEmail) {
// Current email is unverified. This is the email given in sign up form
emailVerifiedInfo = (
<span className={css.emailUnverified}>
<FormattedMessage
id="ContactDetailsForm.emailUnverified"
values={{ resendEmailMessage }}
/>
</span>
);
} else if (pendingEmail) {
// Current email has been tried to change, but the new address is not yet verified
const pendingEmailStyled = <span className={css.emailStyle}>{pendingEmail}</span>;
const pendingEmailCheckInbox = (
<span className={css.checkInbox}>
<FormattedMessage
id="ContactDetailsForm.pendingEmailCheckInbox"
values={{ pendingEmail: pendingEmailStyled }}
/>
</span>
);
emailVerifiedInfo = (
<span className={css.pendingEmailUnverified}>
<FormattedMessage
id="ContactDetailsForm.pendingEmailUnverified"
values={{ pendingEmailCheckInbox, resendEmailMessage }}
/>
</span>
);
}
// phone
const protectedData = profile.protectedData || {};
const currentPhoneNumber = protectedData.phoneNumber;
// has the phone number changed
const phoneNumberChanged = currentPhoneNumber !== phoneNumber;
const phonePlaceholder = intl.formatMessage({
id: 'ContactDetailsForm.phonePlaceholder',
});
const phoneLabel = intl.formatMessage({ id: 'ContactDetailsForm.phoneLabel' });
// password
const passwordLabel = intl.formatMessage({
id: 'ContactDetailsForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'ContactDetailsForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'ContactDetailsForm.passwordRequired',
});
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'ContactDetailsForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordValidators = emailChanged
? validators.composeValidators(passwordRequired, passwordMinLength)
: null;
const passwordFailedMessage = intl.formatMessage({
id: 'ContactDetailsForm.passwordFailed',
});
const passwordTouched = this.submittedValues.currentPassword !== values.currentPassword;
const passwordErrorText = isChangeEmailWrongPassword(saveEmailError)
? passwordFailedMessage
: null;
const confirmClasses = classNames(css.confirmChangesSection, {
[css.confirmChangesSectionVisible]: emailChanged,
});
// generic error
const isGenericEmailError = saveEmailError && !(emailTakenErrorText || passwordErrorText);
let genericError = null;
if (isGenericEmailError && savePhoneNumberError) {
genericError = (
<span className={css.error}>
<FormattedMessage id="ContactDetailsForm.genericFailure" />
</span>
);
} else if (isGenericEmailError) {
genericError = (
<span className={css.error}>
<FormattedMessage id="ContactDetailsForm.genericEmailFailure" />
</span>
);
} else if (savePhoneNumberError) {
genericError = (
<span className={css.error}>
<FormattedMessage id="ContactDetailsForm.genericPhoneNumberFailure" />
</span>
);
}
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 ||
pristineSinceLastSubmit ||
inProgress ||
!(emailChanged || phoneNumberChanged);
return (
<Form
className={classes}
onSubmit={e => {
this.submittedValues = values;
handleSubmit(e);
}}
>
<div className={css.contactDetailsSection}>
<FieldTextInput
type="email"
name="email"
id={formId ? `${formId}.email` : 'email'}
label={emailLabel}
placeholder={emailPlaceholder}
validate={validators.composeValidators(emailRequired, emailValid)}
customErrorText={emailTouched ? null : emailTakenErrorText}
/>
{emailVerifiedInfo}
<FieldPhoneNumberInput
className={css.phone}
name="phoneNumber"
id={formId ? `${formId}.phoneNumber` : 'phoneNumber'}
label={phoneLabel}
placeholder={phonePlaceholder}
/>
</div>
<div className={confirmClasses}>
<h3 className={css.confirmChangesTitle}>
<FormattedMessage id="ContactDetailsForm.confirmChangesTitle" />
</h3>
<p className={css.confirmChangesInfo}>
<FormattedMessage id="ContactDetailsForm.confirmChangesInfo" />
</p>
<FieldTextInput
className={css.password}
type="password"
name="currentPassword"
id={formId ? `${formId}.currentPassword` : 'currentPassword'}
autoComplete="current-password"
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordValidators}
customErrorText={passwordTouched ? null : passwordErrorText}
/>
</div>
<div className={css.bottomWrapper}>
{genericError}
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={inProgress}
ready={ready}
disabled={submitDisabled}
>
<FormattedMessage id="ContactDetailsForm.saveChanges" />
</PrimaryButton>
</div>
</Form>
);
}}
/>
);
}
}
@ -306,6 +337,7 @@ class ContactDetailsFormComponent extends Component {
ContactDetailsFormComponent.defaultProps = {
rootClassName: null,
className: null,
formId: null,
saveEmailError: null,
savePhoneNumberError: null,
inProgress: false,
@ -318,9 +350,9 @@ ContactDetailsFormComponent.defaultProps = {
const { bool, func, string } = PropTypes;
ContactDetailsFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
formId: string,
saveEmailError: propTypes.error,
savePhoneNumberError: propTypes.error,
inProgress: bool,
@ -329,23 +361,10 @@ ContactDetailsFormComponent.propTypes = {
ready: bool.isRequired,
sendVerificationEmailError: propTypes.error,
sendVerificationEmailInProgress: bool,
// from formValueSelector
email: string,
phoneNumber: string,
};
const formName = 'ContactDetailsForm';
const selector = formValueSelector(formName);
const mapStateToProps = state => ({
email: selector(state, 'email'),
phoneNumber: selector(state, 'phoneNumber'),
});
const ContactDetailsForm = compose(injectIntl)(ContactDetailsFormComponent);
const ContactDetailsForm = compose(
connect(mapStateToProps),
reduxForm({ form: formName }),
injectIntl
)(ContactDetailsFormComponent);
ContactDetailsForm.displayName = 'ContactDetailsForm';
export default ContactDetailsForm;

View file

@ -1,6 +1,6 @@
import React from 'react';
import { required } from '../../util/validators';
import { SelectField } from '../../components';
import { FieldSelect } from '../../components';
import css from './EditListingDescriptionForm.css';
@ -18,7 +18,7 @@ const CustomCategorySelectFieldMaybe = props => {
})
);
return categories ? (
<SelectField
<FieldSelect
className={css.category}
name={name}
id={id}
@ -31,7 +31,7 @@ const CustomCategorySelectFieldMaybe = props => {
{c.label}
</option>
))}
</SelectField>
</FieldSelect>
) : null;
};

View file

@ -1,117 +1,122 @@
import React from 'react';
import { bool, func, string, arrayOf, shape } from 'prop-types';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import { maxLength, required } from '../../util/validators';
import { Form, Button, TextInputField } from '../../components';
import { maxLength, required, composeValidators } from '../../util/validators';
import { Form, Button, FieldTextInput } from '../../components';
import CustomCategorySelectFieldMaybe from './CustomCategorySelectFieldMaybe';
import css from './EditListingDescriptionForm.css';
const TITLE_MAX_LENGTH = 60;
const EditListingDescriptionFormComponent = props => {
const {
className,
disabled,
handleSubmit,
intl,
form,
invalid,
saveActionMsg,
submitting,
updated,
updateError,
updateInProgress,
categories,
} = props;
const EditListingDescriptionFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
categories,
className,
disabled,
handleSubmit,
intl,
invalid,
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
} = fieldRenderProps;
const titleMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.title' });
const titlePlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titlePlaceholder',
});
const titleRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titleRequired',
});
const maxLengthMessage = intl.formatMessage(
{ id: 'EditListingDescriptionForm.maxLength' },
{
maxLength: TITLE_MAX_LENGTH,
}
);
const titleMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.title' });
const titlePlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titlePlaceholder',
});
const titleRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.titleRequired',
});
const maxLengthMessage = intl.formatMessage(
{ id: 'EditListingDescriptionForm.maxLength' },
{
maxLength: TITLE_MAX_LENGTH,
}
);
const descriptionMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.description' });
const descriptionPlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionPlaceholder',
});
const maxLength60Message = maxLength(maxLengthMessage, TITLE_MAX_LENGTH);
const descriptionRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionRequired',
});
const descriptionMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.description',
});
const descriptionPlaceholderMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionPlaceholder',
});
const maxLength60Message = maxLength(maxLengthMessage, TITLE_MAX_LENGTH);
const descriptionRequiredMessage = intl.formatMessage({
id: 'EditListingDescriptionForm.descriptionRequired',
});
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingDescriptionForm.updateFailed" />
</p>
) : null;
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingDescriptionForm.updateFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated;
const submitInProgress = submitting || updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<TextInputField
className={css.title}
type="text"
name="title"
id={`${form}.title`}
label={titleMessage}
placeholder={titlePlaceholderMessage}
maxLength={TITLE_MAX_LENGTH}
validate={[required(titleRequiredMessage), maxLength60Message]}
autoFocus
/>
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<FieldTextInput
id="title"
name="title"
className={css.title}
type="text"
label={titleMessage}
placeholder={titlePlaceholderMessage}
maxLength={TITLE_MAX_LENGTH}
validate={composeValidators(required(titleRequiredMessage), maxLength60Message)}
autoFocus
/>
<TextInputField
className={css.description}
type="textarea"
name="description"
id={`${form}.description`}
label={descriptionMessage}
placeholder={descriptionPlaceholderMessage}
validate={[required(descriptionRequiredMessage)]}
/>
<FieldTextInput
id="description"
name="description"
className={css.description}
type="textarea"
label={descriptionMessage}
placeholder={descriptionPlaceholderMessage}
validate={composeValidators(required(descriptionRequiredMessage))}
/>
<CustomCategorySelectFieldMaybe
name="category"
id={`${form}.category`}
categories={categories}
intl={intl}
/>
<CustomCategorySelectFieldMaybe
id="category"
name="category"
categories={categories}
intl={intl}
/>
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
};
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}}
/>
);
EditListingDescriptionFormComponent.defaultProps = { className: null, updateError: null };
EditListingDescriptionFormComponent.propTypes = {
...formPropTypes,
className: string,
intl: intlShape.isRequired,
onSubmit: func.isRequired,
@ -127,8 +132,4 @@ EditListingDescriptionFormComponent.propTypes = {
),
};
const formName = 'EditListingDescriptionForm';
export default compose(reduxForm({ form: formName }), injectIntl)(
EditListingDescriptionFormComponent
);
export default compose(injectIntl)(EditListingDescriptionFormComponent);

View file

@ -1,6 +1,6 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { fakeIntl } from '../../util/test-data';
import EditListingDescriptionForm from './EditListingDescriptionForm';
const noop = () => null;
@ -9,7 +9,6 @@ describe('EditListingDescriptionForm', () => {
it('matches snapshot', () => {
const tree = renderDeep(
<EditListingDescriptionForm
{...fakeFormProps}
intl={fakeIntl}
dispatch={noop}
onSubmit={v => v}

View file

@ -11,20 +11,18 @@ exports[`EditListingDescriptionForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="fakeTestForm.title"
htmlFor="title"
>
EditListingDescriptionForm.title
</label>
<input
autoFocus={true}
className=""
id="fakeTestForm.title"
id="title"
maxLength={60}
name="title"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="EditListingDescriptionForm.titlePlaceholder"
type="text"
@ -35,19 +33,17 @@ exports[`EditListingDescriptionForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="fakeTestForm.description"
htmlFor="description"
>
EditListingDescriptionForm.description
</label>
<textarea
className="undefined"
id="fakeTestForm.description"
id="description"
maxLength={5000}
name="description"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="EditListingDescriptionForm.descriptionPlaceholder"
rows={1}
@ -58,18 +54,16 @@ exports[`EditListingDescriptionForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="fakeTestForm.category"
htmlFor="category"
>
EditListingDescriptionForm.categoryLabel
</label>
<select
className=""
id="fakeTestForm.category"
id="category"
name="category"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
value=""
>

View file

@ -3,11 +3,7 @@ import EditListingFeaturesForm from './EditListingFeaturesForm';
const NAME = 'amenities';
const initialValueArray = ['towels', 'jacuzzi', 'bathroom'];
const initialValueMap = initialValueArray.reduce((map, value) => {
map[value] = true;
return map;
}, {});
const initialValues = { [NAME]: initialValueMap };
const initialValues = { [NAME]: initialValueArray };
export const Amenities = {
component: EditListingFeaturesForm,

View file

@ -1,64 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bool, func, string } from 'prop-types';
import classNames from 'classnames';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
import { FormattedMessage } from 'react-intl';
import { propTypes } from '../../util/types';
import config from '../../config';
import { Button, FieldGroupCheckbox, Form } from '../../components';
import { Button, FieldCheckboxGroup, Form } from '../../components';
import css from './EditListingFeaturesForm.css';
const EditListingFeaturesFormComponent = props => {
const {
form,
submitting,
disabled,
rootClassName,
className,
name,
handleSubmit,
saveActionMsg,
updated,
updateError,
updateInProgress,
} = props;
const EditListingFeaturesFormComponent = props => (
<FinalForm
{...props}
mutators={{ ...arrayMutators }}
render={fieldRenderProps => {
const {
disabled,
rootClassName,
className,
name,
handleSubmit,
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
} = fieldRenderProps;
const classes = classNames(rootClassName || css.root, className);
const submitReady = updated;
const submitInProgress = submitting || updateInProgress;
const submitDisabled = disabled || submitInProgress;
const classes = classNames(rootClassName || css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
const submitDisabled = disabled || submitInProgress;
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingFeaturesForm.updateFailed" />
</p>
) : null;
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingFeaturesForm.updateFailed" />
</p>
) : null;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<FieldGroupCheckbox
className={css.features}
id={`${form}.${name}`}
name={name}
options={config.custom.amenities}
/>
<FieldCheckboxGroup
className={css.features}
id={name}
name={name}
options={config.custom.amenities}
/>
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
};
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}}
/>
);
EditListingFeaturesFormComponent.defaultProps = {
rootClassName: null,
@ -66,20 +72,17 @@ EditListingFeaturesFormComponent.defaultProps = {
updateError: null,
};
const { bool, func, string } = PropTypes;
EditListingFeaturesFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
name: string.isRequired,
handleSubmit: func.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
};
const defaultFormName = 'EditListingFeaturesForm';
const EditListingFeaturesForm = EditListingFeaturesFormComponent;
export default reduxForm({ form: defaultFormName })(EditListingFeaturesFormComponent);
export default EditListingFeaturesForm;

View file

@ -1,99 +1,108 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reduxForm, formValueSelector, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import { autocompleteSearchRequired, autocompletePlaceSelected } from '../../util/validators';
import { Form, LocationAutocompleteInputField, Button, TextInputField } from '../../components';
import {
autocompleteSearchRequired,
autocompletePlaceSelected,
composeValidators,
} from '../../util/validators';
import { Form, LocationAutocompleteInputField, Button, FieldTextInput } from '../../components';
import css from './EditListingLocationForm.css';
export const EditListingLocationFormComponent = props => {
const {
className,
disabled,
handleSubmit,
intl,
form,
invalid,
saveActionMsg,
submitting,
updated,
updateError,
updateInProgress,
} = props;
export const EditListingLocationFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
className,
disabled,
handleSubmit,
intl,
invalid,
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
values,
} = fieldRenderProps;
const titleRequiredMessage = intl.formatMessage({ id: 'EditListingLocationForm.address' });
const addressPlaceholderMessage = intl.formatMessage({
id: 'EditListingLocationForm.addressPlaceholder',
});
const addressRequiredMessage = intl.formatMessage({
id: 'EditListingLocationForm.addressRequired',
});
const addressNotRecognizedMessage = intl.formatMessage({
id: 'EditListingLocationForm.addressNotRecognized',
});
const titleRequiredMessage = intl.formatMessage({ id: 'EditListingLocationForm.address' });
const addressPlaceholderMessage = intl.formatMessage({
id: 'EditListingLocationForm.addressPlaceholder',
});
const addressRequiredMessage = intl.formatMessage({
id: 'EditListingLocationForm.addressRequired',
});
const addressNotRecognizedMessage = intl.formatMessage({
id: 'EditListingLocationForm.addressNotRecognized',
});
const buildingMessage = intl.formatMessage({ id: 'EditListingLocationForm.building' });
const buildingPlaceholderMessage = intl.formatMessage({
id: 'EditListingLocationForm.buildingPlaceholder',
});
const buildingMessage = intl.formatMessage({ id: 'EditListingLocationForm.building' });
const buildingPlaceholderMessage = intl.formatMessage({
id: 'EditListingLocationForm.buildingPlaceholder',
});
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingLocationForm.updateFailed" />
</p>
) : null;
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingLocationForm.updateFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated;
const submitInProgress = submitting || updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<LocationAutocompleteInputField
className={css.locationAddress}
inputClassName={css.locationAutocompleteInput}
iconClassName={css.locationAutocompleteInputIcon}
predictionsClassName={css.predictionsRoot}
validClassName={css.validLocation}
autoFocus
name="location"
label={titleRequiredMessage}
placeholder={addressPlaceholderMessage}
format={null}
validate={[
autocompleteSearchRequired(addressRequiredMessage),
autocompletePlaceSelected(addressNotRecognizedMessage),
]}
/>
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<LocationAutocompleteInputField
className={css.locationAddress}
inputClassName={css.locationAutocompleteInput}
iconClassName={css.locationAutocompleteInputIcon}
predictionsClassName={css.predictionsRoot}
validClassName={css.validLocation}
autoFocus
name="location"
label={titleRequiredMessage}
placeholder={addressPlaceholderMessage}
format={null}
valueFromForm={values.location}
validate={composeValidators(
autocompleteSearchRequired(addressRequiredMessage),
autocompletePlaceSelected(addressNotRecognizedMessage)
)}
/>
<TextInputField
className={css.building}
type="text"
name="building"
id={`${form}.building`}
label={buildingMessage}
placeholder={buildingPlaceholderMessage}
/>
<FieldTextInput
className={css.building}
type="text"
name="building"
id="building"
label={buildingMessage}
placeholder={buildingPlaceholderMessage}
/>
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
};
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}}
/>
);
EditListingLocationFormComponent.defaultProps = {
selectedPlace: null,
@ -103,7 +112,6 @@ EditListingLocationFormComponent.defaultProps = {
const { func, string, bool } = PropTypes;
EditListingLocationFormComponent.propTypes = {
...formPropTypes,
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
@ -113,21 +121,4 @@ EditListingLocationFormComponent.propTypes = {
updateInProgress: bool.isRequired,
};
const formName = 'EditListingLocationForm';
// When a field depends on the value of another field, we must connect
// to the store and select the required values to inject to the
// component.
//
// See: http://redux-form.com/6.6.1/examples/selectingFormValues/
const selector = formValueSelector(formName);
const mapStateToProps = state => {
const location = selector(state, 'location');
return {
selectedPlace: location && location.selectedPlace ? location.selectedPlace : null,
};
};
export default compose(connect(mapStateToProps), reduxForm({ form: formName }), injectIntl)(
EditListingLocationFormComponent
);
export default compose(injectIntl)(EditListingLocationFormComponent);

View file

@ -1,7 +1,7 @@
// NOTE: renderdeep doesn't work due to Google Maps API integration
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { fakeIntl } from '../../util/test-data';
import { EditListingLocationFormComponent } from './EditListingLocationForm';
const noop = () => null;
@ -10,10 +10,9 @@ describe('EditListingLocationForm', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<EditListingLocationFormComponent
{...fakeFormProps}
intl={fakeIntl}
dispatch={noop}
onSubmit={v => v}
onSubmit={noop}
saveActionMsg="Save location"
updated={false}
updateInProgress={false}

View file

@ -1,40 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditListingLocationForm matches snapshot 1`] = `
<Form
className=""
contentRef={null}
onSubmit={[Function]}
>
<Component
autoFocus={true}
format={null}
label="EditListingLocationForm.address"
name="location"
placeholder="EditListingLocationForm.addressPlaceholder"
validate={
Array [
[Function],
[Function],
]
<ReactFinalForm
dispatch={[Function]}
intl={
Object {
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"now": [Function],
}
/>
<TextInputField
id="fakeTestForm.building"
label="EditListingLocationForm.building"
name="building"
placeholder="EditListingLocationForm.buildingPlaceholder"
type="text"
/>
<Button
className={null}
disabled={false}
inProgress={false}
ready={false}
rootClassName={null}
type="submit"
>
Save location
</Button>
</Form>
}
onSubmit={[Function]}
render={[Function]}
saveActionMsg="Save location"
selectedPlace={null}
updateError={null}
updateInProgress={false}
updated={false}
/>
`;

View file

@ -1,67 +1,30 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { array, bool, func, object, shape, string } from 'prop-types';
import { compose } from 'redux';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm, Field } from 'react-final-form';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { isEqual } from 'lodash';
import { arrayMove } from 'react-sortable-hoc';
import { isEqual } from 'lodash';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import { nonEmptyArray } from '../../util/validators';
import { nonEmptyArray, composeValidators } from '../../util/validators';
import { isUploadListingImageOverLimitError } from '../../util/errors';
import { Form, AddImages, Button, ValidationError } from '../../components';
import { AddImages, Button, Form, ValidationError } from '../../components';
import css from './EditListingPhotosForm.css';
const ACCEPT_IMAGES = 'image/*';
// Add image wrapper. Label is the only visible element, file input is hidden.
const RenderAddImage = props => {
const { accept, input, label, type, disabled } = props;
const { name, onChange } = input;
const inputProps = { accept, id: name, name, onChange, type };
return (
<div className={css.addImageWrapper}>
<div className={css.aspectRatioWrapper}>
{disabled ? null : <input {...inputProps} className={css.addImageInput} />}
<label htmlFor={name} className={css.addImage}>
{label}
</label>
</div>
</div>
);
};
const { bool, func, node, object, shape, string } = PropTypes;
RenderAddImage.propTypes = {
accept: string.isRequired,
input: shape({
value: object,
onChange: func.isRequired,
name: string.isRequired,
}).isRequired,
label: node.isRequired,
type: string.isRequired,
disabled: bool.isRequired,
};
export class EditListingPhotosFormComponent extends Component {
constructor(props) {
super(props);
this.state = { imageUploadRequested: false };
this.onImageUploadHandler = this.onImageUploadHandler.bind(this);
this.onSortEnd = this.onSortEnd.bind(this);
this.submittedImages = [];
}
componentWillReceiveProps(nextProps) {
if (!isEqual(this.props.images, nextProps.images)) {
nextProps.change('images', nextProps.images);
}
}
onImageUploadHandler(event) {
const file = event.target.files[0];
onImageUploadHandler(file) {
if (file) {
this.setState({ imageUploadRequested: true });
this.props
@ -81,151 +44,202 @@ export class EditListingPhotosFormComponent extends Component {
}
render() {
const {
className,
disabled,
errors,
handleSubmit,
images,
intl,
invalid,
saveActionMsg,
submitting,
updated,
ready,
updateError,
updateInProgress,
onRemoveImage,
} = this.props;
const chooseImageText = (
<span className={css.chooseImageText}>
<span className={css.chooseImage}>
<FormattedMessage id="EditListingPhotosForm.chooseImage" />
</span>
<span className={css.imageTypes}>
<FormattedMessage id="EditListingPhotosForm.imageTypes" />
</span>
</span>
);
const imageRequiredMessage = intl.formatMessage({ id: 'EditListingPhotosForm.imageRequired' });
const { createListingsError, showListingsError, uploadImageError } = errors;
const uploadOverLimit = isUploadListingImageOverLimitError(uploadImageError);
let uploadImageFailed = null;
if (uploadOverLimit) {
uploadImageFailed = (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.imageUploadFailed.uploadOverLimit" />
</p>
);
} else if (uploadImageError) {
uploadImageFailed = (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.imageUploadFailed.uploadFailed" />
</p>
);
}
// NOTE: These error messages are here since Photos panel is the last visible panel
// before creating a new listing. If that order is changed, these should be changed too.
// Create and show listing errors are shown above submit button
const createListingFailed = createListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.createListingFailed" />
</p>
) : null;
const showListingFailed = showListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.showListingFailed" />
</p>
) : null;
// Updating listing (edit mode) failed
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.updateFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated || ready;
const submitInProgress = submitting || updateInProgress;
const submitDisabled =
invalid || disabled || submitInProgress || this.state.imageUploadRequested || ready;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<AddImages
className={css.imagesField}
images={images}
onSortEnd={this.onSortEnd}
thumbnailClassName={css.thumbnail}
savedImageAltText={intl.formatMessage({ id: 'EditListingPhotosForm.savedImageAltText' })}
onRemoveImage={onRemoveImage}
>
<Field
id="EditListingPhotosForm.AddImages"
accept={ACCEPT_IMAGES}
component={RenderAddImage}
label={chooseImageText}
name="addImage"
onChange={this.onImageUploadHandler}
type="file"
disabled={this.state.imageUploadRequested}
/>
<FinalForm
{...this.props}
onImageUploadHandler={this.onImageUploadHandler}
imageUploadRequested={this.state.imageUploadRequested}
initialValues={{ images: this.props.images }}
render={fieldRenderProps => {
const {
form,
className,
disabled,
errors,
handleSubmit,
images,
imageUploadRequested,
intl,
invalid,
onImageUploadHandler,
onRemoveImage,
ready,
saveActionMsg,
updated,
updateError,
updateInProgress,
} = fieldRenderProps;
<Field
component={props => {
const { input, type, meta } = props;
return (
<div className={css.imageRequiredWrapper}>
<input {...input} type={type} />
<ValidationError fieldMeta={meta} />
</div>
);
}}
name="images"
type="hidden"
validate={[nonEmptyArray(imageRequiredMessage)]}
/>
</AddImages>
{uploadImageFailed}
const chooseImageText = (
<span className={css.chooseImageText}>
<span className={css.chooseImage}>
<FormattedMessage id="EditListingPhotosForm.chooseImage" />
</span>
<span className={css.imageTypes}>
<FormattedMessage id="EditListingPhotosForm.imageTypes" />
</span>
</span>
);
<p className={css.tip}>
<FormattedMessage id="EditListingPhotosForm.addImagesTip" />
</p>
{createListingFailed}
{showListingFailed}
const imageRequiredMessage = intl.formatMessage({
id: 'EditListingPhotosForm.imageRequired',
});
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
const { createListingsError, showListingsError, uploadImageError } = errors;
const uploadOverLimit = isUploadListingImageOverLimitError(uploadImageError);
let uploadImageFailed = null;
if (uploadOverLimit) {
uploadImageFailed = (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.imageUploadFailed.uploadOverLimit" />
</p>
);
} else if (uploadImageError) {
uploadImageFailed = (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.imageUploadFailed.uploadFailed" />
</p>
);
}
// NOTE: These error messages are here since Photos panel is the last visible panel
// before creating a new listing. If that order is changed, these should be changed too.
// Create and show listing errors are shown above submit button
const createListingFailed = createListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.createListingFailed" />
</p>
) : null;
const showListingFailed = showListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.showListingFailed" />
</p>
) : null;
const submittedOnce = this.submittedImages.length > 0;
// imgs can contain added images (with temp ids) and submitted images with uniq ids.
const arrayOfImgIds = imgs =>
imgs.map(i => (typeof i.id === 'string' ? i.imageId : i.id));
const imageIdsFromProps = arrayOfImgIds(images);
const imageIdsFromPreviousSubmit = arrayOfImgIds(this.submittedImages);
const imageArrayHasSameImages = isEqual(imageIdsFromProps, imageIdsFromPreviousSubmit);
const pristineSinceLastSubmit = submittedOnce && imageArrayHasSameImages;
const submitReady = (updated && pristineSinceLastSubmit) || ready;
const submitInProgress = updateInProgress;
const submitDisabled =
invalid || disabled || submitInProgress || imageUploadRequested || ready;
const classes = classNames(css.root, className);
return (
<Form
className={classes}
onSubmit={e => {
this.submittedImages = images;
handleSubmit(e);
}}
>
{updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.updateFailed" />
</p>
) : null}
<AddImages
className={css.imagesField}
images={images}
onSortEnd={this.onSortEnd}
thumbnailClassName={css.thumbnail}
savedImageAltText={intl.formatMessage({
id: 'EditListingPhotosForm.savedImageAltText',
})}
onRemoveImage={onRemoveImage}
>
<Field
id="addImage"
name="addImage"
accept={ACCEPT_IMAGES}
form={null}
label={chooseImageText}
type="file"
disabled={imageUploadRequested}
>
{fieldprops => {
const { accept, input, label, type, disabled } = fieldprops;
const { name } = input;
const onChange = e => {
const file = e.target.files[0];
form.change(`addImage`, file);
form.blur(`addImage`);
onImageUploadHandler(file);
};
const inputProps = { accept, id: name, name, onChange, type };
return (
<div className={css.addImageWrapper}>
<div className={css.aspectRatioWrapper}>
{disabled ? null : (
<input {...inputProps} className={css.addImageInput} />
)}
<label htmlFor={name} className={css.addImage}>
{label}
</label>
</div>
</div>
);
}}
</Field>
<Field
component={props => {
const { input, type, meta } = props;
return (
<div className={css.imageRequiredWrapper}>
<input {...input} type={type} />
<ValidationError fieldMeta={meta} />
</div>
);
}}
name="images"
type="hidden"
validate={composeValidators(nonEmptyArray(imageRequiredMessage))}
/>
</AddImages>
{uploadImageFailed}
<p className={css.tip}>
<FormattedMessage id="EditListingPhotosForm.addImagesTip" />
</p>
{createListingFailed}
{showListingFailed}
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}}
/>
);
}
}
EditListingPhotosFormComponent.defaultProps = { errors: {}, updateError: null };
EditListingPhotosFormComponent.defaultProps = { errors: {}, updateError: null, images: [] };
EditListingPhotosFormComponent.propTypes = {
...formPropTypes,
errors: shape({
createListingsError: object,
showListingsError: object,
uploadImageError: object,
}),
images: array,
intl: intlShape.isRequired,
onImageUpload: func.isRequired,
onUpdateImageOrder: func.isRequired,
@ -238,6 +252,4 @@ EditListingPhotosFormComponent.propTypes = {
onRemoveImage: func.isRequired,
};
const formName = 'EditListingPhotosForm';
export default compose(reduxForm({ form: formName }), injectIntl)(EditListingPhotosFormComponent);
export default compose(injectIntl)(EditListingPhotosFormComponent);

View file

@ -1,10 +1,6 @@
// NOTE: renderdeep doesn't work due to
// "Invariant Violation: getNodeFromInstance: Invalid argument."
// refs and findDOMNode are not supported by react-test-renderer
// (react-sortable-hoc uses them)
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { fakeIntl } from '../../util/test-data';
import { EditListingPhotosFormComponent } from './EditListingPhotosForm';
const noop = () => null;
@ -13,7 +9,6 @@ describe('EditListingPhotosForm', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<EditListingPhotosFormComponent
{...fakeFormProps}
initialValues={{ country: 'US', images: [] }}
intl={fakeIntl}
dispatch={noop}

View file

@ -1,76 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditListingPhotosForm matches snapshot 1`] = `
<Form
className=""
contentRef={null}
<ReactFinalForm
dispatch={[Function]}
errors={Object {}}
imageUploadRequested={false}
images={Array []}
initialValues={
Object {
"images": Array [],
}
}
intl={
Object {
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"now": [Function],
}
}
onImageUpload={[Function]}
onImageUploadHandler={[Function]}
onRemoveImage={[Function]}
onSubmit={[Function]}
>
<AddImages
className={null}
images={Array []}
onRemoveImage={[Function]}
onSortEnd={[Function]}
savedImageAltText="EditListingPhotosForm.savedImageAltText"
thumbnailClassName={null}
>
<Field
accept="image/*"
component={[Function]}
disabled={false}
id="EditListingPhotosForm.AddImages"
label={
<span
className={undefined}
>
<span
className={undefined}
>
<FormattedMessage
id="EditListingPhotosForm.chooseImage"
values={Object {}}
/>
</span>
<span
className={undefined}
>
<FormattedMessage
id="EditListingPhotosForm.imageTypes"
values={Object {}}
/>
</span>
</span>
}
name="addImage"
onChange={[Function]}
type="file"
/>
<Field
component={[Function]}
name="images"
type="hidden"
validate={
Array [
[Function],
]
}
/>
</AddImages>
<p>
<FormattedMessage
id="EditListingPhotosForm.addImagesTip"
values={Object {}}
/>
</p>
<Button
className={null}
disabled={false}
inProgress={false}
ready={false}
rootClassName={null}
type="submit"
>
Save photos
</Button>
</Form>
onUpdateImageOrder={[Function]}
ready={false}
render={[Function]}
saveActionMsg="Save photos"
stripeConnected={false}
updateError={null}
updateInProgress={false}
updated={false}
/>
`;

View file

@ -1,82 +1,76 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import { Form, Button, TextInputField } from '../../components';
import { Form, Button, FieldTextInput } from '../../components';
import css from './EditListingPoliciesForm.css';
export class EditListingPoliciesFormComponent extends Component {
constructor(props) {
super(props);
export const EditListingPoliciesFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
className,
disabled,
handleSubmit,
intl,
invalid,
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
} = fieldRenderProps;
// Initialize form inside this component reduces the amount of files that are tied to
// marketplace specific content in publicData.
const { initialize, publicData } = props;
const { rules = '' } = publicData;
initialize({ rules });
}
const rulesLabelMessage = intl.formatMessage({
id: 'EditListingPoliciesForm.rulesLabel',
});
const rulesPlaceholderMessage = intl.formatMessage({
id: 'EditListingPoliciesForm.rulesPlaceholder',
});
render() {
const {
className,
disabled,
handleSubmit,
intl,
form,
invalid,
saveActionMsg,
submitting,
updated,
updateError,
updateInProgress,
} = this.props;
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPoliciesForm.updateFailed" />
</p>
) : null;
const rulesLabelMessage = intl.formatMessage({ id: 'EditListingPoliciesForm.rulesLabel' });
const rulesPlaceholderMessage = intl.formatMessage({
id: 'EditListingPoliciesForm.rulesPlaceholder',
});
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPoliciesForm.updateFailed" />
</p>
) : null;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
const classes = classNames(css.root, className);
const submitReady = updated;
const submitInProgress = submitting || updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
<FieldTextInput
id="rules"
name="rules"
className={css.policy}
type="textarea"
label={rulesLabelMessage}
placeholder={rulesPlaceholderMessage}
/>
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<TextInputField
className={css.policy}
type="textarea"
name="rules"
id={`${form}.rules`}
label={rulesLabelMessage}
placeholder={rulesPlaceholderMessage}
/>
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}
}
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}}
/>
);
EditListingPoliciesFormComponent.defaultProps = {
selectedPlace: null,
@ -86,7 +80,6 @@ EditListingPoliciesFormComponent.defaultProps = {
const { func, string, bool } = PropTypes;
EditListingPoliciesFormComponent.propTypes = {
...formPropTypes,
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
@ -96,6 +89,4 @@ EditListingPoliciesFormComponent.propTypes = {
updateInProgress: bool.isRequired,
};
const formName = 'EditListingPoliciesForm';
export default compose(reduxForm({ form: formName }), injectIntl)(EditListingPoliciesFormComponent);
export default compose(injectIntl)(EditListingPoliciesFormComponent);

View file

@ -1,7 +1,7 @@
// NOTE: renderdeep doesn't work due to Google Maps API integration
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { fakeIntl } from '../../util/test-data';
import { EditListingPoliciesFormComponent } from './EditListingPoliciesForm';
const noop = () => null;
@ -10,7 +10,6 @@ describe('EditListingPoliciesForm', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<EditListingPoliciesFormComponent
{...fakeFormProps}
publicData={{}}
intl={fakeIntl}
dispatch={noop}

View file

@ -1,27 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditListingPoliciesForm matches snapshot 1`] = `
<Form
className=""
contentRef={null}
<ReactFinalForm
dispatch={[Function]}
intl={
Object {
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"now": [Function],
}
}
onSubmit={[Function]}
>
<TextInputField
id="fakeTestForm.rules"
label="EditListingPoliciesForm.rulesLabel"
name="rules"
placeholder="EditListingPoliciesForm.rulesPlaceholder"
type="textarea"
/>
<Button
className={null}
disabled={false}
inProgress={false}
ready={false}
rootClassName={null}
type="submit"
>
Save rules
</Button>
</Form>
publicData={Object {}}
render={[Function]}
saveActionMsg="Save rules"
selectedPlace={null}
updateError={null}
updateInProgress={false}
updated={false}
/>
`;

View file

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
@ -9,96 +9,100 @@ import { propTypes } from '../../util/types';
import * as validators from '../../util/validators';
import { formatMoney } from '../../util/currency';
import { types as sdkTypes } from '../../util/sdkLoader';
import { Form, Button, FieldCurrencyInput } from '../../components';
import { Button, Form, FieldCurrencyInput } from '../../components';
import css from './EditListingPricingForm.css';
const { Money } = sdkTypes;
export const EditListingPricingFormComponent = props => {
const {
className,
disabled,
handleSubmit,
intl,
invalid,
saveActionMsg,
submitting,
updated,
updateError,
updateInProgress,
} = props;
export const EditListingPricingFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
className,
disabled,
handleSubmit,
intl,
invalid,
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
} = fieldRenderProps;
const pricePerUnitMessage = intl.formatMessage({ id: 'EditListingPricingForm.pricePerUnit' });
const pricePlaceholderMessage = intl.formatMessage({
id: 'EditListingPricingForm.priceInputPlaceholder',
});
const pricePerUnitMessage = intl.formatMessage({
id: 'EditListingPricingForm.pricePerUnit',
});
const pricePlaceholderMessage = intl.formatMessage({
id: 'EditListingPricingForm.priceInputPlaceholder',
});
const priceRequired = validators.required(
intl.formatMessage({
id: 'EditListingPricingForm.priceRequired',
})
);
const minPrice = new Money(config.listingMinimumPriceSubUnits, config.currency);
const minPriceRequired = validators.moneySubUnitAmountAtLeast(
intl.formatMessage(
{
id: 'EditListingPricingForm.priceTooLow',
},
{
minPrice: formatMoney(intl, minPrice),
}
),
config.listingMinimumPriceSubUnits
);
const priceValidators = config.listingMinimumPriceSubUnits
? [priceRequired, minPriceRequired]
: [priceRequired];
const priceRequired = validators.required(
intl.formatMessage({
id: 'EditListingPricingForm.priceRequired',
})
);
const minPrice = new Money(config.listingMinimumPriceSubUnits, config.currency);
const minPriceRequired = validators.moneySubUnitAmountAtLeast(
intl.formatMessage(
{
id: 'EditListingPricingForm.priceTooLow',
},
{
minPrice: formatMoney(intl, minPrice),
}
),
config.listingMinimumPriceSubUnits
);
const priceValidators = config.listingMinimumPriceSubUnits
? validators.composeValidators(priceRequired, minPriceRequired)
: priceRequired;
const errorMessage = updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPricingForm.updateFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
const classes = classNames(css.root, className);
const submitReady = updated;
const submitInProgress = submitting || updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
return (
<Form onSubmit={handleSubmit} className={classes}>
{updateError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPricingForm.updateFailed" />
</p>
) : null}
<FieldCurrencyInput
id="price"
name="price"
className={css.priceInput}
autoFocus
label={pricePerUnitMessage}
placeholder={pricePlaceholderMessage}
currencyConfig={config.currencyConfig}
validate={priceValidators}
/>
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<FieldCurrencyInput
id="EditListingPricingForm.FieldCurrencyInput"
className={css.priceInput}
autoFocus
name="price"
label={pricePerUnitMessage}
placeholder={pricePlaceholderMessage}
currencyConfig={config.currencyConfig}
validate={priceValidators}
/>
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
};
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
{saveActionMsg}
</Button>
</Form>
);
}}
/>
);
EditListingPricingFormComponent.defaultProps = { updateError: null };
const { bool, func, string } = PropTypes;
EditListingPricingFormComponent.propTypes = {
...formPropTypes,
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
@ -107,6 +111,4 @@ EditListingPricingFormComponent.propTypes = {
updateInProgress: bool.isRequired,
};
const formName = 'EditListingPricingForm';
export default compose(reduxForm({ form: formName }), injectIntl)(EditListingPricingFormComponent);
export default compose(injectIntl)(EditListingPricingFormComponent);

View file

@ -1,6 +1,6 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import { fakeIntl, fakeFormProps } from '../../util/test-data';
import { fakeIntl } from '../../util/test-data';
import EditListingPricingForm from './EditListingPricingForm';
const noop = () => null;
@ -9,7 +9,6 @@ describe('EditListingPricingForm', () => {
it('matches snapshot', () => {
const tree = renderDeep(
<EditListingPricingForm
{...fakeFormProps}
intl={fakeIntl}
dispatch={noop}
onSubmit={v => v}

View file

@ -11,14 +11,14 @@ exports[`EditListingPricingForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="EditListingPricingForm.FieldCurrencyInput"
htmlFor="price"
>
EditListingPricingForm.pricePerUnit
</label>
<input
autoFocus={true}
className=""
id="EditListingPricingForm.FieldCurrencyInput"
id="price"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}

View file

@ -15,7 +15,7 @@ import { propTypes } from '../../util/types';
import css from './EmailVerificationForm.css';
const EmailVerificationFormComponent = props => {
const { currentUser, submitting, inProgress, handleSubmit, verificationError } = props;
const { currentUser, inProgress, handleSubmit, verificationError } = props;
const { email, emailVerified, pendingEmail, profile } = currentUser.attributes;
const emailToVerify = <strong>{pendingEmail || email}</strong>;
@ -27,7 +27,7 @@ const EmailVerificationFormComponent = props => {
</div>
);
const submitInProgress = submitting || inProgress;
const submitInProgress = inProgress;
const submitDisabled = submitInProgress;
const verifyEmail = (
@ -53,7 +53,7 @@ const EmailVerificationFormComponent = props => {
<div className={css.bottomWrapper}>
<PrimaryButton type="submit" inProgress={submitInProgress} disabled={submitDisabled}>
{submitting || inProgress ? (
{inProgress ? (
<FormattedMessage id="EmailVerificationForm.verifying" />
) : (
<FormattedMessage id="EmailVerificationForm.verify" />

View file

@ -3,6 +3,7 @@ import EnquiryForm from './EnquiryForm';
export const Empty = {
component: EnquiryForm,
props: {
formId: 'EnquiryFormExample',
listingTitle: 'Sauna with a view',
authorDisplayName: 'Janne',
onSubmit(values) {

View file

@ -2,78 +2,82 @@ import React from 'react';
import { string, bool } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import classNames from 'classnames';
import { Form, PrimaryButton, TextInputField, IconEnquiry } from '../../components';
import { Form, PrimaryButton, FieldTextInput, IconEnquiry } from '../../components';
import * as validators from '../../util/validators';
import { propTypes } from '../../util/types';
import css from './EnquiryForm.css';
const EnquiryFormComponent = props => {
const {
rootClassName,
className,
submitButtonWrapperClassName,
form,
handleSubmit,
submitting,
inProgress,
intl,
listingTitle,
authorDisplayName,
sendEnquiryError,
} = props;
const EnquiryFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
rootClassName,
className,
submitButtonWrapperClassName,
formId,
handleSubmit,
inProgress,
intl,
listingTitle,
authorDisplayName,
sendEnquiryError,
} = fieldRenderProps;
const messageLabel = intl.formatMessage(
{
id: 'EnquiryForm.messageLabel',
},
{ authorDisplayName }
);
const messagePlaceholder = intl.formatMessage(
{
id: 'EnquiryForm.messagePlaceholder',
},
{ authorDisplayName }
);
const messageRequiredMessage = intl.formatMessage({
id: 'EnquiryForm.messageRequired',
});
const messageRequired = validators.requiredAndNonEmptyString(messageRequiredMessage);
const messageLabel = intl.formatMessage(
{
id: 'EnquiryForm.messageLabel',
},
{ authorDisplayName }
);
const messagePlaceholder = intl.formatMessage(
{
id: 'EnquiryForm.messagePlaceholder',
},
{ authorDisplayName }
);
const messageRequiredMessage = intl.formatMessage({
id: 'EnquiryForm.messageRequired',
});
const messageRequired = validators.requiredAndNonEmptyString(messageRequiredMessage);
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = submitting || inProgress;
const submitDisabled = submitInProgress;
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = inProgress;
const submitDisabled = submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
<IconEnquiry className={css.icon} />
<h2 className={css.heading}>
<FormattedMessage id="EnquiryForm.heading" values={{ listingTitle }} />
</h2>
<TextInputField
className={css.field}
type="textarea"
name="message"
id={`${form}.message`}
label={messageLabel}
placeholder={messagePlaceholder}
validate={[messageRequired]}
/>
<div className={submitButtonWrapperClassName}>
{sendEnquiryError ? (
<p className={css.error}>
<FormattedMessage id="EnquiryForm.sendEnquiryError" />
</p>
) : null}
<PrimaryButton type="submit" inProgress={submitInProgress} disabled={submitDisabled}>
<FormattedMessage id="EnquiryForm.submitButtonText" />
</PrimaryButton>
</div>
</Form>
);
};
return (
<Form className={classes} onSubmit={handleSubmit}>
<IconEnquiry className={css.icon} />
<h2 className={css.heading}>
<FormattedMessage id="EnquiryForm.heading" values={{ listingTitle }} />
</h2>
<FieldTextInput
className={css.field}
type="textarea"
name="message"
id={formId ? `${formId}.message` : 'message'}
label={messageLabel}
placeholder={messagePlaceholder}
validate={messageRequired}
/>
<div className={submitButtonWrapperClassName}>
{sendEnquiryError ? (
<p className={css.error}>
<FormattedMessage id="EnquiryForm.sendEnquiryError" />
</p>
) : null}
<PrimaryButton type="submit" inProgress={submitInProgress} disabled={submitDisabled}>
<FormattedMessage id="EnquiryForm.submitButtonText" />
</PrimaryButton>
</div>
</Form>
);
}}
/>
);
EnquiryFormComponent.defaultProps = {
rootClassName: null,
@ -84,7 +88,6 @@ EnquiryFormComponent.defaultProps = {
};
EnquiryFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
submitButtonWrapperClassName: string,
@ -99,8 +102,8 @@ EnquiryFormComponent.propTypes = {
intl: intlShape.isRequired,
};
const defaultFormName = 'EnquiryForm';
const EnquiryForm = compose(injectIntl)(EnquiryFormComponent);
const EnquiryForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(EnquiryFormComponent);
EnquiryForm.displayName = 'EnquiryForm';
export default EnquiryForm;

View file

@ -4,6 +4,7 @@ import LoginForm from './LoginForm';
export const Empty = {
component: LoginForm,
props: {
formId: 'LoginFormExample',
onSubmit(values) {
console.log('log in with form values:', values);
},

View file

@ -2,123 +2,130 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import classNames from 'classnames';
import { Form, PrimaryButton, TextInputField, NamedLink } from '../../components';
import { Form, PrimaryButton, FieldTextInput, NamedLink } from '../../components';
import * as validators from '../../util/validators';
import css from './LoginForm.css';
const LoginFormComponent = props => {
const {
rootClassName,
className,
form,
handleSubmit,
submitting,
inProgress,
intl,
invalid,
} = props;
const LoginFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
rootClassName,
className,
formId,
handleSubmit,
inProgress,
intl,
invalid,
} = fieldRenderProps;
// email
const emailLabel = intl.formatMessage({
id: 'LoginForm.emailLabel',
});
const emailPlaceholder = intl.formatMessage({
id: 'LoginForm.emailPlaceholder',
});
const emailRequiredMessage = intl.formatMessage({
id: 'LoginForm.emailRequired',
});
const emailRequired = validators.required(emailRequiredMessage);
const emailInvalidMessage = intl.formatMessage({
id: 'LoginForm.emailInvalid',
});
const emailValid = validators.emailFormatValid(emailInvalidMessage);
// email
const emailLabel = intl.formatMessage({
id: 'LoginForm.emailLabel',
});
const emailPlaceholder = intl.formatMessage({
id: 'LoginForm.emailPlaceholder',
});
const emailRequiredMessage = intl.formatMessage({
id: 'LoginForm.emailRequired',
});
const emailRequired = validators.required(emailRequiredMessage);
const emailInvalidMessage = intl.formatMessage({
id: 'LoginForm.emailInvalid',
});
const emailValid = validators.emailFormatValid(emailInvalidMessage);
// password
const passwordLabel = intl.formatMessage({
id: 'LoginForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'LoginForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'LoginForm.passwordRequired',
});
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
// password
const passwordLabel = intl.formatMessage({
id: 'LoginForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'LoginForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'LoginForm.passwordRequired',
});
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = submitting || inProgress;
const submitDisabled = invalid || submitInProgress;
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = inProgress;
const submitDisabled = invalid || submitInProgress;
const passwordRecoveryLink = (
<NamedLink name="PasswordRecoveryPage" className={css.recoveryLink}>
<FormattedMessage id="LoginForm.forgotPassword" />
</NamedLink>
);
const passwordRecoveryLink = (
<NamedLink name="PasswordRecoveryPage" className={css.recoveryLink}>
<FormattedMessage id="LoginForm.forgotPassword" />
</NamedLink>
);
return (
<Form className={classes} onSubmit={handleSubmit}>
<div>
<TextInputField
type="email"
name="email"
autoComplete="email"
id={`${form}.email`}
label={emailLabel}
placeholder={emailPlaceholder}
validate={[emailRequired, emailValid]}
/>
<TextInputField
className={css.password}
type="password"
name="password"
autoComplete="current-password"
id={`${form}.password`}
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordRequired}
/>
</div>
<div className={css.bottomWrapper}>
<p className={css.bottomWrapperText}>
<span className={css.recoveryLinkInfo}>
<FormattedMessage id="LoginForm.forgotPasswordInfo" values={{ passwordRecoveryLink }} />
</span>
</p>
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
>
<FormattedMessage id="LoginForm.logIn" />
</PrimaryButton>
</div>
</Form>
);
};
return (
<Form className={classes} onSubmit={handleSubmit}>
<div>
<FieldTextInput
type="email"
id={formId ? `${formId}.email` : 'email'}
name="email"
autoComplete="email"
label={emailLabel}
placeholder={emailPlaceholder}
validate={validators.composeValidators(emailRequired, emailValid)}
/>
<FieldTextInput
className={css.password}
type="password"
id={formId ? `${formId}.password` : 'password'}
name="password"
autoComplete="current-password"
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordRequired}
/>
</div>
<div className={css.bottomWrapper}>
<p className={css.bottomWrapperText}>
<span className={css.recoveryLinkInfo}>
<FormattedMessage
id="LoginForm.forgotPasswordInfo"
values={{ passwordRecoveryLink }}
/>
</span>
</p>
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
>
<FormattedMessage id="LoginForm.logIn" />
</PrimaryButton>
</div>
</Form>
);
}}
/>
);
LoginFormComponent.defaultProps = {
rootClassName: null,
className: null,
form: null,
inProgress: false,
};
const { string, bool } = PropTypes;
LoginFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
form: string,
inProgress: bool,
intl: intlShape.isRequired,
};
const defaultFormName = 'LoginForm';
const LoginForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(LoginFormComponent);
const LoginForm = compose(injectIntl)(LoginFormComponent);
LoginForm.displayName = 'LoginForm';
export default LoginForm;

View file

@ -1,10 +1,13 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import { fakeIntl } from '../../util/test-data';
import LoginForm from './LoginForm';
const noop = () => null;
describe('LoginForm', () => {
it('matches snapshot', () => {
const tree = renderDeep(<LoginForm />);
const tree = renderDeep(<LoginForm intl={fakeIntl} onSubmit={noop} />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -12,19 +12,17 @@ exports[`LoginForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="LoginForm.email"
htmlFor="email"
>
LoginForm.emailLabel
</label>
<input
autoComplete="email"
className=""
id="LoginForm.email"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="LoginForm.emailPlaceholder"
type="email"
@ -35,19 +33,17 @@ exports[`LoginForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="LoginForm.password"
htmlFor="password"
>
LoginForm.passwordLabel
</label>
<input
autoComplete="current-password"
className=""
id="LoginForm.password"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="LoginForm.passwordPlaceholder"
type="password"

View file

@ -2,13 +2,14 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
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';
import { ensureCurrentUser } from '../../util/data';
import { isChangePasswordWrongPassword } from '../../util/errors';
import { Form, PrimaryButton, TextInputField } from '../../components';
import { Form, PrimaryButton, FieldTextInput } from '../../components';
import css from './PasswordChangeForm.css';
@ -18,163 +19,183 @@ class PasswordChangeFormComponent extends Component {
constructor(props) {
super(props);
this.resetTimeoutId = null;
this.submittedValues = {};
}
componentWillUnmount() {
window.clearTimeout(this.resetTimeoutId);
}
render() {
const {
rootClassName,
className,
changePasswordError,
currentUser,
form,
handleSubmit,
submitting,
inProgress,
intl,
invalid,
pristine,
ready,
reset,
} = this.props;
const user = ensureCurrentUser(currentUser);
if (!user.id) {
return null;
}
// New password
const newPasswordLabel = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordLabel',
});
const newPasswordPlaceholder = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordPlaceholder',
});
const newPasswordRequiredMessage = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordRequired',
});
const newPasswordRequired = validators.requiredStringNoTrim(newPasswordRequiredMessage);
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'PasswordChangeForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMaxLengthMessage = intl.formatMessage(
{
id: 'PasswordChangeForm.passwordTooLong',
},
{
maxLength: validators.PASSWORD_MAX_LENGTH,
}
);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordMaxLength = validators.maxLength(
passwordMaxLengthMessage,
validators.PASSWORD_MAX_LENGTH
);
// password
const passwordLabel = intl.formatMessage({
id: 'PasswordChangeForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'PasswordChangeForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'PasswordChangeForm.passwordRequired',
});
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordFailedMessage = intl.formatMessage({
id: 'PasswordChangeForm.passwordFailed',
});
const passwordErrorText = isChangePasswordWrongPassword(changePasswordError)
? passwordFailedMessage
: null;
const confirmClasses = classNames(css.confirmChangesSection, {
[css.confirmChangesSectionVisible]: !pristine,
});
const genericFailure =
changePasswordError && !passwordErrorText ? (
<span className={css.error}>
<FormattedMessage id="PasswordChangeForm.genericFailure" />
</span>
) : null;
const classes = classNames(rootClassName || css.root, className);
const submitDisabled = invalid || submitting || inProgress;
return (
<Form
className={classes}
onSubmit={values => {
handleSubmit(values)
.then(() => {
this.resetTimeoutId = window.setTimeout(reset, RESET_TIMEOUT);
})
.catch(() => {
// Error is handled in duck file already.
});
<FinalForm
{...this.props}
render={fieldRenderProps => {
const {
rootClassName,
className,
formId,
changePasswordError,
currentUser,
handleSubmit,
inProgress,
intl,
invalid,
pristine,
ready,
form,
values,
} = fieldRenderProps;
const user = ensureCurrentUser(currentUser);
if (!user.id) {
return null;
}
// New password
const newPasswordLabel = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordLabel',
});
const newPasswordPlaceholder = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordPlaceholder',
});
const newPasswordRequiredMessage = intl.formatMessage({
id: 'PasswordChangeForm.newPasswordRequired',
});
const newPasswordRequired = validators.requiredStringNoTrim(newPasswordRequiredMessage);
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'PasswordChangeForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMaxLengthMessage = intl.formatMessage(
{
id: 'PasswordChangeForm.passwordTooLong',
},
{
maxLength: validators.PASSWORD_MAX_LENGTH,
}
);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordMaxLength = validators.maxLength(
passwordMaxLengthMessage,
validators.PASSWORD_MAX_LENGTH
);
// password
const passwordLabel = intl.formatMessage({
id: 'PasswordChangeForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'PasswordChangeForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'PasswordChangeForm.passwordRequired',
});
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordFailedMessage = intl.formatMessage({
id: 'PasswordChangeForm.passwordFailed',
});
const passwordTouched = this.submittedValues.currentPassword !== values.currentPassword;
const passwordErrorText = isChangePasswordWrongPassword(changePasswordError)
? passwordFailedMessage
: null;
const confirmClasses = classNames(css.confirmChangesSection, {
[css.confirmChangesSectionVisible]: !pristine,
});
const genericFailure =
changePasswordError && !passwordErrorText ? (
<span className={css.error}>
<FormattedMessage id="PasswordChangeForm.genericFailure" />
</span>
) : 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 || pristineSinceLastSubmit || inProgress;
return (
<Form
className={classes}
onSubmit={e => {
this.submittedValues = values;
handleSubmit(e)
.then(() => {
this.resetTimeoutId = window.setTimeout(form.reset, RESET_TIMEOUT);
})
.catch(() => {
// Error is handled in duck file already.
});
}}
>
<div className={css.newPasswordSection}>
<FieldTextInput
type="password"
id={formId ? `${formId}.newPassword` : 'newPassword'}
name="newPassword"
autoComplete="new-password"
label={newPasswordLabel}
placeholder={newPasswordPlaceholder}
validate={validators.composeValidators(
newPasswordRequired,
passwordMinLength,
passwordMaxLength
)}
/>
</div>
<div className={confirmClasses}>
<h3 className={css.confirmChangesTitle}>
<FormattedMessage id="PasswordChangeForm.confirmChangesTitle" />
</h3>
<p className={css.confirmChangesInfo}>
<FormattedMessage id="PasswordChangeForm.confirmChangesInfo" />
</p>
<FieldTextInput
className={css.password}
type="password"
id="currentPassword"
name="currentPassword"
autoComplete="current-password"
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={validators.composeValidators(
passwordRequired,
passwordMinLength,
passwordMaxLength
)}
customErrorText={passwordTouched ? null : passwordErrorText}
/>
</div>
<div className={css.bottomWrapper}>
{genericFailure}
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={inProgress}
ready={ready}
disabled={submitDisabled}
>
<FormattedMessage id="PasswordChangeForm.saveChanges" />
</PrimaryButton>
</div>
</Form>
);
}}
>
<div className={css.newPasswordSection}>
<TextInputField
type="password"
name="newPassword"
autoComplete="new-password"
id={`${form}.newPassword`}
label={newPasswordLabel}
placeholder={newPasswordPlaceholder}
validate={[newPasswordRequired, passwordMinLength, passwordMaxLength]}
/>
</div>
<div className={confirmClasses}>
<h3 className={css.confirmChangesTitle}>
<FormattedMessage id="PasswordChangeForm.confirmChangesTitle" />
</h3>
<p className={css.confirmChangesInfo}>
<FormattedMessage id="PasswordChangeForm.confirmChangesInfo" />
</p>
<TextInputField
className={css.password}
type="password"
name="currentPassword"
autoComplete="current-password"
id={`${form}.currentPassword`}
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={[passwordRequired, passwordMinLength, passwordMaxLength]}
customErrorText={passwordErrorText}
/>
</div>
<div className={css.bottomWrapper}>
{genericFailure}
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={inProgress}
ready={ready}
disabled={submitDisabled}
>
<FormattedMessage id="PasswordChangeForm.saveChanges" />
</PrimaryButton>
</div>
</Form>
/>
);
}
}
@ -184,24 +205,22 @@ PasswordChangeFormComponent.defaultProps = {
className: null,
changePasswordError: null,
inProgress: false,
formId: null,
};
const { bool, string } = PropTypes;
PasswordChangeFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
changePasswordError: propTypes.error,
inProgress: bool,
intl: intlShape.isRequired,
ready: bool.isRequired,
formId: string,
};
const defaultFormName = 'PasswordChangeForm';
const PasswordChangeForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
PasswordChangeFormComponent
);
const PasswordChangeForm = compose(injectIntl)(PasswordChangeFormComponent);
PasswordChangeForm.displayName = 'PasswordChangeForm';
export default PasswordChangeForm;

View file

@ -1,6 +1,6 @@
import React from 'react';
import { renderShallow } from '../../util/test-helpers';
import { fakeIntl } from '../../util/test-data';
import { fakeIntl, createCurrentUser } from '../../util/test-data';
import { PasswordChangePageComponent } from './PasswordChangePage';
const noop = () => null;
@ -14,6 +14,7 @@ describe('PasswordChangePage', () => {
location={{ search: '' }}
scrollingDisabled={false}
authInProgress={false}
currentUser={createCurrentUser('user1')}
currentUserHasListings={false}
isAuthenticated={false}
onChange={noop}

View file

@ -72,6 +72,33 @@ exports[`PasswordChangePage matches snapshot 1`] = `
values={Object {}}
/>
</h1>
<PasswordChangeForm
changePasswordError={null}
currentUser={
Object {
"attributes": Object {
"banned": false,
"email": "user1@example.com",
"emailVerified": true,
"profile": Object {
"abbreviatedName": "user1 abbreviated name",
"displayName": "user1 display name",
"firstName": "user1 first name",
"lastName": "user1 last name",
},
"stripeConnected": true,
},
"id": UUID {
"uuid": "user1",
},
"type": "currentUser",
}
}
inProgress={false}
onChange={[Function]}
onSubmit={[Function]}
ready={false}
/>
</div>
</LayoutWrapperMain>
<LayoutWrapperFooter

View file

@ -4,6 +4,7 @@ import PasswordRecoveryForm from './PasswordRecoveryForm';
export const Empty = {
component: PasswordRecoveryForm,
props: {
formId: 'PasswordRecoveryFormExample',
onSubmit(values) {
console.log('submit forgotten password email:', values);
},

View file

@ -1,99 +1,132 @@
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 { reduxForm, propTypes as formPropTypes } from 'redux-form';
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';
import { isPasswordRecoveryEmailNotFoundError } from '../../util/errors';
import { Form, PrimaryButton, TextInputField, NamedLink } from '../../components';
import { Form, PrimaryButton, FieldTextInput, NamedLink } from '../../components';
import css from './PasswordRecoveryForm.css';
const PasswordRecoveryFormComponent = props => {
const {
rootClassName,
className,
handleSubmit,
pristine,
submitting,
form,
initialValues,
intl,
inProgress,
recoveryError,
} = props;
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 (
<FinalForm
{...this.props}
render={fieldRenderProps => {
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 = (
<NamedLink name="LoginPage" className={css.modalHelperLink}>
<FormattedMessage id="PasswordRecoveryForm.loginLinkText" />
</NamedLink>
);
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 (
<Form className={classes} onSubmit={handleSubmit}>
<TextInputField
className={css.email}
type="email"
name="email"
autoComplete="email"
id={`${form}.email`}
label={emailLabel}
placeholder={emailPlaceholder}
validate={[emailRequired, emailValid]}
customErrorText={customErrorText}
const loginLink = (
<NamedLink name="LoginPage" className={css.modalHelperLink}>
<FormattedMessage id="PasswordRecoveryForm.loginLinkText" />
</NamedLink>
);
return (
<Form
className={classes}
onSubmit={e => {
this.submittedValues = values;
handleSubmit(e);
}}
>
<FieldTextInput
className={css.email}
type="email"
id={formId ? `${formId}.email` : 'email'}
name="email"
autoComplete="email"
label={emailLabel}
placeholder={emailPlaceholder}
validate={validators.composeValidators(emailRequired, emailValid)}
customErrorText={emailTouched ? null : customErrorText}
/>
<div className={css.bottomWrapper}>
<p className={css.bottomWrapperText}>
<span className={css.modalHelperText}>
<FormattedMessage
id="PasswordRecoveryForm.loginLinkInfo"
values={{ loginLink }}
/>
</span>
</p>
<PrimaryButton
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
>
<FormattedMessage id="PasswordRecoveryForm.sendInstructions" />
</PrimaryButton>
</div>
</Form>
);
}}
/>
<div className={css.bottomWrapper}>
<p className={css.bottomWrapperText}>
<span className={css.modalHelperText}>
<FormattedMessage id="PasswordRecoveryForm.loginLinkInfo" values={{ loginLink }} />
</span>
</p>
<PrimaryButton type="submit" inProgress={submitInProgress} disabled={submitDisabled}>
<FormattedMessage id="PasswordRecoveryForm.sendInstructions" />
</PrimaryButton>
</div>
</Form>
);
};
);
}
}
PasswordRecoveryFormComponent.defaultProps = {
rootClassName: null,
className: null,
formId: null,
inProgress: false,
recoveryError: null,
};
@ -101,9 +134,9 @@ PasswordRecoveryFormComponent.defaultProps = {
const { bool, string } = PropTypes;
PasswordRecoveryFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
formId: string,
inProgress: bool,
recoveryError: propTypes.error,
@ -112,10 +145,7 @@ PasswordRecoveryFormComponent.propTypes = {
intl: intlShape.isRequired,
};
const defaultFormName = 'PasswordRecoveryForm';
const PasswordRecoveryForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
PasswordRecoveryFormComponent
);
const PasswordRecoveryForm = compose(injectIntl)(PasswordRecoveryFormComponent);
PasswordRecoveryForm.displayName = 'PasswordRecoveryForm';
export default PasswordRecoveryForm;

View file

@ -36,7 +36,7 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
values={Object {}}
/>
</p>
<ReduxForm
<PasswordRecoveryForm
inProgress={false}
initialValues={
Object {

View file

@ -4,6 +4,7 @@ import PasswordResetForm from './PasswordResetForm';
export const Empty = {
component: PasswordResetForm,
props: {
formId: 'PasswordResetFormExample',
onSubmit(values) {
console.log('submit with values:', values);
},

View file

@ -2,105 +2,111 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import classNames from 'classnames';
import { Form, PrimaryButton, TextInputField } from '../../components';
import { Form, PrimaryButton, FieldTextInput } from '../../components';
import * as validators from '../../util/validators';
import css from './PasswordResetForm.css';
const PasswordResetFormComponent = props => {
const {
rootClassName,
className,
form,
handleSubmit,
submitting,
inProgress,
intl,
invalid,
} = props;
const PasswordResetFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
rootClassName,
className,
formId,
handleSubmit,
inProgress,
intl,
invalid,
} = fieldRenderProps;
// password
const passwordLabel = intl.formatMessage({
id: 'PasswordResetForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'PasswordResetForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'PasswordResetForm.passwordRequired',
});
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'PasswordResetForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMaxLengthMessage = intl.formatMessage(
{
id: 'PasswordResetForm.passwordTooLong',
},
{
maxLength: validators.PASSWORD_MAX_LENGTH,
}
);
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordMaxLength = validators.maxLength(
passwordMaxLengthMessage,
validators.PASSWORD_MAX_LENGTH
);
// password
const passwordLabel = intl.formatMessage({
id: 'PasswordResetForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'PasswordResetForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'PasswordResetForm.passwordRequired',
});
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'PasswordResetForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMaxLengthMessage = intl.formatMessage(
{
id: 'PasswordResetForm.passwordTooLong',
},
{
maxLength: validators.PASSWORD_MAX_LENGTH,
}
);
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordMaxLength = validators.maxLength(
passwordMaxLengthMessage,
validators.PASSWORD_MAX_LENGTH
);
const classes = classNames(rootClassName || css.root, className);
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = submitting || inProgress;
const submitDisabled = invalid || submitInProgress;
const submitInProgress = inProgress;
const submitDisabled = invalid || submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
<TextInputField
className={css.password}
type="password"
name="password"
autoComplete="new-password"
id={`${form}.password`}
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={[passwordRequired, passwordMinLength, passwordMaxLength]}
/>
<PrimaryButton type="submit" inProgress={submitInProgress} disabled={submitDisabled}>
<FormattedMessage id="PasswordResetForm.submitButtonText" />
</PrimaryButton>
</Form>
);
};
return (
<Form className={classes} onSubmit={handleSubmit}>
<FieldTextInput
className={css.password}
type="password"
id={formId ? `${formId}.password` : 'password'}
name="password"
autoComplete="new-password"
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={validators.composeValidators(
passwordRequired,
passwordMinLength,
passwordMaxLength
)}
/>
<PrimaryButton type="submit" inProgress={submitInProgress} disabled={submitDisabled}>
<FormattedMessage id="PasswordResetForm.submitButtonText" />
</PrimaryButton>
</Form>
);
}}
/>
);
PasswordResetFormComponent.defaultProps = {
rootClassName: null,
className: null,
inProgress: false,
formId: null,
};
const { string, bool } = PropTypes;
PasswordResetFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
inProgress: bool,
intl: intlShape.isRequired,
formId: string,
};
const defaultFormName = 'PasswordResetForm';
const PasswordResetForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
PasswordResetFormComponent
);
const PasswordResetForm = compose(injectIntl)(PasswordResetFormComponent);
PasswordResetForm.displayName = 'PasswordResetForm';
export default PasswordResetForm;

View file

@ -1,18 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, formValueSelector, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import classNames from 'classnames';
import config from '../../config';
import {
Form,
Button,
StripeBankAccountTokenInputField,
SelectField,
FieldSelect,
FieldBirthdayInput,
TextInputField,
FieldTextInput,
Form,
} from '../../components';
import * as validators from '../../util/validators';
import { isStripeInvalidPostalCode } from '../../util/errors';
@ -42,266 +41,278 @@ const countryCurrency = countryCode => {
return country.currency;
};
const PayoutDetailsFormComponent = props => {
const {
className,
country,
createStripeAccountError,
form,
disabled,
inProgress,
ready,
submitButtonText,
handleSubmit,
pristine,
submitting,
invalid,
intl,
} = props;
const PayoutDetailsFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
className,
createStripeAccountError,
change,
disabled,
handleSubmit,
inProgress,
intl,
invalid,
pristine,
ready,
submitButtonText,
values,
} = fieldRenderProps;
const { country } = values;
const firstNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.firstNameLabel' });
const firstNamePlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.firstNamePlaceholder' });
const firstNameRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.firstNameRequired',
})
);
const firstNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.firstNameLabel' });
const firstNamePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.firstNamePlaceholder',
});
const firstNameRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.firstNameRequired',
})
);
const lastNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.lastNameLabel' });
const lastNamePlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.lastNamePlaceholder' });
const lastNameRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.lastNameRequired',
})
);
const lastNameLabel = intl.formatMessage({ id: 'PayoutDetailsForm.lastNameLabel' });
const lastNamePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.lastNamePlaceholder',
});
const lastNameRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.lastNameRequired',
})
);
const birthdayId = `${form}.birthday`;
const birthdayLabel = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabel' });
const birthdayLabelMonth = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabelMonth' });
const birthdayLabelYear = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabelYear' });
const birthdayRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.birthdayRequired',
})
);
const birthdayMinAge = validators.ageAtLeast(
intl.formatMessage(
{
id: 'PayoutDetailsForm.birthdayMinAge',
},
{
minAge: MIN_STRIPE_ACCOUNT_AGE,
const birthdayLabel = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabel' });
const birthdayLabelMonth = intl.formatMessage({
id: 'PayoutDetailsForm.birthdayLabelMonth',
});
const birthdayLabelYear = intl.formatMessage({ id: 'PayoutDetailsForm.birthdayLabelYear' });
const birthdayRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.birthdayRequired',
})
);
const birthdayMinAge = validators.ageAtLeast(
intl.formatMessage(
{
id: 'PayoutDetailsForm.birthdayMinAge',
},
{
minAge: MIN_STRIPE_ACCOUNT_AGE,
}
),
MIN_STRIPE_ACCOUNT_AGE
);
const countryLabel = intl.formatMessage({ id: 'PayoutDetailsForm.countryLabel' });
const countryPlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.countryPlaceholder',
});
const countryRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.countryRequired',
})
);
const streetAddressLabel = intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressLabel',
});
const streetAddressPlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressPlaceholder',
});
const streetAddressRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressRequired',
})
);
const postalCodeLabel = intl.formatMessage({ id: 'PayoutDetailsForm.postalCodeLabel' });
const postalCodePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.postalCodePlaceholder',
});
const postalCodeRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.postalCodeRequired',
})
);
const cityLabel = intl.formatMessage({ id: 'PayoutDetailsForm.cityLabel' });
const cityPlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.cityPlaceholder' });
const cityRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.cityRequired',
})
);
const showAddressFields = country && requiresAddress(country);
// StripeBankAccountTokenInputField handles the error messages
// internally, we just have to make sure we require a valid token
// out of the field. Therefore the empty validation message.
const bankAccountRequired = validators.required(' ');
const classes = classNames(css.root, className, {
[css.disabled]: disabled,
});
const submitInProgress = inProgress;
const submitDisabled = pristine || invalid || disabled || submitInProgress;
let error = null;
if (isStripeInvalidPostalCode(createStripeAccountError)) {
error = (
<div className={css.error}>
<FormattedMessage id="PayoutDetailsForm.createStripeAccountFailedInvalidPostalCode" />
</div>
);
} else if (createStripeAccountError) {
error = (
<div className={css.error}>
<FormattedMessage id="PayoutDetailsForm.createStripeAccountFailed" />
</div>
);
}
),
MIN_STRIPE_ACCOUNT_AGE
);
const countryLabel = intl.formatMessage({ id: 'PayoutDetailsForm.countryLabel' });
const countryPlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.countryPlaceholder' });
const countryRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.countryRequired',
})
);
return (
<Form className={classes} onSubmit={handleSubmit}>
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.personalDetailsTitle" />
</h3>
<div className={css.formRow}>
<FieldTextInput
id="fname"
name="fname"
disabled={disabled}
className={css.firstName}
type="text"
autoComplete="given-name"
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<FieldTextInput
id="lname"
name="lname"
disabled={disabled}
className={css.lastName}
type="text"
autoComplete="family-name"
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
<FieldBirthdayInput
id="birthDate"
name="birthDate"
disabled={disabled}
className={css.field}
label={birthdayLabel}
labelForMonth={birthdayLabelMonth}
labelForYear={birthdayLabelYear}
format={null}
valueFromForm={values.birthDate}
validate={validators.composeValidators(birthdayRequired, birthdayMinAge)}
/>
</div>
const streetAddressLabel = intl.formatMessage({ id: 'PayoutDetailsForm.streetAddressLabel' });
const streetAddressPlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressPlaceholder',
});
const streetAddressRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.streetAddressRequired',
})
);
const postalCodeLabel = intl.formatMessage({ id: 'PayoutDetailsForm.postalCodeLabel' });
const postalCodePlaceholder = intl.formatMessage({
id: 'PayoutDetailsForm.postalCodePlaceholder',
});
const postalCodeRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.postalCodeRequired',
})
);
const cityLabel = intl.formatMessage({ id: 'PayoutDetailsForm.cityLabel' });
const cityPlaceholder = intl.formatMessage({ id: 'PayoutDetailsForm.cityPlaceholder' });
const cityRequired = validators.required(
intl.formatMessage({
id: 'PayoutDetailsForm.cityRequired',
})
);
const showAddressFields = country && requiresAddress(country);
const addressSection = showAddressFields ? (
<div>
<TextInputField
disabled={disabled}
className={css.field}
type="text"
name="streetAddress"
autoComplete="street-address"
id={`${form}.streetAddress`}
label={streetAddressLabel}
placeholder={streetAddressPlaceholder}
validate={streetAddressRequired}
clearOnUnmount
/>
<div className={css.formRow}>
<TextInputField
disabled={disabled}
className={css.postalCode}
type="text"
name="postalCode"
autoComplete="postal-code"
id={`${form}.postalCode`}
label={postalCodeLabel}
placeholder={postalCodePlaceholder}
validate={postalCodeRequired}
clearOnUnmount
/>
<TextInputField
disabled={disabled}
className={css.city}
type="text"
name="city"
autoComplete="address-level2"
id={`${form}.city`}
label={cityLabel}
placeholder={cityPlaceholder}
validate={cityRequired}
clearOnUnmount
/>
</div>
</div>
) : null;
// StripeBankAccountTokenInputField handles the error messages
// internally, we just have to make sure we require a valid token
// out of the field. Therefore the empty validation message.
const bankAccountRequired = validators.required(' ');
const bankAccountSection = country ? (
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.bankDetails" />
</h3>
<StripeBankAccountTokenInputField
disabled={disabled}
name="bankAccountToken"
formName={form}
country={country}
currency={countryCurrency(country)}
validate={bankAccountRequired}
/>
</div>
) : null;
const classes = classNames(css.root, className, {
[css.disabled]: disabled,
});
const submitInProgress = submitting || inProgress;
const submitDisabled = pristine || invalid || disabled || submitInProgress;
let error = null;
if (isStripeInvalidPostalCode(createStripeAccountError)) {
error = (
<div className={css.error}>
<FormattedMessage id="PayoutDetailsForm.createStripeAccountFailedInvalidPostalCode" />
</div>
);
} else if (createStripeAccountError) {
error = (
<div className={css.error}>
<FormattedMessage id="PayoutDetailsForm.createStripeAccountFailed" />
</div>
);
}
return (
<Form className={classes} onSubmit={handleSubmit}>
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.personalDetailsTitle" />
</h3>
<div className={css.formRow}>
<TextInputField
disabled={disabled}
className={css.firstName}
type="text"
name="fname"
autoComplete="given-name"
id={`${form}.fname`}
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<TextInputField
disabled={disabled}
className={css.lastName}
type="text"
name="lname"
autoComplete="family-name"
id={`${form}.lname`}
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
<FieldBirthdayInput
disabled={disabled}
className={css.field}
id={birthdayId}
name="birthDate"
label={birthdayLabel}
labelForMonth={birthdayLabelMonth}
labelForYear={birthdayLabelYear}
format={null}
validate={[birthdayRequired, birthdayMinAge]}
/>
</div>
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.addressTitle" />
</h3>
<SelectField
disabled={disabled}
className={css.selectCountry}
name="country"
autoComplete="country"
id={`${form}.country`}
label={countryLabel}
validate={countryRequired}
>
<option value="">{countryPlaceholder}</option>
{supportedCountries.map(c => (
<option key={c} value={c}>
{intl.formatMessage({ id: `PayoutDetailsForm.countryNames.${c}` })}
</option>
))}
</SelectField>
{addressSection}
</div>
{bankAccountSection}
{error}
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={ready}
>
{submitButtonText ? (
submitButtonText
) : (
<FormattedMessage id="PayoutDetailsForm.submitButtonText" />
)}
</Button>
</Form>
);
};
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.addressTitle" />
</h3>
<FieldSelect
id="country"
name="country"
disabled={disabled}
className={css.selectCountry}
autoComplete="country"
label={countryLabel}
validate={countryRequired}
>
<option value="">{countryPlaceholder}</option>
{supportedCountries.map(c => (
<option key={c} value={c}>
{intl.formatMessage({ id: `PayoutDetailsForm.countryNames.${c}` })}
</option>
))}
</FieldSelect>
{showAddressFields ? (
<div>
<FieldTextInput
id="streetAddress"
name="streetAddress"
disabled={disabled}
className={css.field}
type="text"
autoComplete="street-address"
label={streetAddressLabel}
placeholder={streetAddressPlaceholder}
validate={streetAddressRequired}
onUnmount={() => change('streetAddress', undefined)}
/>
<div className={css.formRow}>
<FieldTextInput
id="postalCode"
name="postalCode"
disabled={disabled}
className={css.postalCode}
type="text"
autoComplete="postal-code"
label={postalCodeLabel}
placeholder={postalCodePlaceholder}
validate={postalCodeRequired}
onUnmount={() => change('postalCode', undefined)}
/>
<FieldTextInput
id="city"
name="city"
disabled={disabled}
className={css.city}
type="text"
autoComplete="address-level2"
label={cityLabel}
placeholder={cityPlaceholder}
validate={cityRequired}
onUnmount={() => change('city', undefined)}
/>
</div>
</div>
) : null}
</div>
{country ? (
<div className={css.sectionContainer}>
<h3 className={css.subTitle}>
<FormattedMessage id="PayoutDetailsForm.bankDetails" />
</h3>
<StripeBankAccountTokenInputField
disabled={disabled}
name="bankAccountToken"
formName="PayoutDetailsForm"
country={country}
currency={countryCurrency(country)}
validate={bankAccountRequired}
/>
</div>
) : null}
{error}
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={ready}
>
{submitButtonText ? (
submitButtonText
) : (
<FormattedMessage id="PayoutDetailsForm.submitButtonText" />
)}
</Button>
</Form>
);
}}
/>
);
PayoutDetailsFormComponent.defaultProps = {
className: null,
@ -316,7 +327,6 @@ PayoutDetailsFormComponent.defaultProps = {
const { bool, object, string } = PropTypes;
PayoutDetailsFormComponent.propTypes = {
...formPropTypes,
className: string,
createStripeAccountError: object,
disabled: bool,
@ -324,27 +334,10 @@ PayoutDetailsFormComponent.propTypes = {
ready: bool,
submitButtonText: string,
// from mapStateToProps
country: string,
// from injectIntl
intl: intlShape.isRequired,
};
const formName = 'PayoutDetailsForm';
const selector = formValueSelector(formName);
const mapStateToProps = state => {
const country = selector(state, 'country');
return { country };
};
const formOptions = {
form: formName,
};
const PayoutDetailsForm = compose(connect(mapStateToProps), reduxForm(formOptions), injectIntl)(
PayoutDetailsFormComponent
);
const PayoutDetailsForm = compose(injectIntl)(PayoutDetailsFormComponent);
export default PayoutDetailsForm;

View file

@ -168,7 +168,7 @@ exports[`PayoutPreferencesPage matches snapshot with Stripe not connected 1`] =
values={Object {}}
/>
</p>
<Connect(ReduxForm)
<InjectIntl(PayoutDetailsFormComponent)
createStripeAccountError={null}
disabled={false}
inProgress={false}
@ -267,7 +267,7 @@ exports[`PayoutPreferencesPage matches snapshot with details submitted 1`] = `
values={Object {}}
/>
</p>
<Connect(ReduxForm)
<InjectIntl(PayoutDetailsFormComponent)
createStripeAccountError={null}
disabled={true}
inProgress={false}

View file

@ -1,88 +1,28 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bool, string } from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Field, Form as FinalForm } from 'react-final-form';
import { isEqual } from 'lodash';
import classNames from 'classnames';
import { ensureCurrentUser } from '../../util/data';
import { propTypes } from '../../util/types';
import * as validators from '../../util/validators';
import { isUploadProfileImageOverLimitError } from '../../util/errors';
import { Form, Avatar, Button, ImageFromFile, IconSpinner, TextInputField } from '../../components';
import { Form, Avatar, Button, ImageFromFile, IconSpinner, FieldTextInput } from '../../components';
import css from './ProfileSettingsForm.css';
const ACCEPT_IMAGES = 'image/*';
const UPLOAD_CHANGE_DELAY = 2000; // Show spinner so that browser has time to load img srcset
const RenderAvatar = props => {
const { accept, id, input, label, type, disabled, uploadImageError } = props;
const { name, onChange } = input;
let error = null;
if (isUploadProfileImageOverLimitError(uploadImageError)) {
error = (
<div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.imageUploadFailedFileTooLarge" />
</div>
);
} else if (uploadImageError) {
error = (
<div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.imageUploadFailed" />
</div>
);
}
return (
<div className={css.uploadAvatarWrapper}>
<label className={css.label} htmlFor={id}>
{label}
</label>
<input
accept={accept}
className={css.uploadAvatarInput}
disabled={disabled}
id={id}
name={name}
onChange={event => {
const file = event.target.files[0];
if (file != null) {
const tempId = `${file.name}_${Date.now()}`;
onChange({ id: tempId, file });
}
}}
type={type}
/>
{error}
</div>
);
};
RenderAvatar.defaultProps = { uploadImageError: null };
const { bool, func, node, object, shape, string } = PropTypes;
RenderAvatar.propTypes = {
accept: string.isRequired,
disabled: bool.isRequired,
id: string.isRequired,
input: shape({
value: object,
onChange: func.isRequired,
name: string.isRequired,
}).isRequired,
label: node.isRequired,
type: string.isRequired,
uploadImageError: propTypes.error,
};
class ProfileSettingsFormComponent extends Component {
constructor(props) {
super(props);
this.uploadDelayTimeoutId = null;
this.state = { uploadDelay: false };
this.submittedValues = {};
}
componentWillReceiveProps(nextProps) {
@ -101,210 +41,279 @@ class ProfileSettingsFormComponent extends Component {
}
render() {
const {
className,
currentUser,
form,
handleSubmit,
intl,
invalid,
onImageUpload,
pristine,
profileImage,
rootClassName,
submitting,
updateInProgress,
updateProfileReady,
updateProfileError,
uploadImageError,
uploadInProgress,
} = this.props;
const user = ensureCurrentUser(currentUser);
// First name
const firstNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameLabel',
});
const firstNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.firstNamePlaceholder',
});
const firstNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameRequired',
});
const firstNameRequired = validators.required(firstNameRequiredMessage);
// Last name
const lastNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameLabel',
});
const lastNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.lastNamePlaceholder',
});
const lastNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameRequired',
});
const lastNameRequired = validators.required(lastNameRequiredMessage);
// Bio
const bioLabel = intl.formatMessage({
id: 'ProfileSettingsForm.bioLabel',
});
const bioPlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.bioPlaceholder',
});
const uploadingOverlay =
uploadInProgress || this.state.uploadDelay ? (
<div className={css.uploadingImageOverlay}>
<IconSpinner />
</div>
) : null;
const hasUploadError = !!uploadImageError && !uploadInProgress;
const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError });
const transientUserProfileImage = profileImage.uploadedImage || user.profileImage;
const transientUser = { ...user, profileImage: transientUserProfileImage };
// Ensure that file exists if imageFromFile is used
const fileExists = !!profileImage.file;
const fileUploadInProgress = uploadInProgress && fileExists;
const delayAfterUpload = profileImage.imageId && this.state.uploadDelay;
const imageFromFile =
fileExists && (fileUploadInProgress || delayAfterUpload) ? (
<ImageFromFile
id={profileImage.id}
className={errorClasses}
rootClassName={css.uploadingImage}
aspectRatioClassName={css.squareAspectRatio}
file={profileImage.file}
>
{uploadingOverlay}
</ImageFromFile>
) : null;
// Avatar is rendered in hidden during the upload delay
// Upload delay smoothes image change process:
// responsive img has time to load srcset stuff before it is shown to user.
const avatarClasses = classNames(errorClasses, css.avatar, {
[css.avatarInvisible]: this.state.uploadDelay,
});
const avatarComponent =
!fileUploadInProgress && profileImage.imageId ? (
<Avatar
className={avatarClasses}
renderSizes="(max-width: 767px) 96px, 240px"
user={transientUser}
disableProfileLink
/>
) : null;
const chooseAvatarLabel =
profileImage.imageId || fileUploadInProgress ? (
<div className={css.avatarContainer}>
{imageFromFile}
{avatarComponent}
<div className={css.changeAvatar}>
<FormattedMessage id="ProfileSettingsForm.changeAvatar" />
</div>
</div>
) : (
<div className={css.avatarPlaceholder}>
<div className={css.avatarPlaceholderText}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePicture" />
</div>
<div className={css.avatarPlaceholderTextMobile}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePictureMobile" />
</div>
</div>
);
const submitError = updateProfileError ? (
<div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.updateProfileFailed" />
</div>
) : null;
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = submitting || updateInProgress;
const submitReady = updateProfileReady;
const submitDisabled = invalid || pristine || uploadInProgress || submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
<div className={css.sectionContainer}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourProfilePicture" />
</h3>
<Field
accept={ACCEPT_IMAGES}
component={RenderAvatar}
disabled={uploadInProgress}
id="ProfileSettingsForm.changeAvatar"
label={chooseAvatarLabel}
name="profileImage"
onChange={onImageUpload}
type="file"
uploadImageError={uploadImageError}
/>
<div className={css.tip}>
<FormattedMessage id="ProfileSettingsForm.tip" />
</div>
<div className={css.fileInfo}>
<FormattedMessage id="ProfileSettingsForm.fileInfo" />
</div>
</div>
<div className={css.sectionContainer}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourName" />
</h3>
<div className={css.nameContainer}>
<TextInputField
className={css.firstName}
type="text"
name="firstName"
id={`${form}.firstName`}
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<TextInputField
className={css.lastName}
type="text"
name="lastName"
id={`${form}.lastName`}
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
</div>
<div className={classNames(css.sectionContainer, css.lastSection)}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.bioHeading" />
</h3>
<TextInputField
type="textarea"
name="bio"
id={`${form}.bio`}
label={bioLabel}
placeholder={bioPlaceholder}
/>
<p className={css.bioInfo}>
<FormattedMessage id="ProfileSettingsForm.bioInfo" />
</p>
</div>
{submitError}
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={submitReady}
>
<FormattedMessage id="ProfileSettingsForm.saveChanges" />
</Button>
</Form>
<FinalForm
{...this.props}
render={fieldRenderProps => {
const {
className,
currentUser,
handleSubmit,
intl,
invalid,
onImageUpload,
pristine,
profileImage,
rootClassName,
updateInProgress,
updateProfileError,
uploadImageError,
uploadInProgress,
form,
values,
} = fieldRenderProps;
const user = ensureCurrentUser(currentUser);
// First name
const firstNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameLabel',
});
const firstNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.firstNamePlaceholder',
});
const firstNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameRequired',
});
const firstNameRequired = validators.required(firstNameRequiredMessage);
// Last name
const lastNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameLabel',
});
const lastNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.lastNamePlaceholder',
});
const lastNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameRequired',
});
const lastNameRequired = validators.required(lastNameRequiredMessage);
// Bio
const bioLabel = intl.formatMessage({
id: 'ProfileSettingsForm.bioLabel',
});
const bioPlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.bioPlaceholder',
});
const uploadingOverlay =
uploadInProgress || this.state.uploadDelay ? (
<div className={css.uploadingImageOverlay}>
<IconSpinner />
</div>
) : null;
const hasUploadError = !!uploadImageError && !uploadInProgress;
const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError });
const transientUserProfileImage = profileImage.uploadedImage || user.profileImage;
const transientUser = { ...user, profileImage: transientUserProfileImage };
// Ensure that file exists if imageFromFile is used
const fileExists = !!profileImage.file;
const fileUploadInProgress = uploadInProgress && fileExists;
const delayAfterUpload = profileImage.imageId && this.state.uploadDelay;
const imageFromFile =
fileExists && (fileUploadInProgress || delayAfterUpload) ? (
<ImageFromFile
id={profileImage.id}
className={errorClasses}
rootClassName={css.uploadingImage}
aspectRatioClassName={css.squareAspectRatio}
file={profileImage.file}
>
{uploadingOverlay}
</ImageFromFile>
) : null;
// Avatar is rendered in hidden during the upload delay
// Upload delay smoothes image change process:
// responsive img has time to load srcset stuff before it is shown to user.
const avatarClasses = classNames(errorClasses, css.avatar, {
[css.avatarInvisible]: this.state.uploadDelay,
});
const avatarComponent =
!fileUploadInProgress && profileImage.imageId ? (
<Avatar
className={avatarClasses}
renderSizes="(max-width: 767px) 96px, 240px"
user={transientUser}
disableProfileLink
/>
) : null;
const chooseAvatarLabel =
profileImage.imageId || fileUploadInProgress ? (
<div className={css.avatarContainer}>
{imageFromFile}
{avatarComponent}
<div className={css.changeAvatar}>
<FormattedMessage id="ProfileSettingsForm.changeAvatar" />
</div>
</div>
) : (
<div className={css.avatarPlaceholder}>
<div className={css.avatarPlaceholderText}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePicture" />
</div>
<div className={css.avatarPlaceholderTextMobile}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePictureMobile" />
</div>
</div>
);
const submitError = updateProfileError ? (
<div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.updateProfileFailed" />
</div>
) : null;
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = updateInProgress;
const submittedOnce = Object.keys(this.submittedValues).length > 0;
const pristineSinceLastSubmit = submittedOnce && isEqual(values, this.submittedValues);
const submitDisabled =
invalid || pristine || pristineSinceLastSubmit || uploadInProgress || submitInProgress;
return (
<Form
className={classes}
onSubmit={e => {
this.submittedValues = values;
handleSubmit(e);
}}
>
<div className={css.sectionContainer}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourProfilePicture" />
</h3>
<Field
accept={ACCEPT_IMAGES}
id="profileImage"
name="profileImage"
label={chooseAvatarLabel}
type="file"
form={null}
uploadImageError={uploadImageError}
disabled={uploadInProgress}
>
{fieldProps => {
const {
accept,
id,
input,
label,
type,
disabled,
uploadImageError,
} = fieldProps;
const { name } = input;
const onChange = e => {
const file = e.target.files[0];
form.change(`profileImage`, file);
form.blur(`profileImage`);
if (file != null) {
const tempId = `${file.name}_${Date.now()}`;
onImageUpload({ id: tempId, file });
}
};
let error = null;
if (isUploadProfileImageOverLimitError(uploadImageError)) {
error = (
<div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.imageUploadFailedFileTooLarge" />
</div>
);
} else if (uploadImageError) {
error = (
<div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.imageUploadFailed" />
</div>
);
}
return (
<div className={css.uploadAvatarWrapper}>
<label className={css.label} htmlFor={id}>
{label}
</label>
<input
accept={accept}
id={id}
name={name}
className={css.uploadAvatarInput}
disabled={disabled}
onChange={onChange}
type={type}
/>
{error}
</div>
);
}}
</Field>
<div className={css.tip}>
<FormattedMessage id="ProfileSettingsForm.tip" />
</div>
<div className={css.fileInfo}>
<FormattedMessage id="ProfileSettingsForm.fileInfo" />
</div>
</div>
<div className={css.sectionContainer}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourName" />
</h3>
<div className={css.nameContainer}>
<FieldTextInput
className={css.firstName}
type="text"
id="firstName"
name="firstName"
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<FieldTextInput
className={css.lastName}
type="text"
id="lastName"
name="lastName"
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
</div>
<div className={classNames(css.sectionContainer, css.lastSection)}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.bioHeading" />
</h3>
<FieldTextInput
type="textarea"
id="bio"
name="bio"
label={bioLabel}
placeholder={bioPlaceholder}
/>
<p className={css.bioInfo}>
<FormattedMessage id="ProfileSettingsForm.bioInfo" />
</p>
</div>
{submitError}
<Button
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={pristineSinceLastSubmit}
>
<FormattedMessage id="ProfileSettingsForm.saveChanges" />
</Button>
</Form>
);
}}
/>
);
}
}
@ -318,7 +327,6 @@ ProfileSettingsFormComponent.defaultProps = {
};
ProfileSettingsFormComponent.propTypes = {
...formPropTypes,
rootClassName: string,
className: string,
@ -332,10 +340,8 @@ ProfileSettingsFormComponent.propTypes = {
intl: intlShape.isRequired,
};
const defaultFormName = 'ProfileSettingsForm';
const ProfileSettingsForm = compose(injectIntl)(ProfileSettingsFormComponent);
const ProfileSettingsForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(
ProfileSettingsFormComponent
);
ProfileSettingsForm.displayName = 'ProfileSettingsForm';
export default ProfileSettingsForm;

View file

@ -18,7 +18,7 @@ import {
} from '../../components';
import { ProfileSettingsForm, TopbarContainer } from '../../containers';
import { clearUpdatedForm, updateProfile, uploadImage } from './ProfileSettingsPage.duck';
import { updateProfile, uploadImage } from './ProfileSettingsPage.duck';
import css from './ProfileSettingsPage.css';
const onImageUploadHandler = (values, fn) => {
@ -29,15 +29,10 @@ const onImageUploadHandler = (values, fn) => {
};
export class ProfileSettingsPageComponent extends Component {
constructor(props) {
super(props);
this.state = { profileUpdated: false };
}
render() {
const {
currentUser,
image,
onChange,
onImageUpload,
onUpdateProfile,
scrollingDisabled,
@ -67,14 +62,7 @@ export class ProfileSettingsPageComponent extends Component {
? { ...profile, profileImageId: uploadedImage.imageId }
: profile;
onUpdateProfile(updatedValues).then(() => {
this.setState({ profileUpdated: true });
});
};
const handleChange = () => {
this.setState({ profileUpdated: false });
onChange();
onUpdateProfile(updatedValues);
};
const user = ensureCurrentUser(currentUser);
@ -86,16 +74,14 @@ export class ProfileSettingsPageComponent extends Component {
<ProfileSettingsForm
className={css.form}
currentUser={currentUser}
initialValues={{ firstName, lastName, bio, profileImage }}
initialValues={{ firstName, lastName, bio, profileImage: user.profileImage }}
profileImage={profileImage}
onImageUpload={e => onImageUploadHandler(e, onImageUpload)}
uploadInProgress={uploadInProgress}
updateInProgress={updateInProgress}
updateProfileReady={this.state.profileUpdated}
uploadImageError={uploadImageError}
updateProfileError={updateProfileError}
onSubmit={handleSubmit}
onChange={handleChange}
/>
) : null;
@ -153,7 +139,6 @@ ProfileSettingsPageComponent.propTypes = {
file: object,
uploadedImage: propTypes.image,
}),
onChange: func.isRequired,
onImageUpload: func.isRequired,
onUpdateProfile: func.isRequired,
scrollingDisabled: bool.isRequired,
@ -187,7 +172,6 @@ const mapStateToProps = state => {
};
const mapDispatchToProps = dispatch => ({
onChange: () => dispatch(clearUpdatedForm()),
onImageUpload: data => dispatch(uploadImage(data)),
onUpdateProfile: data => dispatch(updateProfile(data)),
});

View file

@ -4,6 +4,7 @@ import ReviewForm from './ReviewForm';
export const Empty = {
component: ReviewForm,
props: {
formId: 'ReviewFormExample',
onSubmit: values => {
console.log('Submit ReviewForm with (unformatted) values:', values);
},

View file

@ -1,103 +1,106 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { Form as FinalForm } from 'react-final-form';
import classNames from 'classnames';
import { isTransactionsTransitionAlreadyReviewed } from '../../util/errors';
import { propTypes } from '../../util/types';
import { required } from '../../util/validators';
import { FieldReviewRating, Form, PrimaryButton, TextInputField } from '../../components';
import { FieldReviewRating, Form, PrimaryButton, FieldTextInput } from '../../components';
import css from './ReviewForm.css';
const ReviewFormComponent = props => {
const {
className,
rootClassName,
disabled,
handleSubmit,
intl,
form,
invalid,
submitting,
reviewSent,
sendReviewError,
sendReviewInProgress,
} = props;
const ReviewFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
className,
rootClassName,
disabled,
handleSubmit,
intl,
formId,
invalid,
reviewSent,
sendReviewError,
sendReviewInProgress,
} = fieldRenderProps;
const reviewRating = intl.formatMessage({ id: 'ReviewForm.reviewRatingLabel' });
const reviewRatingRequiredMessage = intl.formatMessage({
id: 'ReviewForm.reviewRatingRequired',
});
const reviewRating = intl.formatMessage({ id: 'ReviewForm.reviewRatingLabel' });
const reviewRatingRequiredMessage = intl.formatMessage({
id: 'ReviewForm.reviewRatingRequired',
});
const reviewContent = intl.formatMessage({ id: 'ReviewForm.reviewContentLabel' });
const reviewContentPlaceholderMessage = intl.formatMessage({
id: 'ReviewForm.reviewContentPlaceholder',
});
const reviewContentRequiredMessage = intl.formatMessage({
id: 'ReviewForm.reviewContentRequired',
});
const reviewContent = intl.formatMessage({ id: 'ReviewForm.reviewContentLabel' });
const reviewContentPlaceholderMessage = intl.formatMessage({
id: 'ReviewForm.reviewContentPlaceholder',
});
const reviewContentRequiredMessage = intl.formatMessage({
id: 'ReviewForm.reviewContentRequired',
});
const errorMessage = isTransactionsTransitionAlreadyReviewed(sendReviewError) ? (
<p className={css.error}>
<FormattedMessage id="ReviewForm.reviewSubmitAlreadySent" />
</p>
) : (
<p className={css.error}>
<FormattedMessage id="ReviewForm.reviewSubmitFailed" />
</p>
);
const errorArea = sendReviewError ? errorMessage : <p className={css.errorPlaceholder} />;
const errorMessage = isTransactionsTransitionAlreadyReviewed(sendReviewError) ? (
<p className={css.error}>
<FormattedMessage id="ReviewForm.reviewSubmitAlreadySent" />
</p>
) : (
<p className={css.error}>
<FormattedMessage id="ReviewForm.reviewSubmitFailed" />
</p>
);
const errorArea = sendReviewError ? errorMessage : <p className={css.errorPlaceholder} />;
const reviewSubmitMessage = intl.formatMessage({
id: 'ReviewForm.reviewSubmit',
});
const reviewSubmitMessage = intl.formatMessage({
id: 'ReviewForm.reviewSubmit',
});
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = submitting || sendReviewInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = sendReviewInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
return (
<Form className={classes} onSubmit={handleSubmit}>
<FieldReviewRating
className={css.reviewRating}
id={`${form}.starRating`}
name="reviewRating"
label={reviewRating}
validate={required(reviewRatingRequiredMessage)}
/>
return (
<Form className={classes} onSubmit={handleSubmit}>
<FieldReviewRating
className={css.reviewRating}
id={formId ? `${formId}.starRating` : 'starRating'}
name="reviewRating"
label={reviewRating}
validate={required(reviewRatingRequiredMessage)}
/>
<TextInputField
className={css.reviewContent}
type="textarea"
name="reviewContent"
id={`${form}.reviewContent`}
label={reviewContent}
placeholder={reviewContentPlaceholderMessage}
validate={[required(reviewContentRequiredMessage)]}
/>
<FieldTextInput
className={css.reviewContent}
type="textarea"
id={formId ? `${formId}.reviewContent` : 'reviewContent'}
name="reviewContent"
label={reviewContent}
placeholder={reviewContentPlaceholderMessage}
validate={required(reviewContentRequiredMessage)}
/>
{errorArea}
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={reviewSent}
>
{reviewSubmitMessage}
</PrimaryButton>
</Form>
);
};
{errorArea}
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
ready={reviewSent}
>
{reviewSubmitMessage}
</PrimaryButton>
</Form>
);
}}
/>
);
ReviewFormComponent.defaultProps = { className: null, rootClassName: null, sendReviewError: null };
const { bool, func, string } = PropTypes;
ReviewFormComponent.propTypes = {
...formPropTypes,
className: string,
rootClassName: string,
intl: intlShape.isRequired,
@ -107,6 +110,7 @@ ReviewFormComponent.propTypes = {
sendReviewInProgress: bool.isRequired,
};
const formName = 'ReviewForm';
const ReviewForm = compose(injectIntl)(ReviewFormComponent);
ReviewForm.displayName = 'ReviewForm';
export default compose(reduxForm({ form: formName }), injectIntl)(ReviewFormComponent);
export default ReviewForm;

View file

@ -56,14 +56,13 @@ class SendMessageFormComponent extends Component {
messagePlaceholder,
form,
handleSubmit,
submitting,
inProgress,
sendMessageError,
invalid,
} = this.props;
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = submitting || inProgress;
const submitInProgress = inProgress;
const submitDisabled = invalid || submitInProgress;
return (

View file

@ -4,6 +4,7 @@ import SignupForm from './SignupForm';
export const Empty = {
component: SignupForm,
props: {
formId: 'SignupFormExample',
onSubmit(values) {
console.log('sign up with form values:', values);
},

View file

@ -2,198 +2,208 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { Form as FinalForm } from 'react-final-form';
import classNames from 'classnames';
import { Form, PrimaryButton, TextInputField } from '../../components';
import * as validators from '../../util/validators';
import { Form, PrimaryButton, FieldTextInput } from '../../components';
import css from './SignupForm.css';
const KEY_CODE_ENTER = 13;
const SignupFormComponent = props => {
const {
rootClassName,
className,
form,
handleSubmit,
submitting,
inProgress,
invalid,
intl,
onOpenTermsOfService,
} = props;
const SignupFormComponent = props => (
<FinalForm
{...props}
render={fieldRenderProps => {
const {
rootClassName,
className,
formId,
handleSubmit,
inProgress,
invalid,
intl,
onOpenTermsOfService,
} = fieldRenderProps;
// email
const emailLabel = intl.formatMessage({
id: 'SignupForm.emailLabel',
});
const emailPlaceholder = intl.formatMessage({
id: 'SignupForm.emailPlaceholder',
});
const emailRequiredMessage = intl.formatMessage({
id: 'SignupForm.emailRequired',
});
const emailRequired = validators.required(emailRequiredMessage);
const emailInvalidMessage = intl.formatMessage({
id: 'SignupForm.emailInvalid',
});
const emailValid = validators.emailFormatValid(emailInvalidMessage);
// email
const emailLabel = intl.formatMessage({
id: 'SignupForm.emailLabel',
});
const emailPlaceholder = intl.formatMessage({
id: 'SignupForm.emailPlaceholder',
});
const emailRequiredMessage = intl.formatMessage({
id: 'SignupForm.emailRequired',
});
const emailRequired = validators.required(emailRequiredMessage);
const emailInvalidMessage = intl.formatMessage({
id: 'SignupForm.emailInvalid',
});
const emailValid = validators.emailFormatValid(emailInvalidMessage);
// password
const passwordLabel = intl.formatMessage({
id: 'SignupForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'SignupForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'SignupForm.passwordRequired',
});
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'SignupForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMaxLengthMessage = intl.formatMessage(
{
id: 'SignupForm.passwordTooLong',
},
{
maxLength: validators.PASSWORD_MAX_LENGTH,
}
);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordMaxLength = validators.maxLength(
passwordMaxLengthMessage,
validators.PASSWORD_MAX_LENGTH
);
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordValidators = [passwordRequired, passwordMinLength, passwordMaxLength];
// password
const passwordLabel = intl.formatMessage({
id: 'SignupForm.passwordLabel',
});
const passwordPlaceholder = intl.formatMessage({
id: 'SignupForm.passwordPlaceholder',
});
const passwordRequiredMessage = intl.formatMessage({
id: 'SignupForm.passwordRequired',
});
const passwordMinLengthMessage = intl.formatMessage(
{
id: 'SignupForm.passwordTooShort',
},
{
minLength: validators.PASSWORD_MIN_LENGTH,
}
);
const passwordMaxLengthMessage = intl.formatMessage(
{
id: 'SignupForm.passwordTooLong',
},
{
maxLength: validators.PASSWORD_MAX_LENGTH,
}
);
const passwordMinLength = validators.minLength(
passwordMinLengthMessage,
validators.PASSWORD_MIN_LENGTH
);
const passwordMaxLength = validators.maxLength(
passwordMaxLengthMessage,
validators.PASSWORD_MAX_LENGTH
);
const passwordRequired = validators.requiredStringNoTrim(passwordRequiredMessage);
const passwordValidators = validators.composeValidators(
passwordRequired,
passwordMinLength,
passwordMaxLength
);
// firstName
const firstNameLabel = intl.formatMessage({
id: 'SignupForm.firstNameLabel',
});
const firstNamePlaceholder = intl.formatMessage({
id: 'SignupForm.firstNamePlaceholder',
});
const firstNameRequiredMessage = intl.formatMessage({
id: 'SignupForm.firstNameRequired',
});
const firstNameRequired = validators.required(firstNameRequiredMessage);
// firstName
const firstNameLabel = intl.formatMessage({
id: 'SignupForm.firstNameLabel',
});
const firstNamePlaceholder = intl.formatMessage({
id: 'SignupForm.firstNamePlaceholder',
});
const firstNameRequiredMessage = intl.formatMessage({
id: 'SignupForm.firstNameRequired',
});
const firstNameRequired = validators.required(firstNameRequiredMessage);
// lastName
const lastNameLabel = intl.formatMessage({
id: 'SignupForm.lastNameLabel',
});
const lastNamePlaceholder = intl.formatMessage({
id: 'SignupForm.lastNamePlaceholder',
});
const lastNameRequiredMessage = intl.formatMessage({
id: 'SignupForm.lastNameRequired',
});
const lastNameRequired = validators.required(lastNameRequiredMessage);
// lastName
const lastNameLabel = intl.formatMessage({
id: 'SignupForm.lastNameLabel',
});
const lastNamePlaceholder = intl.formatMessage({
id: 'SignupForm.lastNamePlaceholder',
});
const lastNameRequiredMessage = intl.formatMessage({
id: 'SignupForm.lastNameRequired',
});
const lastNameRequired = validators.required(lastNameRequiredMessage);
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = submitting || inProgress;
const submitDisabled = invalid || submitInProgress;
const classes = classNames(rootClassName || css.root, className);
const submitInProgress = inProgress;
const submitDisabled = invalid || submitInProgress;
const handleTermsKeyUp = e => {
// Allow click action with keyboard like with normal links
if (e.keyCode === KEY_CODE_ENTER) {
onOpenTermsOfService();
}
};
const termsLink = (
<span
className={css.termsLink}
onClick={onOpenTermsOfService}
role="button"
tabIndex="0"
onKeyUp={handleTermsKeyUp}
>
<FormattedMessage id="SignupForm.termsAndConditionsLinkText" />
</span>
);
return (
<Form className={classes} onSubmit={handleSubmit}>
<div>
<TextInputField
type="email"
name="email"
autoComplete="email"
id={`${form}.email`}
label={emailLabel}
placeholder={emailPlaceholder}
validate={[emailRequired, emailValid]}
/>
<div className={css.name}>
<TextInputField
className={css.firstNameRoot}
type="text"
name="fname"
autoComplete="given-name"
id={`${form}.fname`}
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<TextInputField
className={css.lastNameRoot}
type="text"
name="lname"
autoComplete="family-name"
id={`${form}.lname`}
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
<TextInputField
className={css.password}
type="password"
name="password"
autoComplete="new-password"
id={`${form}.password`}
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordValidators}
/>
</div>
<div className={css.bottomWrapper}>
<p className={css.bottomWrapperText}>
<span className={css.termsText}>
<FormattedMessage id="SignupForm.termsAndConditionsAcceptText" values={{ termsLink }} />
</span>
</p>
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
const handleTermsKeyUp = e => {
// Allow click action with keyboard like with normal links
if (e.keyCode === KEY_CODE_ENTER) {
onOpenTermsOfService();
}
};
const termsLink = (
<span
className={css.termsLink}
onClick={onOpenTermsOfService}
role="button"
tabIndex="0"
onKeyUp={handleTermsKeyUp}
>
<FormattedMessage id="SignupForm.signUp" />
</PrimaryButton>
</div>
</Form>
);
};
<FormattedMessage id="SignupForm.termsAndConditionsLinkText" />
</span>
);
return (
<Form className={classes} onSubmit={handleSubmit}>
<div>
<FieldTextInput
type="email"
id={formId ? `${formId}.email` : 'email'}
name="email"
autoComplete="email"
label={emailLabel}
placeholder={emailPlaceholder}
validate={validators.composeValidators(emailRequired, emailValid)}
/>
<div className={css.name}>
<FieldTextInput
className={css.firstNameRoot}
type="text"
id={formId ? `${formId}.fname` : 'fname'}
name="fname"
autoComplete="given-name"
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<FieldTextInput
className={css.lastNameRoot}
type="text"
id={formId ? `${formId}.lname` : 'lname'}
name="lname"
autoComplete="family-name"
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
<FieldTextInput
className={css.password}
type="password"
id={formId ? `${formId}.password` : 'password'}
name="password"
autoComplete="new-password"
label={passwordLabel}
placeholder={passwordPlaceholder}
validate={passwordValidators}
/>
</div>
<div className={css.bottomWrapper}>
<p className={css.bottomWrapperText}>
<span className={css.termsText}>
<FormattedMessage
id="SignupForm.termsAndConditionsAcceptText"
values={{ termsLink }}
/>
</span>
</p>
<PrimaryButton
className={css.submitButton}
type="submit"
inProgress={submitInProgress}
disabled={submitDisabled}
>
<FormattedMessage id="SignupForm.signUp" />
</PrimaryButton>
</div>
</Form>
);
}}
/>
);
SignupFormComponent.defaultProps = { inProgress: false };
const { bool, func } = PropTypes;
SignupFormComponent.propTypes = {
...formPropTypes,
inProgress: bool,
onOpenTermsOfService: func.isRequired,
@ -202,8 +212,7 @@ SignupFormComponent.propTypes = {
intl: intlShape.isRequired,
};
const defaultFormName = 'SignupForm';
const SignupForm = compose(reduxForm({ form: defaultFormName }), injectIntl)(SignupFormComponent);
const SignupForm = compose(injectIntl)(SignupFormComponent);
SignupForm.displayName = 'SignupForm';
export default SignupForm;

View file

@ -1,12 +1,15 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import { fakeIntl } from '../../util/test-data';
import SignupForm from './SignupForm';
const noop = () => null;
describe('SignupForm', () => {
it('matches snapshot', () => {
const tree = renderDeep(<SignupForm onOpenTermsOfService={noop} />);
const tree = renderDeep(
<SignupForm intl={fakeIntl} onOpenTermsOfService={noop} onSubmit={noop} />
);
expect(tree).toMatchSnapshot();
});
});

View file

@ -12,19 +12,17 @@ exports[`SignupForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="SignupForm.email"
htmlFor="email"
>
SignupForm.emailLabel
</label>
<input
autoComplete="email"
className=""
id="SignupForm.email"
id="email"
name="email"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="SignupForm.emailPlaceholder"
type="email"
@ -36,19 +34,17 @@ exports[`SignupForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="SignupForm.fname"
htmlFor="fname"
>
SignupForm.firstNameLabel
</label>
<input
autoComplete="given-name"
className=""
id="SignupForm.fname"
id="fname"
name="fname"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="SignupForm.firstNamePlaceholder"
type="text"
@ -59,19 +55,17 @@ exports[`SignupForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="SignupForm.lname"
htmlFor="lname"
>
SignupForm.lastNameLabel
</label>
<input
autoComplete="family-name"
className=""
id="SignupForm.lname"
id="lname"
name="lname"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="SignupForm.lastNamePlaceholder"
type="text"
@ -83,19 +77,17 @@ exports[`SignupForm matches snapshot 1`] = `
className=""
>
<label
htmlFor="SignupForm.password"
htmlFor="password"
>
SignupForm.passwordLabel
</label>
<input
autoComplete="new-password"
className=""
id="SignupForm.password"
id="password"
name="password"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
placeholder="SignupForm.passwordPlaceholder"
type="password"

View file

@ -8,12 +8,15 @@ import * as ExpandingTextarea from './components/ExpandingTextarea/ExpandingText
import * as FieldBirthdayInput from './components/FieldBirthdayInput/FieldBirthdayInput.example';
import * as FieldBoolean from './components/FieldBoolean/FieldBoolean.example';
import * as FieldCheckbox from './components/FieldCheckbox/FieldCheckbox.example';
import * as FieldCheckboxGroup from './components/FieldCheckboxGroup/FieldCheckboxGroup.example';
import * as FieldCurrencyInput from './components/FieldCurrencyInput/FieldCurrencyInput.example';
import * as FieldDateInput from './components/FieldDateInput/FieldDateInput.example';
import * as FieldDateRangeInput from './components/FieldDateRangeInput/FieldDateRangeInput.example';
import * as FieldGroupCheckbox from './components/FieldGroupCheckbox/FieldGroupCheckbox.example';
import * as FieldPhoneNumberInput from './components/FieldPhoneNumberInput/FieldPhoneNumberInput.example';
import * as FieldReviewRating from './components/FieldReviewRating/FieldReviewRating.example';
import * as FieldSelect from './components/FieldSelect/FieldSelect.example';
import * as FieldTextInput from './components/FieldTextInput/FieldTextInput.example';
import * as Footer from './components/Footer/Footer.example';
import * as IconBannedUser from './components/IconBannedUser/IconBannedUser.example';
import * as IconCheckmark from './components/IconCheckmark/IconCheckmark.example';
@ -98,12 +101,15 @@ export {
FieldBirthdayInput,
FieldBoolean,
FieldCheckbox,
FieldCheckboxGroup,
FieldCurrencyInput,
FieldDateInput,
FieldDateRangeInput,
FieldGroupCheckbox,
FieldPhoneNumberInput,
FieldReviewRating,
FieldSelect,
FieldTextInput,
Footer,
IconBannedUser,
IconCheckmark,

View file

@ -11,6 +11,12 @@
* Note that this file is required for the build process.
*/
// React 16 depends on the collection types Map and Set, as well as requestAnimationFrame.
// https://reactjs.org/docs/javascript-environment-requirements.html
import 'core-js/es6/map';
import 'core-js/es6/set';
import 'raf/polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import Decimal from 'decimal.js';
@ -30,7 +36,7 @@ import './marketplaceIndex.css';
const { BigDecimal } = sdkTypes;
const render = store => {
const render = (store, shouldHydrate) => {
// If the server already loaded the auth information, render the app
// immediately. Otherwise wait for the flag to be loaded and render
// when auth information is present.
@ -39,7 +45,11 @@ const render = store => {
info
.then(() => {
store.dispatch(fetchCurrentUser());
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
if (shouldHydrate) {
ReactDOM.hydrate(<ClientApp store={store} />, document.getElementById('root'));
} else {
ReactDOM.render(<ClientApp store={store} />, document.getElementById('root'));
}
})
.catch(e => {
log.error(e, 'browser-side-render-failed');
@ -88,7 +98,7 @@ if (typeof window !== 'undefined') {
const store = configureStore(initialState, sdk, analyticsHandlers);
require('./util/polyfills');
render(store);
render(store, !!window.__PRELOADED_STATE__);
if (config.dev) {
// Expose stuff for the browser REPL

View file

@ -1,81 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ValidationError } from '../components';
/**
* Hoc to convert a component used within a Field to one that renders
* a label and possible errors.
*
* @param {Component|String} Comp component or a String that is used
* to create an input element
*
* @param {Object} options extra class names to inject to the child
* components
*
* @return {Component} new component that wraps the given one with a
* label and possible errors
*/
export const enhancedField = (Comp, options = {}) => {
const { rootClassName = '', labelClassName = '' } = options;
class EnhancedField extends Component {
componentWillUnmount() {
if (this.props.clearOnUnmount) {
this.props.input.onChange('');
}
}
render() {
/* eslint-disable no-unused-vars */
const { clearOnUnmount, ...restProps } = this.props;
/* eslint-enable no-unused-vars */
const { input, type, label, placeholder, meta, ...otherProps } = restProps;
let component = null;
if (typeof Comp !== 'string') {
component = <Comp {...restProps} />;
} else if (Comp === 'textarea') {
component = <textarea {...otherProps} {...input} placeholder={placeholder} />;
} else {
component = <input {...otherProps} {...input} type={type} placeholder={placeholder} />;
}
const labelInfo = label ? (
<label className={labelClassName} htmlFor={input.name}>
{label}
</label>
) : null;
return (
<div className={rootClassName}>
{labelInfo}
{component}
<ValidationError fieldMeta={meta} />
</div>
);
}
}
let displayName = Comp;
if (Comp && typeof Comp !== 'string') {
displayName = Comp.displayName || Comp.name;
}
EnhancedField.displayName = `EnhancedField(${displayName})`;
EnhancedField.defaultProps = { type: null, placeholder: '', label: null, clearOnUnmount: false };
const { shape, func, string, object, bool } = PropTypes;
EnhancedField.propTypes = {
input: shape({
onChange: func.isRequired,
name: string.isRequired,
}).isRequired,
type: string,
label: string,
placeholder: string,
meta: object.isRequired,
clearOnUnmount: bool,
};
return EnhancedField;
};

View file

@ -252,10 +252,13 @@ export const fakeFormProps = {
untouch: noop,
submit: noop,
reset: noop,
resetSection: noop,
initialize: noop,
handleSubmit: noop,
destroy: noop,
clearAsyncError: noop,
clearFields: noop,
clearSubmitErrors: noop,
change: noop,
blur: noop,
autofill: noop,

View file

@ -1,7 +1,7 @@
import React from 'react';
import { mapValues } from 'lodash';
import Enzyme, { shallow, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-15';
import Adapter from 'enzyme-adapter-react-16';
import toJson from 'enzyme-to-json';
import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';

View file

@ -119,3 +119,6 @@ export const ageAtLeast = (message, minYears) => value => {
const ageInYears = now.diff(moment(value), 'years', true);
return value && value instanceof Date && ageInYears >= minYears ? VALID : message;
};
export const composeValidators = (...validators) => value =>
validators.reduce((error, validator) => error || validator(value), VALID);

View file

@ -5,6 +5,7 @@ import {
minLength,
maxLength,
moneySubUnitAmountAtLeast,
composeValidators,
} from './validators';
const { Money } = sdkTypes;
@ -148,4 +149,39 @@ describe('validators', () => {
expect(moneySubUnitAmountAtLeast('fail', 50)(new Money(100, 'USD'))).toBeUndefined();
});
});
describe('composeValidators()', () => {
const validateLength = composeValidators(minLength('minLength', 4), maxLength('maxLength', 6));
it('should fail on composed minLength (4) and maxLength (6): undefined', () => {
expect(validateLength(undefined)).toEqual('minLength');
});
it('should fail on composed minLength (4) and maxLength (6): null', () => {
expect(validateLength(null)).toEqual('minLength');
});
it('should fail on composed minLength (4) and maxLength (6): bad', () => {
expect(validateLength('bad')).toEqual('minLength');
});
it('should fail on composed minLength (4) and maxLength (6): longword', () => {
expect(validateLength('longword')).toEqual('maxLength');
});
it('should allow on composed minLength (4) and maxLength (6): good', () => {
expect(validateLength('good')).toBeUndefined();
});
it('should fail on composed required and minLength (4): empty string. Error: required', () => {
const requiredWithMinLength = composeValidators(
required('required'),
minLength('minLength', 6)
);
expect(requiredWithMinLength('')).toEqual('required');
});
it('should fail on composed minLength (4) and required: empty string. Error: minLength', () => {
const requiredWithMinLength = composeValidators(
minLength('minLength', 4),
required('required')
);
expect(validateLength('')).toEqual('minLength');
});
});
});

233
yarn.lock
View file

@ -262,7 +262,7 @@ array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
array-flatten@^2.1.0, array-flatten@^2.1.1:
array-flatten@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
@ -306,6 +306,14 @@ array.prototype.find@^2.0.4:
define-properties "^1.1.2"
es-abstract "^1.7.0"
array.prototype.flatten@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/array.prototype.flatten/-/array.prototype.flatten-1.2.1.tgz#a77ae1b64524ce373b137fade324d12040d3c680"
dependencies:
define-properties "^1.1.2"
es-abstract "^1.10.0"
function-bind "^1.1.1"
arrify@^1.0.0, arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@ -1846,7 +1854,7 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
consolidated-events@^1.1.0:
consolidated-events@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-1.1.1.tgz#25395465b35e531395418b7bbecb5ecaf198d179"
@ -1958,14 +1966,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-react-class@^15.6.0:
version "15.6.2"
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.2.tgz#cf1ed15f12aad7f14ef5f2dfe05e6c42f91ef02a"
dependencies:
fbjs "^0.8.9"
loose-envify "^1.3.1"
object-assign "^4.1.1"
cross-spawn@5.1.0, cross-spawn@^5.0.1, cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@ -2511,17 +2511,19 @@ entities@^1.1.1, entities@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
enzyme-adapter-react-15@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/enzyme-adapter-react-15/-/enzyme-adapter-react-15-1.0.5.tgz#99f9a03ff2c2303e517342935798a6bdfbb75fac"
enzyme-adapter-react-16@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4"
dependencies:
enzyme-adapter-utils "^1.1.0"
enzyme-adapter-utils "^1.3.0"
lodash "^4.17.4"
object.assign "^4.0.4"
object.values "^1.0.4"
prop-types "^15.5.10"
prop-types "^15.6.0"
react-reconciler "^0.7.0"
react-test-renderer "^16.0.0-0"
enzyme-adapter-utils@^1.1.0:
enzyme-adapter-utils@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7"
dependencies:
@ -2568,6 +2570,16 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"
es-abstract@^1.10.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.11.0.tgz#cce87d518f0496893b1a30cd8461835535480681"
dependencies:
es-to-primitive "^1.1.1"
function-bind "^1.1.1"
has "^1.0.1"
is-callable "^1.1.3"
is-regex "^1.0.4"
es-abstract@^1.6.1, es-abstract@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c"
@ -2593,9 +2605,9 @@ es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
es6-symbol "~3.1.1"
next-tick "1"
es6-error@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.0.1.tgz#eeb3e280f57e2ec48d72a9fccaf6247d3c1f5719"
es6-error@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
version "2.0.3"
@ -3122,18 +3134,6 @@ fbjs@^0.8.4:
setimmediate "^1.0.5"
ua-parser-js "^0.7.9"
fbjs@^0.8.9:
version "0.8.14"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c"
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.9"
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@ -3188,6 +3188,14 @@ fill-range@^4.0.0:
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
final-form-arrays@^1.0.4:
version "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.5.2"
resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.5.2.tgz#f8a9518784bdc2f72e71548a42b04c640963ca49"
finalhandler@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
@ -3740,14 +3748,14 @@ hoek@4.x.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
hoist-non-react-statics@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1:
hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@ -4047,6 +4055,12 @@ invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.1, invariant@^2.2.2:
dependencies:
loose-envify "^1.0.0"
invariant@^2.2.3:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
loose-envify "^1.0.0"
invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
@ -4953,7 +4967,11 @@ locate-path@^2.0.0:
p-locate "^2.0.0"
path-exists "^3.0.0"
lodash-es@^4.17.3, lodash-es@^4.2.0, lodash-es@^4.2.1:
lodash-es@^4.17.5:
version "4.17.8"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.8.tgz#6fa8c8c5d337481df0bdf1c0d899d42473121e45"
lodash-es@^4.2.1:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
@ -5030,7 +5048,7 @@ lodash@^3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.1.1, lodash@^4.12.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
lodash@^4.1.1, lodash@^4.12.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.1, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@ -6616,7 +6634,7 @@ prop-types-exact@^1.1.1:
has "^1.0.1"
object.assign "^4.0.4"
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0:
prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@ -6806,33 +6824,29 @@ rc@^1.1.6, rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-addons-shallow-compare@^15.5.2, react-addons-shallow-compare@^15.6.2:
react-addons-shallow-compare@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz#198a00b91fc37623db64a28fd17b596ba362702f"
dependencies:
fbjs "^0.8.4"
object-assign "^4.1.0"
react-addons-test-utils@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156"
react-dates@^16.0.0:
version "16.0.1"
resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-16.0.1.tgz#0522e65c57b1a5dd685160127e29dff8bb41e573"
version "16.6.0"
resolved "https://registry.yarnpkg.com/react-dates/-/react-dates-16.6.0.tgz#03cbd7ff308b0d709b87834b592088cf837bcc6e"
dependencies:
airbnb-prop-types "^2.8.1"
consolidated-events "^1.1.0"
consolidated-events "^1.1.1"
is-touch-device "^1.0.1"
lodash "^4.1.1"
object.assign "^4.0.4"
object.assign "^4.1.0"
object.values "^1.0.4"
prop-types "^15.5.10"
react-addons-shallow-compare "^15.5.2"
prop-types "^15.6.0"
react-addons-shallow-compare "^15.6.2"
react-moment-proptypes "^1.5.0"
react-portal "^4.1.0"
react-with-styles "^2.2.0"
react-with-styles-interface-css "^3.0.0"
react-portal "^4.1.2"
react-with-styles "^3.1.0"
react-with-styles-interface-css "^4.0.1"
react-dev-utils@^5.0.0:
version "5.0.1"
@ -6857,19 +6871,27 @@ react-dev-utils@^5.0.0:
strip-ansi "3.0.1"
text-table "0.2.0"
react-dom@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
react-dom@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.1.tgz#6a3c90a4fb62f915bdbcf6204422d93a7d4ca573"
dependencies:
fbjs "^0.8.9"
fbjs "^0.8.16"
loose-envify "^1.1.0"
object-assign "^4.1.0"
prop-types "^15.5.10"
object-assign "^4.1.1"
prop-types "^15.6.0"
react-error-overlay@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
react-final-form-arrays@^1.0.4:
version "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.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"
resolved "https://registry.yarnpkg.com/react-google-maps/-/react-google-maps-9.4.5.tgz#920c199bdc925e0ce93880edffb09428d263aafa"
@ -6904,28 +6926,41 @@ react-intl@^2.4.0:
intl-relativeformat "^2.0.0"
invariant "^2.1.1"
react-is@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.1.tgz#ee66e6d8283224a83b3030e110056798488359ba"
react-moment-proptypes@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/react-moment-proptypes/-/react-moment-proptypes-1.5.0.tgz#4a448cd6479efc5dd509283f361f3753c3abe60e"
dependencies:
moment ">=1.6.0"
react-portal@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.1.2.tgz#7f28f3c8c2ed5c541907c0ed0f24e8996acf627f"
react-portal@^4.1.2:
version "4.1.4"
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.1.4.tgz#3fc3f3f3a0e81362ab1dc9afa3c4bb5a84ec76a3"
dependencies:
prop-types "^15.5.8"
react-redux@^5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946"
react-reconciler@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.7.0.tgz#9614894103e5f138deeeb5eabaf3ee80eb1d026d"
dependencies:
hoist-non-react-statics "^2.2.1"
invariant "^2.0.0"
lodash "^4.2.0"
lodash-es "^4.2.0"
fbjs "^0.8.16"
loose-envify "^1.1.0"
prop-types "^15.5.10"
object-assign "^4.1.1"
prop-types "^15.6.0"
react-redux@^5.0.7:
version "5.0.7"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
dependencies:
hoist-non-react-statics "^2.5.0"
invariant "^2.0.0"
lodash "^4.17.5"
lodash-es "^4.17.5"
loose-envify "^1.1.0"
prop-types "^15.6.0"
react-router-dom@^4.2.2:
version "4.2.2"
@ -6966,12 +7001,14 @@ react-sortable-hoc@^0.6.8:
lodash "^4.12.0"
prop-types "^15.5.7"
react-test-renderer@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8"
react-test-renderer@^16.0.0-0:
version "16.3.1"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.3.1.tgz#d9257936d8535bd40f57f3d5a84e7b0452fb17f2"
dependencies:
fbjs "^0.8.9"
object-assign "^4.1.0"
fbjs "^0.8.16"
object-assign "^4.1.1"
prop-types "^15.6.0"
react-is "^16.3.1"
react-with-direction@^1.1.0:
version "1.1.0"
@ -6985,33 +7022,31 @@ react-with-direction@^1.1.0:
object.values "^1.0.4"
prop-types "^15.6.0"
react-with-styles-interface-css@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-with-styles-interface-css/-/react-with-styles-interface-css-3.0.0.tgz#ebb000c23f75567f25af6792ab7a29089d798fda"
react-with-styles-interface-css@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/react-with-styles-interface-css/-/react-with-styles-interface-css-4.0.1.tgz#79d0a40bae388e11f15362d35aa52e8917982db0"
dependencies:
aphrodite "^1.2.5"
array-flatten "^2.1.1"
array.prototype.flatten "^1.2.1"
global-cache "^1.2.1"
react-with-styles@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/react-with-styles/-/react-with-styles-2.3.1.tgz#5cd54718cd21b188c6b5c1334e2d445251164e67"
react-with-styles@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/react-with-styles/-/react-with-styles-3.1.1.tgz#a2a86ed04b73fcadcd20cd30bf6a5111b6d852dc"
dependencies:
deepmerge "^1.5.2"
global-cache "^1.2.1"
hoist-non-react-statics "^2.3.1"
prop-types "^15.6.0"
react-with-direction "^1.1.0"
react@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
react@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.3.1.tgz#4a2da433d471251c69b6033ada30e2ed1202cfd8"
dependencies:
create-react-class "^15.6.0"
fbjs "^0.8.9"
fbjs "^0.8.16"
loose-envify "^1.1.0"
object-assign "^4.1.0"
prop-types "^15.5.10"
object-assign "^4.1.1"
prop-types "^15.6.0"
read-cache@^1.0.0:
version "1.0.0"
@ -7146,18 +7181,18 @@ reduce-function-call@^1.0.1, reduce-function-call@^1.0.2:
dependencies:
balanced-match "^0.4.2"
redux-form@^6.8.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-6.8.0.tgz#ff1b590b59f987d7e3ff080d752f7120bfe42af3"
redux-form@^7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-7.3.0.tgz#b92ef1639c86a6009b0821aacfc80ad8b5ac8c05"
dependencies:
deep-equal "^1.0.1"
es6-error "^4.0.0"
hoist-non-react-statics "^1.2.0"
invariant "^2.2.2"
es6-error "^4.1.1"
hoist-non-react-statics "^2.5.0"
invariant "^2.2.3"
is-promise "^2.1.0"
lodash "^4.17.3"
lodash-es "^4.17.3"
prop-types "^15.5.9"
lodash "^4.17.5"
lodash-es "^4.17.5"
prop-types "^15.6.1"
redux-thunk@^2.2.0:
version "2.2.0"