diff --git a/package.json b/package.json
index a40106e5..1ba5c0fd 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"react-intl": "^2.2.3",
"react-redux": "^5.0.3",
"react-router-dom": "4.0.0-beta.6",
+ "react-sortable-hoc": "^0.6.1",
"redux": "^3.6.0",
"redux-form": "^6.5.0",
"redux-saga": "^0.14.3",
diff --git a/src/components/AddImages/AddImages.css b/src/components/AddImages/AddImages.css
new file mode 100644
index 00000000..0062b883
--- /dev/null
+++ b/src/components/AddImages/AddImages.css
@@ -0,0 +1,33 @@
+.imagesContainer {
+ width: 100%;
+ min-height: 120px;
+ padding-left: 0;
+}
+
+.thumbnail {
+ display: block;
+ float: left;
+ position: relative;
+ width: 110px;
+ height: 110px;
+ margin: 5px;
+ overflow: hidden;
+}
+
+.thumbnailImage {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.thumbnailLoading {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba(211, 211, 211, 0.8);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/src/components/AddImages/AddImages.example.css b/src/components/AddImages/AddImages.example.css
new file mode 100644
index 00000000..1471a158
--- /dev/null
+++ b/src/components/AddImages/AddImages.example.css
@@ -0,0 +1,29 @@
+.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;
+}
diff --git a/src/components/AddImages/AddImages.example.js b/src/components/AddImages/AddImages.example.js
new file mode 100644
index 00000000..68e47571
--- /dev/null
+++ b/src/components/AddImages/AddImages.example.js
@@ -0,0 +1,81 @@
+import React, { Component } from 'react';
+import { findIndex, uniqueId } from 'lodash';
+import { arrayMove } from 'react-sortable-hoc';
+import AddImages from './AddImages';
+import css from './AddImages.example.css';
+
+const getId = () => {
+ return uniqueId();
+};
+
+class AddImagesTest extends Component {
+ constructor(props, state) {
+ super(props, state);
+ this.state = {
+ images: [],
+ };
+ this.onChange = this.onChange.bind(this);
+ this.onSortEnd = this.onSortEnd.bind(this);
+ }
+
+ onChange(event) {
+ const file = event.target.files[0];
+ const fileId = getId();
+ const imageData = { file, id: fileId, imageId: null };
+
+ // Show loading overlay
+ this.setState({
+ images: this.state.images.concat([imageData]),
+ });
+
+ // Fake image uploaded state: show image thumbnail
+ setTimeout(
+ () => {
+
+ this.setState((prevState) => {
+ const images = prevState.images;
+ const imageIndex = findIndex(images, i => i.id === fileId);
+ const updatedImage = { ...imageData, imageId: fileId };
+ const updatedImages = [
+ ...images.slice(0, imageIndex),
+ updatedImage,
+ ...images.slice(imageIndex + 1),
+ ];
+ return {
+ images: updatedImages,
+ };
+ });
+ },
+ 1000);
+ }
+
+ onSortEnd({ oldIndex, newIndex }) {
+ const { images } = this.state;
+ this.setState({
+ images: arrayMove(images, oldIndex, newIndex),
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export const Empty = {
+ component: AddImagesTest,
+};
diff --git a/src/components/AddImages/AddImages.js b/src/components/AddImages/AddImages.js
new file mode 100644
index 00000000..6e546b23
--- /dev/null
+++ b/src/components/AddImages/AddImages.js
@@ -0,0 +1,97 @@
+/**
+ * Creates a sortable image grid with children added to the end of the created grid.
+ *
+ * Example:
+ * // images = [{ id: 'tempId', imageId: 'realIdFromAPI', file: File }];
+ *
+ *
+ *
+ */
+import React, { Component, PropTypes } from 'react';
+import { SortableContainer, SortableElement } from 'react-sortable-hoc';
+import { Promised } from '../../components';
+import css from './AddImages.css';
+
+// readImage returns a promise which is resolved
+// when FileReader has loaded given file as dataURL
+const readImage = file => new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = e => resolve(e.target.result);
+ reader.onerror = e => {
+ // eslint-disable-next-line
+ console.error('Error (', e, `) happened while reading ${file.name}: ${e.target.result}`);
+ reject(new Error(`Error reading ${file.name}: ${e.target.result}`));
+ };
+ reader.readAsDataURL(file);
+});
+
+// Create sortable elments out of given thumbnail file
+class Thumbnail extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ promisedImage: readImage(this.props.file),
+ };
+ }
+
+ render() {
+ const { file, id, imageId } = this.props;
+ // While image is uploading we show overlay on top of thumbnail
+ const uploadingOverlay = !imageId ? Uploading
: null;
+ return (
+ {
+ return (
+
+
+ {uploadingOverlay}
+
+ );
+ }}
+ renderRejected={() => Could not read file}
+ />
+ );
+ }
+};
+
+Thumbnail.defaultProps = { imageId: null };
+
+const { any, array, func, node, string } = PropTypes;
+
+Thumbnail.propTypes = {
+ file: any.isRequired,
+ id: string.isRequired,
+ imageId: string,
+};
+
+const SortableImage = SortableElement(Thumbnail);
+
+// Create container where there are sortable images and passed children like "Add image" input etc.
+const SortableImages = SortableContainer(props => {
+ const { children, images } = props;
+ return (
+
+ {images.map((image, index) => )}
+ {children}
+
+ );
+});
+
+// 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 ;
+};
+
+AddImages.defaultProps = { images: [] };
+
+AddImages.propTypes = {
+ images: array,
+ children: node.isRequired,
+ onSortEnd: func.isRequired,
+};
+
+export default AddImages;
diff --git a/src/components/index.js b/src/components/index.js
index 940f786c..8a497427 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -1,3 +1,4 @@
+import AddImages from './AddImages/AddImages';
import BookingInfo from './BookingInfo/BookingInfo';
import Discussion from './Discussion/Discussion';
import FilterPanel from './FilterPanel/FilterPanel';
@@ -16,6 +17,7 @@ import RoutesProvider from './RoutesProvider/RoutesProvider';
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
export {
+ AddImages,
BookingInfo,
Discussion,
FilterPanel,
diff --git a/src/containers/EditListingForm/EditListingForm.css b/src/containers/EditListingForm/EditListingForm.css
index c4333d7c..9eb8ee48 100644
--- a/src/containers/EditListingForm/EditListingForm.css
+++ b/src/containers/EditListingForm/EditListingForm.css
@@ -2,11 +2,6 @@
color: red;
}
-.imagesContainer {
- width: 100%;
- min-height: 110px;
-}
-
.addImageWrapper {
float: left;
width: 110px;
@@ -33,31 +28,8 @@
margin-top: 0;
}
-.thumbnail {
- float: left;
- position: relative;
- width: 110px;
- height: 110px;
- margin: 5px;
- overflow: hidden;
-}
-
-.thumbnailImage {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.thumbnailLoading {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- background-color: rgba(211, 211, 211, 0.8);
- display: flex;
- justify-content: center;
- align-items: center;
+.addImageInput {
+ display: none;
}
.imageRequiredWrapper {
diff --git a/src/containers/EditListingForm/EditListingForm.example.js b/src/containers/EditListingForm/EditListingForm.example.js
index 91d193de..40c0c361 100644
--- a/src/containers/EditListingForm/EditListingForm.example.js
+++ b/src/containers/EditListingForm/EditListingForm.example.js
@@ -5,11 +5,14 @@ export const Empty = {
component: EditListingForm,
props: {
images: [],
- onImageUpload(values) {
+ onImageUpload: values => {
console.log(`onImageUpload with id (${values.id}) and file name (${values.file.name})`);
},
- onSubmit(values) {
+ onSubmit: values => {
console.log('Submit EditListingForm with (unformatted) values:', values);
},
+ onUpdateImageOrder: imageOrder => {
+ console.log('onUpdateImageOrder with new imageOrder:', imageOrder);
+ },
},
};
diff --git a/src/containers/EditListingForm/EditListingForm.js b/src/containers/EditListingForm/EditListingForm.js
index 34a5e1af..5dfef8a5 100644
--- a/src/containers/EditListingForm/EditListingForm.js
+++ b/src/containers/EditListingForm/EditListingForm.js
@@ -2,26 +2,14 @@ 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 { Promised } from '../../components';
+import { AddImages } from '../../components';
import css from './EditListingForm.css';
const ACCEPT_IMAGES = 'image/*';
const TITLE_MAX_LENGTH = 60;
-// readImage returns a promise which is resolved
-// when FileReader has loaded given file as dataURL
-const readImage = file => new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = e => resolve(e.target.result);
- reader.onerror = e => {
- // eslint-disable-next-line
- console.error(`Error ${e} happened while reading ${file.name}: ${e.target.result}`);
- reject(new Error(`Error reading ${file.name}: ${e.target.result}`));
- };
- reader.readAsDataURL(file);
-});
-
// Custom inputs with validator messages
const RenderField = ({ input, label, type, meta }) => {
const { touched, error } = meta;
@@ -61,7 +49,7 @@ const RenderAddImage = props => {
const inputProps = { accept, id: name, name, onChange, type };
return (
-
+
);
@@ -83,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() {
@@ -102,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);
@@ -135,28 +129,7 @@ class EditListingForm extends Component {
/>
Images
-
- {images.map(i => {
- // While image is uploading we show overlay on top of thumbnail
- const uploadingOverlay = !i.imageId
- ?
Uploading
- : null;
- return (
-
{
- return (
-
-

- {uploadingOverlay}
-
- );
- }}
- renderRejected={() => Could not read file
}
- />
- );
- })}
+
-
+
{
it('matches snapshot', () => {
- const tree = renderDeep();
+ const tree = renderShallow( v} onSubmit={v => v} onUpdateImageOrder={v => v} />);
expect(tree).toMatchSnapshot();
});
});
diff --git a/src/containers/EditListingForm/__snapshots__/EditListingForm.test.js.snap b/src/containers/EditListingForm/__snapshots__/EditListingForm.test.js.snap
index 082b5cbf..577ff376 100644
--- a/src/containers/EditListingForm/__snapshots__/EditListingForm.test.js.snap
+++ b/src/containers/EditListingForm/__snapshots__/EditListingForm.test.js.snap
@@ -1,82 +1,19 @@
exports[`EditListingForm matches snapshot 1`] = `
-
+
`;
diff --git a/src/containers/EditListingPage/EditListingPage.duck.js b/src/containers/EditListingPage/EditListingPage.duck.js
index 7771df12..681ff0c6 100644
--- a/src/containers/EditListingPage/EditListingPage.duck.js
+++ b/src/containers/EditListingPage/EditListingPage.duck.js
@@ -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.
diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js
index 845bcc0e..5ce65cdd 100644
--- a/src/containers/EditListingPage/EditListingPage.js
+++ b/src/containers/EditListingPage/EditListingPage.js
@@ -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}
/>
@@ -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)),
};
};
diff --git a/src/containers/EditListingPage/EditListingPage.test.js b/src/containers/EditListingPage/EditListingPage.test.js
index ba72ed3d..cd6c2ff7 100644
--- a/src/containers/EditListingPage/EditListingPage.test.js
+++ b/src/containers/EditListingPage/EditListingPage.test.js
@@ -12,6 +12,7 @@ describe('EditListingPageComponent', () => {
onCreateListing={v => v}
onLoadListing={v => v}
onImageUpload={v => v}
+ onUpdateImageOrder={v => v}
page={{ imageOrder: [], images: {} }}
type="new"
/>,
diff --git a/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap b/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap
index b04b050c..a302296e 100644
--- a/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap
+++ b/src/containers/EditListingPage/__snapshots__/EditListingPage.test.js.snap
@@ -6,6 +6,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
initData={Object {}}
onImageUpload={[Function]}
onSubmit={[Function]}
+ onUpdateImageOrder={[Function]}
saveActionMsg="Create listing" />
`;
diff --git a/src/examples.js b/src/examples.js
index 65d69c3d..4953700e 100644
--- a/src/examples.js
+++ b/src/examples.js
@@ -1,4 +1,5 @@
// components
+import * as AddImages from './components/AddImages/AddImages.example';
import * as BookingInfo from './components/BookingInfo/BookingInfo.example';
import * as ListingCard from './components/ListingCard/ListingCard.example';
import * as NamedLink from './components/NamedLink/NamedLink.example';
@@ -17,6 +18,7 @@ import * as PasswordForgottenForm
import * as SignUpForm from './containers/SignUpForm/SignUpForm.example';
export {
+ AddImages,
BookingInfo,
ChangeAccountPasswordForm,
ChangePasswordForm,
diff --git a/yarn.lock b/yarn.lock
index 61ab8782..8e97f847 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -932,7 +932,7 @@ babel-runtime@6.22.0:
core-js "^2.4.0"
regenerator-runtime "^0.10.0"
-babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0:
+babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
dependencies:
@@ -4033,7 +4033,7 @@ lodash.uniq@^4.3.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
-"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
+"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.12.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -5421,6 +5421,14 @@ react-side-effect@^1.1.0:
exenv "^1.2.1"
shallowequal "^0.2.2"
+react-sortable-hoc@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-0.6.1.tgz#921ffdfab7b3918164e02db61db3bd309a2b5df3"
+ dependencies:
+ babel-runtime "^6.11.6"
+ invariant "^2.2.1"
+ lodash "^4.12.0"
+
react-test-renderer@^15.4.2:
version "15.4.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.4.2.tgz#27e1dff5d26d0e830f99614c487622bc831416f3"