EditListingPhotosPanel and form within

This commit is contained in:
Vesa Luusua 2017-05-11 15:46:33 +03:00
parent dcfac16948
commit 73d98b5689
11 changed files with 368 additions and 0 deletions

View file

@ -0,0 +1,6 @@
.root {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}

View file

@ -0,0 +1,58 @@
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { EditListingPhotosForm } from '../../containers';
import css from './EditListingPhotosPanel.css';
const EditListingPhotosPanel = props => {
const {
className,
rootClassName,
images,
onImageUpload,
onSubmit,
onUpdateImageOrder,
stripeConnected,
} = props;
const rootClass = rootClassName || css.root;
const classes = classNames(rootClass, className);
// TODO This will be defined in the stripe popup form later.
const country = 'US';
return (
<div className={classes}>
<h1><FormattedMessage id="EditListingPhotosPanel.title" /></h1>
<EditListingPhotosForm
initialValues={{ country, images }}
images={images}
onImageUpload={onImageUpload}
onSubmit={onSubmit}
onUpdateImageOrder={onUpdateImageOrder}
stripeConnected={stripeConnected}
/>
</div>
);
};
const { array, bool, func, string } = PropTypes;
EditListingPhotosPanel.defaultProps = {
className: null,
rootClassName: null,
images: [],
};
EditListingPhotosPanel.propTypes = {
className: string,
rootClassName: string,
images: array,
onImageUpload: func.isRequired,
onUpdateImageOrder: func.isRequired,
onSubmit: func.isRequired,
stripeConnected: bool.isRequired,
};
export default EditListingPhotosPanel;

View file

@ -9,6 +9,7 @@ import DateInput from './DateInput/DateInput';
import Discussion from './Discussion/Discussion';
import EditListingDescriptionPanel from './EditListingDescriptionPanel/EditListingDescriptionPanel';
import EditListingLocationPanel from './EditListingLocationPanel/EditListingLocationPanel';
import EditListingPhotosPanel from './EditListingPhotosPanel/EditListingPhotosPanel';
import EditListingPricingPanel from './EditListingPricingPanel/EditListingPricingPanel';
import EditListingWizard from './EditListingWizard/EditListingWizard';
import ExternalLink from './ExternalLink/ExternalLink';
@ -53,6 +54,7 @@ export {
Discussion,
EditListingDescriptionPanel,
EditListingLocationPanel,
EditListingPhotosPanel,
EditListingPricingPanel,
EditListingWizard,
ExternalLink,

View file

@ -0,0 +1,41 @@
.addImageWrapper {
float: left;
width: 110px;
height: 110px;
margin: 5px;
&::after {
content: ".";
visibility: hidden;
display: block;
height: 0;
clear: both;
}
}
.addImage {
width: 110px;
height: 110px;
background-color: lightgrey;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin-top: 0;
}
.addImageInput {
display: none;
}
.imageRequiredWrapper {
width: 100%;
clear: both;
}
.imageRequiredError {
color: red;
}
.submitButton {
margin-top: 1rem;
}

View file

@ -0,0 +1,19 @@
/* eslint-disable no-console */
import EditListingPhotosForm from './EditListingPhotosForm';
export const Empty = {
component: EditListingPhotosForm,
props: {
initialValues: { country: 'US', images: [] },
stripeConnected: false,
onImageUpload: values => {
console.log(`onImageUpload with id (${values.id}) and file name (${values.file.name})`);
},
onSubmit: values => {
console.log('Submit EditListingPhotosForm with (unformatted) values:', values);
},
onUpdateImageOrder: imageOrder => {
console.log('onUpdateImageOrder with new imageOrder:', imageOrder);
},
},
};

View file

@ -0,0 +1,160 @@
import React, { Component, PropTypes } from 'react';
import { compose } from 'redux';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import { isEqual } from 'lodash';
import { arrayMove } from 'react-sortable-hoc';
import config from '../../config';
import { noEmptyArray, required } from '../../util/validators';
import {
AddImages,
Button,
Input,
StripeBankAccountToken,
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 } = props;
const { name, onChange } = input;
const inputProps = { accept, id: name, name, onChange, type };
return (
<div className={css.addImageWrapper}>
<Input {...inputProps} className={css.addImageInput} />
<label htmlFor={name} className={css.addImage}>{label}</label>
</div>
);
};
const { bool, func, object, shape, string } = PropTypes;
RenderAddImage.propTypes = {
accept: string.isRequired,
input: shape({
value: object,
onChange: func.isRequired,
name: string.isRequired,
}).isRequired,
label: string.isRequired,
type: string.isRequired,
};
export class EditListingPhotosFormComponent extends Component {
constructor(props) {
super(props);
this.onImageUploadHandler = this.onImageUploadHandler.bind(this);
this.onSortEnd = this.onSortEnd.bind(this);
}
componentWillReceiveProps(nextProps) {
if (!isEqual(this.props.images, nextProps.images)) {
nextProps.change('images', nextProps.images);
}
}
onImageUploadHandler(event) {
const file = event.target.files[0];
if (file) {
this.props.onImageUpload({ id: `${file.name}_${Date.now()}`, file });
}
}
onSortEnd({ oldIndex, newIndex }) {
const images = arrayMove(this.props.images, oldIndex, newIndex);
this.props.onUpdateImageOrder(images.map(i => i.id));
}
render() {
const {
disabled,
handleSubmit,
images,
intl,
invalid,
saveActionMsg,
submitting,
stripeConnected,
} = this.props;
const imageRequiredMessage = intl.formatMessage({ id: 'EditListingPhotosForm.imageRequired' });
const bankAccountNumberRequiredMessage = intl.formatMessage({
id: 'EditListingPhotosForm.bankAccountNumberRequired',
});
// TODO This will be defined in the stripe popup form later.
// When that's ready, remove country from initialValues and hidden field.
const country = this.props.initialValues.country;
const showBankAccountField = !stripeConnected;
const currency = config.currencyConfig.currency;
return (
<form onSubmit={handleSubmit}>
<AddImages images={images} onSortEnd={this.onSortEnd}>
<Field
accept={ACCEPT_IMAGES}
component={RenderAddImage}
label="+ Add image"
name="addImage"
onChange={this.onImageUploadHandler}
type="file"
/>
<Field
component={props => {
const { input, type, meta } = props;
return (
<div className={css.imageRequiredWrapper}>
<Input {...input} type={type} />
<ValidationError className={css.imageRequiredError} fieldMeta={meta} />
</div>
);
}}
name="images"
type="hidden"
validate={[noEmptyArray(imageRequiredMessage)]}
/>
</AddImages>
<Field component="input" name="country" type="hidden" value={country} />
{showBankAccountField
? <Field
name="bankAccountToken"
component={StripeBankAccountToken}
props={{ country, currency }}
validate={required(bankAccountNumberRequiredMessage)}
/>
: null}
<Button
className={css.submitButton}
type="submit"
disabled={invalid || submitting || disabled}
>
{saveActionMsg}
</Button>
</form>
);
}
}
EditListingPhotosFormComponent.defaultProps = { saveActionMsg: 'Publish listing' };
EditListingPhotosFormComponent.propTypes = {
...formPropTypes,
intl: intlShape.isRequired,
onImageUpload: func.isRequired,
onUpdateImageOrder: func.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string,
stripeConnected: bool.isRequired,
};
const formName = 'EditListingPhotosForm';
export default compose(reduxForm({ form: formName }), injectIntl)(EditListingPhotosFormComponent);

View file

@ -0,0 +1,28 @@
// TODO: 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 { EditListingPhotosFormComponent } from './EditListingPhotosForm';
const noop = () => null;
describe('EditListingPhotosForm', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<EditListingPhotosFormComponent
{...fakeFormProps}
initialValues={{ country: 'US', images: [] }}
intl={fakeIntl}
dispatch={noop}
onImageUpload={v => v}
onSubmit={v => v}
onUpdateImageOrder={v => v}
stripeConnected={false}
/>
);
expect(tree).toMatchSnapshot();
});
});

View file

@ -0,0 +1,46 @@
exports[`EditListingPhotosForm matches snapshot 1`] = `
<form
onSubmit={[Function]}>
<AddImages
images={Array []}
onSortEnd={[Function]}>
<Field
accept="image/*"
component={[Function]}
label="+ Add image"
name="addImage"
onChange={[Function]}
type="file" />
<Field
component={[Function]}
name="images"
type="hidden"
validate={
Array [
[Function],
]
} />
</AddImages>
<Field
component="input"
name="country"
type="hidden"
value="US" />
<Field
component={[Function]}
name="bankAccountToken"
props={
Object {
"country": "US",
"currency": "USD",
}
}
validate={[Function]} />
<Button
className={null}
rootClassName=""
type="submit">
Publish listing
</Button>
</form>
`;

View file

@ -6,6 +6,7 @@ import CheckoutPage from './CheckoutPage/CheckoutPage';
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
import EditListingDescriptionForm from './EditListingDescriptionForm/EditListingDescriptionForm';
import EditListingLocationForm from './EditListingLocationForm/EditListingLocationForm';
import EditListingPhotosForm from './EditListingPhotosForm/EditListingPhotosForm';
import EditListingPricingForm from './EditListingPricingForm/EditListingPricingForm';
import EditListingPage from './EditListingPage/EditListingPage';
import EditProfilePage from './EditProfilePage/EditProfilePage';
@ -39,6 +40,7 @@ export {
ContactDetailsPage,
EditListingDescriptionForm,
EditListingLocationForm,
EditListingPhotosForm,
EditListingPricingForm,
EditListingPage,
EditProfilePage,

View file

@ -27,6 +27,8 @@ import * as EditListingDescriptionForm
from './containers/EditListingDescriptionForm/EditListingDescriptionForm.example';
import * as EditListingLocationForm
from './containers/EditListingLocationForm/EditListingLocationForm.example';
import * as EditListingPhotosForm
from './containers/EditListingPhotosForm/EditListingPhotosForm.example';
import * as EditListingPricingForm
from './containers/EditListingPricingForm/EditListingPricingForm.example';
import * as SearchForm from './containers/SearchForm/SearchForm.example';
@ -47,6 +49,7 @@ export {
DateInput,
EditListingDescriptionForm,
EditListingLocationForm,
EditListingPhotosForm,
EditListingPricingForm,
EditListingWizard,
ListingCard,

View file

@ -43,6 +43,9 @@
"EditListingLocationForm.locationRequired": "You need to provide a location",
"EditListingLocationForm.locationNotRecognized": "We didn't recognize this location. Please try another location.",
"EditListingLocationPanel.title": "Where's your sauna located?",
"EditListingPhotosForm.imageRequired": "You need to add at least one image.",
"EditListingPhotosForm.bankAccountNumberRequired": "You need to add a bank account number.",
"EditListingPhotosPanel.title": "Add photos",
"EditListingPricingForm.price": "Price",
"EditListingPricingForm.pricePlaceholder": "$0.00",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",