Use SortableContainer

This commit is contained in:
Vesa Luusua 2017-03-13 21:38:07 +02:00
parent b9a421834a
commit 3ff30f4720
6 changed files with 75 additions and 33 deletions

View file

@ -8,6 +8,7 @@
* </AddImages>
*/
import React, { PropTypes } from 'react';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';
import { Promised } from '../../components';
import css from './AddImages.css';
@ -24,47 +25,54 @@ const readImage = file => new Promise((resolve, reject) => {
reader.readAsDataURL(file);
});
const AddImages = props => {
// Create sortable elments out of given thumbnail file
const SortableImage = SortableElement(props => {
const { file, id, imageId } = props;
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !imageId ? <div className={css.thumbnailLoading}>Uploading</div> : null;
return (
<Promised
key={id}
promise={readImage(file)}
renderFulfilled={dataURL => {
return (
<li className={css.thumbnail}>
<img src={dataURL} alt={encodeURIComponent(file.name)} className={css.thumbnailImage} />
{uploadingOverlay}
</li>
);
}}
renderRejected={() => <li className={css.thumbnail}>Could not read file</li>}
/>
);
});
// Create container where there are sortable images and passed children like "Add image" input etc.
const SortableImages = SortableContainer(props => {
const { children, images } = props;
return (
<div className={css.imagesContainer}>
{images.map(i => {
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !i.imageId
? <div className={css.thumbnailLoading}>Uploading</div>
: null;
return (
<Promised
key={i.id}
promise={readImage(i.file)}
renderFulfilled={dataURL => {
return (
<div className={css.thumbnail}>
<img
src={dataURL}
alt={encodeURIComponent(i.file.name)}
className={css.thumbnailImage}
/>
{uploadingOverlay}
</div>
);
}}
renderRejected={() => <div className={css.thumbnail}>Could not read file</div>}
/>
);
})}
<ol className={css.imagesContainer}>
{images.map((image, index) => <SortableImage {...image} index={index} key={image.id} />)}
{children}
</div>
</ol>
);
});
// Configure sortable container see. https://github.com/clauderic/react-sortable-hoc
// Items can be sorted horizontally, vertically or in a grid.
// axis="xy" means grid like sorting
const AddImages = props => {
return <SortableImages axis="xy" {...props} />;
};
AddImages.defaultProps = { images: [] };
const { array, node } = PropTypes;
const { array, func, node } = PropTypes;
AddImages.propTypes = {
images: array,
children: node.isRequired,
onSortEnd: func.isRequired,
};
export default AddImages;

View file

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from 'react';
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 { noEmptyArray, maxLength, required } from '../../util/validators';
import { AddImages } from '../../components';
import css from './EditListingForm.css';
@ -70,6 +71,7 @@ class EditListingForm extends Component {
super(props);
this.handleInitialize = this.handleInitialize.bind(this);
this.onImageUploadHandler = this.onImageUploadHandler.bind(this);
this.onSortEnd = this.onSortEnd.bind(this);
}
componentDidMount() {
@ -89,6 +91,11 @@ class EditListingForm extends Component {
}
}
onSortEnd({ oldIndex, newIndex }) {
const images = arrayMove(this.props.images, oldIndex, newIndex);
this.props.onUpdateImageOrder(images.map(i => i.id));
}
handleInitialize() {
const { initData = {}, initialize } = this.props;
initialize(initData);
@ -122,7 +129,7 @@ class EditListingForm extends Component {
/>
<h3>Images</h3>
<AddImages images={images}>
<AddImages images={images} onSortEnd={this.onSortEnd}>
<Field
accept={ACCEPT_IMAGES}
component={RenderAddImage}
@ -169,6 +176,9 @@ EditListingForm.propTypes = {
...formPropTypes,
initData: shape({ title: string, description: string }),
intl: intlShape.isRequired,
onImageUpload: func.isRequired,
onUpdateImageOrder: func.isRequired,
onSubmit: func.isRequired,
};
export default reduxForm({ form: 'EditListingForm' })(injectIntl(EditListingForm));

View file

@ -21,6 +21,8 @@ export const UPLOAD_IMAGE_REQUEST = 'app/EditListingPage/UPLOAD_IMAGE_REQUEST';
export const UPLOAD_IMAGE_SUCCESS = 'app/EditListingPage/UPLOAD_IMAGE_SUCCESS';
export const UPLOAD_IMAGE_ERROR = 'app/EditListingPage/UPLOAD_IMAGE_ERROR';
export const UPDATE_IMAGE_ORDER = 'app/EditListingPage/UPDATE_IMAGE_ORDER';
// ================ Reducer ================ //
const initialState = {
@ -82,6 +84,8 @@ export default function reducer(state = initialState, action = {}) {
};
return { ...state, images };
}
case UPDATE_IMAGE_ORDER:
return { ...state, imageOrder: payload.imageOrder };
default:
return state;
@ -92,6 +96,11 @@ export default function reducer(state = initialState, action = {}) {
// ================ Action creators ================ //
export const updateImageOrder = imageOrder => ({
type: UPDATE_IMAGE_ORDER,
payload: { imageOrder },
});
// All the action creators that don't have the {Success, Error} suffix
// take the params object that the corresponding SDK endpoint method
// expects.

View file

@ -10,6 +10,7 @@ import {
requestCreateListing,
requestShowListing,
requestImageUpload,
updateImageOrder,
} from './EditListingPage.duck';
const formatRequestData = values => {
@ -39,7 +40,16 @@ export class EditListingPageComponent extends Component {
}
render() {
const { data, intl, onCreateListing, onImageUpload, page, params, type } = this.props;
const {
data,
intl,
onCreateListing,
onImageUpload,
onUpdateImageOrder,
page,
params,
type,
} = this.props;
const isNew = type === 'new';
const id = page.submittedListingId || (params && new types.UUID(params.id));
const listingsById = getListingsById(data, [id]);
@ -77,6 +87,7 @@ export class EditListingPageComponent extends Component {
initData={initData}
onImageUpload={onImageUpload}
onSubmit={onSubmit(onCreateListing)}
onUpdateImageOrder={onUpdateImageOrder}
saveActionMsg={saveActionMsg}
/>
</PageLayout>
@ -103,9 +114,10 @@ const { func, object, shape, string } = PropTypes;
EditListingPageComponent.propTypes = {
data: object.isRequired,
intl: intlShape.isRequired,
onImageUpload: func.isRequired,
onLoadListing: func.isRequired,
onCreateListing: func.isRequired,
onLoadListing: func.isRequired,
onImageUpload: func.isRequired,
onUpdateImageOrder: func.isRequired,
page: object.isRequired,
params: shape({
id: string,
@ -124,6 +136,7 @@ const mapDispatchToProps = dispatch => {
onLoadListing: id => dispatch(requestShowListing({ id, include: ['author', 'images'] })),
onCreateListing: values => dispatch(requestCreateListing(formatRequestData(values))),
onImageUpload: data => dispatch(requestImageUpload(data)),
onUpdateImageOrder: imageOrder => dispatch(updateImageOrder(imageOrder)),
};
};

View file

@ -12,6 +12,7 @@ describe('EditListingPageComponent', () => {
onCreateListing={v => v}
onLoadListing={v => v}
onImageUpload={v => v}
onUpdateImageOrder={v => v}
page={{ imageOrder: [], images: {} }}
type="new"
/>,

View file

@ -6,6 +6,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
initData={Object {}}
onImageUpload={[Function]}
onSubmit={[Function]}
onUpdateImageOrder={[Function]}
saveActionMsg="Create listing" />
</Connect(withRouter(PageLayout))>
`;