mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
commit
e2fd352dcf
100 changed files with 4018 additions and 3072 deletions
21
package.json
21
package.json
|
|
@ -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/*",
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
28
src/components/FieldCheckboxGroup/FieldCheckboxGroup.css
Normal file
28
src/components/FieldCheckboxGroup/FieldCheckboxGroup.css
Normal 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;
|
||||
}
|
||||
}
|
||||
151
src/components/FieldCheckboxGroup/FieldCheckboxGroup.example.js
Normal file
151
src/components/FieldCheckboxGroup/FieldCheckboxGroup.example.js
Normal 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',
|
||||
};
|
||||
96
src/components/FieldCheckboxGroup/FieldCheckboxGroup.js
Normal file
96
src/components/FieldCheckboxGroup/FieldCheckboxGroup.js
Normal 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;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ describe('DateInput', () => {
|
|||
onBlur: noop,
|
||||
onChange: noop,
|
||||
onFocus: noop,
|
||||
onDragStart: noop,
|
||||
onDrop: noop,
|
||||
id: 'bookingDate',
|
||||
placeholderText: 'today',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -18,8 +18,6 @@ describe('DateRangeInput', () => {
|
|||
onBlur: noop,
|
||||
onChange: noop,
|
||||
onFocus: noop,
|
||||
onDragStart: noop,
|
||||
onDrop: noop,
|
||||
startDateId: 'bookingStartDate',
|
||||
startDatePlaceholderText: 'today',
|
||||
endDateId: 'bookingEndDate',
|
||||
|
|
|
|||
68
src/components/FieldGroupCheckbox/FieldCheckbox.css
Normal file
68
src/components/FieldGroupCheckbox/FieldCheckbox.css
Normal 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;
|
||||
}
|
||||
91
src/components/FieldGroupCheckbox/FieldCheckbox.js
Normal file
91
src/components/FieldGroupCheckbox/FieldCheckbox.js
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
18
src/components/FieldSelect/FieldSelect.css
Normal file
18
src/components/FieldSelect/FieldSelect.css
Normal 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);
|
||||
}
|
||||
51
src/components/FieldSelect/FieldSelect.example.js
Normal file
51
src/components/FieldSelect/FieldSelect.example.js
Normal 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',
|
||||
};
|
||||
72
src/components/FieldSelect/FieldSelect.js
Normal file
72
src/components/FieldSelect/FieldSelect.js
Normal 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;
|
||||
19
src/components/FieldTextInput/FieldTextInput.css
Normal file
19
src/components/FieldTextInput/FieldTextInput.css
Normal 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 {
|
||||
}
|
||||
7
src/components/FieldTextInput/FieldTextInput.example.css
Normal file
7
src/components/FieldTextInput/FieldTextInput.example.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.field {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin-top: 24px;
|
||||
}
|
||||
90
src/components/FieldTextInput/FieldTextInput.example.js
Normal file
90
src/components/FieldTextInput/FieldTextInput.example.js
Normal 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',
|
||||
};
|
||||
121
src/components/FieldTextInput/FieldTextInput.js
Normal file
121
src/components/FieldTextInput/FieldTextInput.js
Normal 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;
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ exports[`AuthenticationPageComponent matches snapshot 1`] = `
|
|||
]
|
||||
}
|
||||
/>
|
||||
<ReduxForm
|
||||
<LoginForm
|
||||
inProgress={false}
|
||||
onSubmit={[Function]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ exports[`ContactDetailsPage matches snapshot 1`] = `
|
|||
values={Object {}}
|
||||
/>
|
||||
</p>
|
||||
<ReduxForm
|
||||
<PasswordRecoveryForm
|
||||
inProgress={false}
|
||||
initialValues={
|
||||
Object {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import PasswordResetForm from './PasswordResetForm';
|
|||
export const Empty = {
|
||||
component: PasswordResetForm,
|
||||
props: {
|
||||
formId: 'PasswordResetFormExample',
|
||||
onSubmit(values) {
|
||||
console.log('submit with values:', values);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
16
src/index.js
16
src/index.js
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
233
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue