Implement listing editing

This commit is contained in:
Kimmo Puputti 2017-08-11 11:08:17 +03:00
parent b8b12c1c40
commit d827128703
29 changed files with 359 additions and 61 deletions

View file

@ -23,12 +23,16 @@
overflow: hidden;
}
/* Firefox doesn't support image aspect ratio inside flexbox */
.aspectRatioWrapper {
padding-bottom: 60%; /* 5:3 Aspect Ratio = 100% / (w / h) */
.threeToTwoWrapper {
position: relative;
}
.thumbnailImage {
.aspectWrapper {
padding-bottom: calc(100% * (2 / 3));
}
.rootForImage {
/* Layout - image will take space defined by aspect ratio wrapper */
position: absolute;
top: 0;
bottom: 0;

View file

@ -11,7 +11,7 @@ import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { SortableContainer } from 'react-sortable-hoc';
import classNames from 'classnames';
import { Promised } from '../../components';
import { Promised, ResponsiveImage } from '../../components';
import { uuid } from '../../util/propTypes';
import css from './AddImages.css';
@ -52,8 +52,8 @@ class Thumbnail extends Component {
renderFulfilled={dataURL => {
return (
<div className={classes}>
<div className={css.aspectRatioWrapper}>
<img src={dataURL} alt={file.name} className={css.thumbnailImage} />
<div className={css.aspectWrapper}>
<img src={dataURL} alt={file.name} className={css.rootForImage} />
</div>
{uploadingOverlay}
</div>
@ -69,7 +69,7 @@ class Thumbnail extends Component {
Thumbnail.defaultProps = { className: null, imageId: null };
const { any, array, func, node, string } = PropTypes;
const { any, array, func, node, string, object } = PropTypes;
Thumbnail.propTypes = {
className: string,
@ -78,6 +78,40 @@ Thumbnail.propTypes = {
imageId: uuid,
};
const ThumbnailWrapper = props => {
const { className, image } = props;
if (image.file) {
return <Thumbnail className={className} {...image} />;
} else {
const classes = classNames(css.thumbnail, className);
return (
<div className={classes}>
<div className={css.threeToTwoWrapper}>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
image={image}
alt="TODO: alt text"
nameSet={[
{ name: 'landscape-crop', size: '400w' },
{ name: 'landscape-crop2x', size: '800w' },
]}
sizes="100%"
/>
</div>
</div>
</div>
);
}
};
ThumbnailWrapper.defaultProps = { className: null };
ThumbnailWrapper.propTypes = {
className: string,
image: object.isRequired,
};
// Sorting is disabled temporarily.
//
// The issue with sorting is that in touch devices it makes
@ -89,9 +123,7 @@ Thumbnail.propTypes = {
//
// TODO Think what to do with the scrolling issue when sorting is in use
//
// TODO Enable me:
// const SortableImage = SortableElement(Thumbnail);
const SortableImage = Thumbnail;
const SortableImage = ThumbnailWrapper;
// Create container where there are sortable images and passed children like "Add image" input etc.
const SortableImages = SortableContainer(props => {
@ -101,7 +133,12 @@ const SortableImages = SortableContainer(props => {
<div className={classes}>
{images.map((image, index) => {
return (
<SortableImage {...image} index={index} key={image.id} className={thumbnailClassName} />
<SortableImage
image={image}
index={index}
key={image.id.uuid || image.id}
className={thumbnailClassName}
/>
);
})}
{children}

View file

@ -6,7 +6,16 @@ import { EditListingDescriptionForm } from '../../containers';
import css from './EditListingDescriptionPanel.css';
const EditListingDescriptionPanel = props => {
const { className, rootClassName, listing, onSubmit, submitButtonText } = props;
const {
className,
rootClassName,
listing,
onSubmit,
onChange,
submitButtonText,
panelUpdated,
errors,
} = props;
const classes = classNames(rootClassName || css.root, className);
const { attributes: { description, title } } = listing || { attributes: {} };
@ -19,12 +28,15 @@ const EditListingDescriptionPanel = props => {
initialValues={{ title, description }}
saveActionMsg={submitButtonText}
onSubmit={onSubmit}
onChange={onChange}
updated={panelUpdated}
updateError={errors.updateListingError}
/>
</div>
);
};
const { func, object, string } = PropTypes;
const { func, object, string, bool } = PropTypes;
EditListingDescriptionPanel.defaultProps = {
className: null,
@ -37,7 +49,10 @@ EditListingDescriptionPanel.propTypes = {
rootClassName: string,
listing: object, // TODO Should be propTypes.listing after API support is added.
onSubmit: func.isRequired,
onChange: func.isRequired,
submitButtonText: string.isRequired,
panelUpdated: bool.isRequired,
errors: object.isRequired,
};
export default EditListingDescriptionPanel;

View file

@ -6,7 +6,16 @@ import { EditListingLocationForm } from '../../containers';
import css from './EditListingLocationPanel.css';
const EditListingLocationPanel = props => {
const { className, rootClassName, listing, onSubmit, submitButtonText } = props;
const {
className,
rootClassName,
listing,
onSubmit,
onChange,
submitButtonText,
panelUpdated,
errors,
} = props;
const classes = classNames(rootClassName || css.root, className);
const { attributes: { address, geolocation } } = listing || { attributes: {} };
@ -44,13 +53,16 @@ const EditListingLocationPanel = props => {
className={css.form}
initialValues={initialSearchFormValues}
onSubmit={onSubmit}
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
/>
</div>
);
};
const { func, object, string } = PropTypes;
const { func, object, string, bool } = PropTypes;
EditListingLocationPanel.defaultProps = {
className: null,
@ -63,7 +75,10 @@ EditListingLocationPanel.propTypes = {
rootClassName: string,
listing: object, // TODO Should be propTypes.listing after API support is added.
onSubmit: func.isRequired,
onChange: func.isRequired,
submitButtonText: string.isRequired,
panelUpdated: bool.isRequired,
errors: object.isRequired,
};
export default EditListingLocationPanel;

View file

@ -81,6 +81,8 @@ class EditListingPhotosPanel extends Component {
onUpdateImageOrder,
onManageDisableScrolling,
submitButtonText,
panelUpdated,
onChange,
} = this.props;
const rootClass = rootClassName || css.root;
@ -100,8 +102,11 @@ class EditListingPhotosPanel extends Component {
images={images}
onImageUpload={onImageUpload}
onSubmit={this.handlePhotosSubmit}
onChange={onChange}
onUpdateImageOrder={onUpdateImageOrder}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
/>
<Modal
id="EditListingPhotosPanel.payoutModal"
@ -148,6 +153,7 @@ EditListingPhotosPanel.propTypes = {
currentUser: propTypes.currentUser,
errors: shape({
createListingsError: object,
updateListingError: object,
showListingsError: object,
uploadImageError: object,
}),
@ -157,8 +163,10 @@ EditListingPhotosPanel.propTypes = {
onPayoutDetailsSubmit: func.isRequired,
onUpdateImageOrder: func.isRequired,
onSubmit: func.isRequired,
onChange: func.isRequired,
submitButtonText: string.isRequired,
onManageDisableScrolling: func.isRequired,
panelUpdated: bool.isRequired,
};
export default EditListingPhotosPanel;

View file

@ -6,7 +6,16 @@ import { EditListingPricingForm } from '../../containers';
import css from './EditListingPricingPanel.css';
const EditListingPricingPanel = props => {
const { className, rootClassName, listing, onSubmit, submitButtonText } = props;
const {
className,
rootClassName,
listing,
onSubmit,
onChange,
submitButtonText,
panelUpdated,
errors,
} = props;
const classes = classNames(rootClassName || css.root, className);
const { attributes: { price } } = listing || { attributes: {} };
@ -18,13 +27,16 @@ const EditListingPricingPanel = props => {
className={css.form}
initialValues={{ price }}
onSubmit={onSubmit}
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
/>
</div>
);
};
const { func, object, string } = PropTypes;
const { func, object, string, bool } = PropTypes;
EditListingPricingPanel.defaultProps = {
className: null,
@ -37,7 +49,10 @@ EditListingPricingPanel.propTypes = {
rootClassName: string,
listing: object, // TODO Should be propTypes.listing after API support is added.
onSubmit: func.isRequired,
onChange: func.isRequired,
submitButtonText: string.isRequired,
panelUpdated: bool.isRequired,
errors: object.isRequired,
};
export default EditListingPricingPanel;

View file

@ -22,11 +22,14 @@ export const NoPhotos = {
stripeConnected: true,
onImageUpload: noop,
onUpdateImageOrder: noop,
onUpdateListing: noop,
onCreateListing: noop,
onCreateListingDraft: noop,
onPayoutDetailsSubmit: noop,
onUpdateListingDraft: noop,
onManageDisableScrolling: noop,
onChange: noop,
errors: {},
},
useDefaultWrapperStyles: false,
};

View file

@ -80,14 +80,17 @@ const EditListingWizard = props => {
images,
listing,
onCreateListing,
onUpdateListing,
onCreateListingDraft,
onImageUpload,
onPayoutDetailsSubmit,
onUpdateImageOrder,
onUpdateListingDraft,
onChange,
rootClassName,
currentUser,
onManageDisableScrolling,
updatedTab,
intl,
} = props;
@ -111,6 +114,9 @@ const EditListingWizard = props => {
}
const onUpsertListingDraft = currentListing.id ? onUpdateListingDraft : onCreateListingDraft;
const update = (tab, values) => {
onUpdateListing(tab, { ...values, id: currentListing.id });
};
return (
<Tabs rootClassName={classes} navRootClassName={css.nav} tabRootClassName={css.tab}>
@ -119,17 +125,23 @@ const EditListingWizard = props => {
tabLabel="Description"
tabLinkProps={tabLink(DESCRIPTION)}
selected={selectedTab === DESCRIPTION}
panelUpdated={updatedTab === DESCRIPTION}
disabled={!stepsStatus[DESCRIPTION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, DESCRIPTION)}
onChange={onChange}
onSubmit={values => {
onUpsertListingDraft(values);
const pathParams = tabParams(LOCATION);
// Redirect to location tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
if (isNew) {
onUpsertListingDraft(values);
const pathParams = tabParams(LOCATION);
// Redirect to location tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
} else {
update(DESCRIPTION, values);
}
}}
/>
<EditListingLocationPanel
@ -137,24 +149,31 @@ const EditListingWizard = props => {
tabLabel="Location"
tabLinkProps={tabLink(LOCATION)}
selected={selectedTab === LOCATION}
panelUpdated={updatedTab === LOCATION}
disabled={!stepsStatus[LOCATION]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, LOCATION)}
onChange={onChange}
onSubmit={values => {
const { building, location } = values;
const { selectedPlace: { address, origin } } = location;
// TODO When API supports building number, etc. change this to use those fields instead.
onUpdateListingDraft({
const updateValues = {
address: JSON.stringify({ locationAddress: address, building }),
geolocation: origin,
});
};
const pathParams = tabParams(PRICING);
// Redirect to pricing tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
if (isNew) {
// TODO When API supports building number, etc. change this to use those fields instead.
onUpdateListingDraft(updateValues);
const pathParams = tabParams(PRICING);
// Redirect to pricing tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
} else {
update(LOCATION, updateValues);
}
}}
/>
<EditListingPricingPanel
@ -162,17 +181,23 @@ const EditListingWizard = props => {
tabLabel="Pricing"
tabLinkProps={tabLink(PRICING)}
selected={selectedTab === PRICING}
panelUpdated={updatedTab === PRICING}
disabled={!stepsStatus[PRICING]}
errors={errors}
listing={listing}
submitButtonText={submitText(intl, isNew, PRICING)}
onChange={onChange}
onSubmit={values => {
onUpdateListingDraft(values);
const pathParams = tabParams(PHOTOS);
// Redirect to photos tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
if (isNew) {
onUpdateListingDraft(values);
const pathParams = tabParams(PHOTOS);
// Redirect to photos tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
} else {
update(PRICING, values);
}
}}
/>
<EditListingPhotosPanel
@ -181,6 +206,7 @@ const EditListingWizard = props => {
tabLinkProps={tabLink(PHOTOS)}
selected={selectedTab === PHOTOS}
disabled={!stepsStatus[PHOTOS]}
panelUpdated={updatedTab === PHOTOS}
errors={errors}
fetchInProgress={fetchInProgress}
listing={listing}
@ -188,9 +214,16 @@ const EditListingWizard = props => {
onImageUpload={onImageUpload}
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
submitButtonText={submitText(intl, isNew, PHOTOS)}
onChange={onChange}
onSubmit={values => {
const { country, images: updatedImages } = values;
onCreateListing({ ...listing.attributes, country, images: updatedImages });
const updateValues = { ...listing.attributes, country, images: updatedImages };
if (isNew) {
onCreateListing(updateValues);
} else {
const imageIds = updatedImages.map(img => img.imageId || img.id);
update(PHOTOS, { images: imageIds });
}
}}
onUpdateImageOrder={onUpdateImageOrder}
currentUser={currentUser}
@ -206,6 +239,7 @@ EditListingWizard.defaultProps = {
listing: null,
rootClassName: null,
currentUser: null,
updatedTab: null,
};
const { array, arrayOf, bool, func, object, oneOf, shape, string } = PropTypes;
@ -220,9 +254,10 @@ EditListingWizard.propTypes = {
}).isRequired,
errors: shape({
createListingsError: object,
updateListingError: object,
showListingsError: object,
uploadImageError: object,
}),
}).isRequired,
fetchInProgress: bool.isRequired,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
history: shape({
@ -241,14 +276,17 @@ EditListingWizard.propTypes = {
images: array,
}),
onCreateListing: func.isRequired,
onUpdateListing: func.isRequired,
onCreateListingDraft: func.isRequired,
onImageUpload: func.isRequired,
onPayoutDetailsSubmit: func.isRequired,
onUpdateImageOrder: func.isRequired,
onUpdateListingDraft: func.isRequired,
onChange: func.isRequired,
rootClassName: string,
currentUser: propTypes.currentUser,
onManageDisableScrolling: func.isRequired,
updatedTab: string,
// from injectIntl
intl: intlShape.isRequired,

View file

@ -8,6 +8,10 @@
flex-direction: column;
}
.error {
color: var(--failColor);
}
.description {
flex-shrink: 0;
margin-top: 24px;
@ -27,3 +31,8 @@
margin-top: 100px;
}
}
.loaderIcon {
/* width: 100%; */
/* height: 40px; */
}

View file

@ -8,6 +8,7 @@ export const Empty = {
console.log('Submit EditListingDescriptionForm with (unformatted) values:', values);
},
saveActionMsg: 'Save description',
updated: false,
},
group: 'forms',
};

View file

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { maxLength, required } from '../../util/validators';
import { Button, TextInputField } from '../../components';
@ -20,6 +20,8 @@ const EditListingDescriptionFormComponent = props => {
invalid,
saveActionMsg,
submitting,
updated,
updateError,
} = props;
const titleMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.title' });
@ -45,9 +47,20 @@ const EditListingDescriptionFormComponent = props => {
id: 'EditListingDescriptionForm.descriptionRequired',
});
const errorMessage = updateError
? <p className={css.error}>
<FormattedMessage id="EditListingDescriptionForm.updateFailed" />
</p>
: null;
const buttonContent = updated
? <FormattedMessage id="EditListingDescriptionForm.updated" />
: saveActionMsg;
const classes = classNames(css.root, className);
return (
<form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<TextInputField
type="text"
name="title"
@ -73,15 +86,15 @@ const EditListingDescriptionFormComponent = props => {
type="submit"
disabled={invalid || submitting || disabled}
>
{saveActionMsg}
{buttonContent}
</Button>
</form>
);
};
EditListingDescriptionFormComponent.defaultProps = { className: null };
EditListingDescriptionFormComponent.defaultProps = { className: null, updateError: null };
const { func, string } = PropTypes;
const { func, string, bool, instanceOf } = PropTypes;
EditListingDescriptionFormComponent.propTypes = {
...formPropTypes,
@ -89,6 +102,8 @@ EditListingDescriptionFormComponent.propTypes = {
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: instanceOf(Error),
};
const formName = 'EditListingDescriptionForm';

View file

@ -14,6 +14,7 @@ describe('EditListingDescriptionForm', () => {
dispatch={noop}
onSubmit={v => v}
saveActionMsg="Save description"
updated={false}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -8,6 +8,10 @@
flex-direction: column;
}
.error {
color: var(--failColor);
}
.locationAutocompleteInput {
height: 36px;
padding-left: 0;

View file

@ -8,6 +8,7 @@ export const Empty = {
console.log('Submit EditListingLocationForm with (unformatted) values:', values);
},
saveActionMsg: 'Save location',
updated: false,
},
group: 'forms',
};

View file

@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reduxForm, formValueSelector, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { autocompleteSearchRequired, autocompletePlaceSelected } from '../../util/validators';
@ -20,6 +20,8 @@ export const EditListingLocationFormComponent = props => {
invalid,
saveActionMsg,
submitting,
updated,
updateError,
} = props;
const titleRequiredMessage = intl.formatMessage({ id: 'EditListingLocationForm.address' });
@ -38,10 +40,17 @@ export const EditListingLocationFormComponent = props => {
id: 'EditListingLocationForm.buildingPlaceholder',
});
const errorMessage = updateError
? <p className={css.error}>
<FormattedMessage id="EditListingLocationForm.updateFailed" />
</p>
: null;
const classes = classNames(css.root, className);
return (
<form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<LocationAutocompleteInputField
inputClassName={css.locationAutocompleteInput}
iconClassName={css.locationAutocompleteInputIcon}
@ -72,7 +81,7 @@ export const EditListingLocationFormComponent = props => {
type="submit"
disabled={invalid || submitting || disabled}
>
{saveActionMsg}
{updated ? <FormattedMessage id="EditListingLocationForm.updated" /> : saveActionMsg}
</Button>
</form>
);
@ -80,9 +89,10 @@ export const EditListingLocationFormComponent = props => {
EditListingLocationFormComponent.defaultProps = {
selectedPlace: null,
updateError: null,
};
const { func, string } = PropTypes;
const { func, string, bool, instanceOf } = PropTypes;
EditListingLocationFormComponent.propTypes = {
...formPropTypes,
@ -90,6 +100,8 @@ EditListingLocationFormComponent.propTypes = {
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
selectedPlace: propTypes.place,
updated: bool.isRequired,
updateError: instanceOf(Error),
};
const formName = 'EditListingLocationForm';

View file

@ -15,6 +15,7 @@ describe('EditListingLocationForm', () => {
dispatch={noop}
onSubmit={v => v}
saveActionMsg="Save location"
updated={false}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -11,10 +11,17 @@ const errorAction = actionType => error => ({ type: actionType, payload: error,
// ================ Action types ================ //
export const MARK_TAB_UPDATED = 'app/EditListingPage/MARK_TAB_UPDATED';
export const CLEAR_UPDATED_TAB = 'app/EditListingPage/CLEAR_UPDATED_TAB';
export const CREATE_LISTING_REQUEST = 'app/EditListingPage/CREATE_LISTING_REQUEST';
export const CREATE_LISTING_SUCCESS = 'app/EditListingPage/CREATE_LISTING_SUCCESS';
export const CREATE_LISTING_ERROR = 'app/EditListingPage/CREATE_LISTING_ERROR';
export const UPDATE_LISTING_REQUEST = 'app/EditListingPage/UPDATE_LISTING_REQUEST';
export const UPDATE_LISTING_SUCCESS = 'app/EditListingPage/UPDATE_LISTING_SUCCESS';
export const UPDATE_LISTING_ERROR = 'app/EditListingPage/UPDATE_LISTING_ERROR';
export const SHOW_LISTINGS_REQUEST = 'app/EditListingPage/SHOW_LISTINGS_REQUEST';
export const SHOW_LISTINGS_SUCCESS = 'app/EditListingPage/SHOW_LISTINGS_SUCCESS';
export const SHOW_LISTINGS_ERROR = 'app/EditListingPage/SHOW_LISTINGS_ERROR';
@ -33,6 +40,7 @@ export const UPDATE_LISTING_DRAFT = 'app/EditListingPage/UPDATE_LISTING_DRAFT';
const initialState = {
// Error instance placeholders for each endpoint
createListingsError: null,
updateListingError: null,
showListingsError: null,
uploadImageError: null,
submittedListingId: null,
@ -40,11 +48,17 @@ const initialState = {
images: {},
imageOrder: [],
listingDraft: null,
updatedTab: null,
};
export default function reducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case MARK_TAB_UPDATED:
return { ...state, updatedTab: payload };
case CLEAR_UPDATED_TAB:
return { ...state, updatedTab: null, updateListingError: null };
case CREATE_LISTING_REQUEST:
return {
...state,
@ -59,6 +73,15 @@ export default function reducer(state = initialState, action = {}) {
console.error(payload);
return { ...state, createListingsError: payload, redirectToListing: false };
case UPDATE_LISTING_REQUEST:
return { ...state, updateListingError: null };
case UPDATE_LISTING_SUCCESS:
return state;
case UPDATE_LISTING_ERROR:
// eslint-disable-next-line no-console
console.error(payload);
return { ...state, updateListingError: payload };
case SHOW_LISTINGS_REQUEST:
return { ...state, showListingsError: null };
case SHOW_LISTINGS_SUCCESS:
@ -118,6 +141,15 @@ export default function reducer(state = initialState, action = {}) {
// ================ Action creators ================ //
export const markTabUpdated = tab => ({
type: MARK_TAB_UPDATED,
payload: tab,
});
export const clearUpdatedTab = () => ({
type: CLEAR_UPDATED_TAB,
});
export const updateImageOrder = imageOrder => ({
type: UPDATE_IMAGE_ORDER,
payload: { imageOrder },
@ -149,6 +181,11 @@ export const createListing = requestAction(CREATE_LISTING_REQUEST);
export const createListingSuccess = successAction(CREATE_LISTING_SUCCESS);
export const createListingError = errorAction(CREATE_LISTING_ERROR);
// SDK method: listings.update
export const updateListing = requestAction(UPDATE_LISTING_REQUEST);
export const updateListingSuccess = successAction(UPDATE_LISTING_SUCCESS);
export const updateListingError = errorAction(UPDATE_LISTING_ERROR);
// SDK method: listings.show
export const showListings = requestAction(SHOW_LISTINGS_REQUEST);
export const showListingsSuccess = successAction(SHOW_LISTINGS_SUCCESS);
@ -214,8 +251,29 @@ export function requestImageUpload(actionPayload) {
};
}
export function requestUpdateListing(tab, data) {
return (dispatch, getState, sdk) => {
dispatch(updateListing(data));
const { id } = data;
let updateResponse;
return sdk.listings
.update(data)
.then(response => {
updateResponse = response;
const payload = { id, include: ['author', 'images'] };
return dispatch(requestShowListing(payload));
})
.then(() => {
dispatch(markTabUpdated(tab));
dispatch(updateListingSuccess(updateResponse));
})
.catch(e => dispatch(updateListingError(e)));
};
}
export function loadData(params) {
return dispatch => {
dispatch(clearUpdatedTab());
const { id, type } = params;
if (type === 'new') {
return Promise.resolve(null);

View file

@ -15,9 +15,11 @@ import {
createListingDraft,
updateListingDraft,
requestCreateListing,
requestUpdateListing,
requestImageUpload,
updateImageOrder,
loadData,
clearUpdatedTab,
} from './EditListingPage.duck';
import css from './EditListingPage.css';
@ -59,6 +61,7 @@ export const EditListingPageComponent = props => {
logoutError,
notificationCount,
onCreateListing,
onUpdateListing,
onCreateListingDraft,
onImageUpload,
onLogout,
@ -66,6 +69,7 @@ export const EditListingPageComponent = props => {
onPayoutDetailsSubmit,
onUpdateImageOrder,
onUpdateListingDraft,
onChange,
page,
params,
scrollingDisabled,
@ -86,15 +90,21 @@ export const EditListingPageComponent = props => {
const listingSlug = currentListing ? createSlug(currentListing.attributes.title) : null;
return <NamedRedirect name="ListingPage" params={{ id: listingId.uuid, slug: listingSlug }} />;
} else if (showForm) {
const { createListingsError = null, showListingsError = null, uploadImageError = null } = page;
const errors = { createListingsError, showListingsError, uploadImageError };
const {
createListingsError = null,
updateListingError = null,
showListingsError = null,
uploadImageError = null,
} = page;
const errors = { createListingsError, updateListingError, showListingsError, uploadImageError };
// Show form if user is posting a new listing or editing existing one
const disableForm = page.redirectToListing && !showListingsError;
// Images are passed to EditListingForm so that it can generate thumbnails out of them
const currentListingImages = currentListing ? currentListing.images : [];
const images = isNew ? page.imageOrder.map(i => page.images[i]) : currentListingImages;
const draftImages = page.imageOrder.map(i => page.images[i]);
const images = currentListingImages.concat(draftImages);
const title = isNew
? intl.formatMessage({ id: 'EditListingPage.titleCreateListing' })
@ -130,13 +140,16 @@ export const EditListingPageComponent = props => {
images={images}
listing={isNew ? page.listingDraft : currentListing}
onCreateListing={onCreateListing}
onUpdateListing={onUpdateListing}
onCreateListingDraft={onCreateListingDraft}
onUpdateListingDraft={onUpdateListingDraft}
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
onImageUpload={onImageUpload}
onUpdateImageOrder={onUpdateImageOrder}
onChange={onChange}
currentUser={currentUser}
onManageDisableScrolling={onManageDisableScrolling}
updatedTab={page.updatedTab}
/>
</PageLayout>
);
@ -180,6 +193,8 @@ EditListingPageComponent.propTypes = {
onPayoutDetailsSubmit: func.isRequired,
onUpdateImageOrder: func.isRequired,
onUpdateListingDraft: func.isRequired,
onUpdateListing: func.isRequired,
onChange: func.isRequired,
page: object.isRequired,
params: shape({
id: string.isRequired,
@ -231,6 +246,7 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
onCreateListing: values => dispatch(requestCreateListing(formatRequestData(values))),
onUpdateListing: (tab, values) => dispatch(requestUpdateListing(tab, values)),
onCreateListingDraft: values => dispatch(createListingDraft(values)),
onImageUpload: data => dispatch(requestImageUpload(data)),
onLogout: historyPush => dispatch(logout(historyPush)),
@ -239,6 +255,7 @@ const mapDispatchToProps = dispatch => ({
onPayoutDetailsSubmit: values => dispatch(createStripeAccount(values)),
onUpdateImageOrder: imageOrder => dispatch(updateImageOrder(imageOrder)),
onUpdateListingDraft: values => dispatch(updateListingDraft(values)),
onChange: () => dispatch(clearUpdatedTab()),
});
const EditListingPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter)(

View file

@ -26,10 +26,12 @@ describe('EditListingPageComponent', () => {
onCreateListing={noop}
onCreateListingDraft={noop}
onUpdateListingDraft={noop}
onUpdateListing={noop}
onImageUpload={noop}
onManageDisableScrolling={noop}
onPayoutDetailsSubmit={noop}
onUpdateImageOrder={noop}
onChange={noop}
page={{ imageOrder: [], images: {} }}
scrollingDisabled={false}
tab="description"

View file

@ -28,6 +28,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
Object {
"createListingsError": null,
"showListingsError": null,
"updateListingError": null,
"uploadImageError": null,
}
}
@ -39,12 +40,14 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
}
}
images={Array []}
onChange={[Function]}
onCreateListing={[Function]}
onCreateListingDraft={[Function]}
onImageUpload={[Function]}
onManageDisableScrolling={[Function]}
onPayoutDetailsSubmit={[Function]}
onUpdateImageOrder={[Function]}
onUpdateListing={[Function]}
onUpdateListingDraft={[Function]}
params={
Object {

View file

@ -54,7 +54,7 @@
/* Firefox doesn't support image aspect ratio inside flexbox */
.aspectRatioWrapper {
padding-bottom: 60%; /* 5:3 Aspect Ratio = 100% / (w / h) */
padding-bottom: calc(100% * (2 / 3));
}
.addImage {

View file

@ -13,6 +13,7 @@ export const Empty = {
console.log('Submit EditListingPhotosForm with (unformatted) values:', values);
},
saveActionMsg: 'Save photos',
updated: false,
onUpdateImageOrder: imageOrder => {
console.log('onUpdateImageOrder with new imageOrder:', imageOrder);
},

View file

@ -27,7 +27,7 @@ const RenderAddImage = props => {
);
};
const { func, node, object, shape, string } = PropTypes;
const { func, node, object, shape, string, bool, instanceOf } = PropTypes;
RenderAddImage.propTypes = {
accept: string.isRequired,
@ -85,6 +85,8 @@ export class EditListingPhotosFormComponent extends Component {
invalid,
saveActionMsg,
submitting,
updated,
updateError,
} = this.props;
const chooseImageText = (
@ -124,13 +126,19 @@ export class EditListingPhotosFormComponent extends Component {
</p>
: null;
const errorMessage = updateError
? <p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.updateFailed" />
</p>
: null;
const classes = classNames(css.root, className);
const disableForm = invalid || submitting || disabled || this.state.imageUploadRequested;
return (
<form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<AddImages
className={css.imagesField}
images={images}
@ -171,14 +179,14 @@ export class EditListingPhotosFormComponent extends Component {
{showListingFailed}
<Button className={css.submitButton} type="submit" disabled={disableForm}>
{saveActionMsg}
{updated ? <FormattedMessage id="EditListingPhotosForm.updated" /> : saveActionMsg}
</Button>
</form>
);
}
}
EditListingPhotosFormComponent.defaultProps = { errors: {} };
EditListingPhotosFormComponent.defaultProps = { errors: {}, updateError: null };
EditListingPhotosFormComponent.propTypes = {
...formPropTypes,
@ -192,6 +200,8 @@ EditListingPhotosFormComponent.propTypes = {
onUpdateImageOrder: func.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: instanceOf(Error),
};
const formName = 'EditListingPhotosForm';

View file

@ -22,6 +22,7 @@ describe('EditListingPhotosForm', () => {
saveActionMsg="Save photos"
onUpdateImageOrder={v => v}
stripeConnected={false}
updated={false}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -9,6 +9,10 @@
flex-grow: 1;
}
.error {
color: var(--failColor);
}
.priceInput {
flex-shrink: 0;
width: 100%;

View file

@ -8,6 +8,7 @@ export const Empty = {
console.log('Submit EditListingPricingForm with (unformatted) values:', values);
},
saveActionMsg: 'Save price',
updated: false,
},
group: 'forms',
};

View file

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl } from 'react-intl';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import { required } from '../../util/validators';
@ -18,6 +18,8 @@ export const EditListingPricingFormComponent = props => {
invalid,
saveActionMsg,
submitting,
updated,
updateError,
} = props;
const pricePerNightMessage = intl.formatMessage({ id: 'EditListingPricingForm.pricePerNight' });
@ -26,10 +28,17 @@ export const EditListingPricingFormComponent = props => {
id: 'EditListingPricingForm.priceInputPlaceholder',
});
const errorMessage = updateError
? <p className={css.error}>
<FormattedMessage id="EditListingPricingForm.updateFailed" />
</p>
: null;
const classes = classNames(css.root, className);
return (
<form className={classes} onSubmit={handleSubmit}>
{errorMessage}
<CurrencyInputField
id="EditListingPricingForm.CurrencyInputField"
className={css.priceInput}
@ -46,19 +55,23 @@ export const EditListingPricingFormComponent = props => {
type="submit"
disabled={invalid || submitting || disabled}
>
{saveActionMsg}
{updated ? <FormattedMessage id="EditListingPricingForm.updated" /> : saveActionMsg}
</Button>
</form>
);
};
const { func, string } = PropTypes;
EditListingPricingFormComponent.defaultProps = { updateError: null };
const { func, string, bool, instanceOf } = PropTypes;
EditListingPricingFormComponent.propTypes = {
...formPropTypes,
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: instanceOf(Error),
};
const formName = 'EditListingPricingForm';

View file

@ -14,6 +14,7 @@ describe('EditListingPricingForm', () => {
dispatch={noop}
onSubmit={v => v}
saveActionMsg="Save price"
updated={false}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -51,6 +51,8 @@
"EditListingDescriptionForm.title": "Name of your sauna",
"EditListingDescriptionForm.titlePlaceholder": "What's the name of your sauna?",
"EditListingDescriptionForm.titleRequired": "You need to add a name.",
"EditListingDescriptionForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingDescriptionForm.updated": "Updated!",
"EditListingDescriptionPanel.title": "Add your sauna",
"EditListingLocationForm.address": "Address",
"EditListingLocationForm.addressNotRecognized": "We didn't recognize this location. Please try another location.",
@ -58,6 +60,8 @@
"EditListingLocationForm.addressRequired": "You need to provide a location",
"EditListingLocationForm.building": "Apt, suite, building # (optional)",
"EditListingLocationForm.buildingPlaceholder": "E.g. A 42",
"EditListingLocationForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingLocationForm.updated": "Updated!",
"EditListingLocationPanel.title": "Where's your sauna?",
"EditListingPage.titleCreateListing": "Create a listing",
"EditListingPage.titleEditListing": "Edit listing",
@ -69,6 +73,8 @@
"EditListingPhotosForm.imageTypes": ".JPG, .GIF or .PNG max. 10 MB",
"EditListingPhotosForm.imageUploadFailed.uploadOverLimit": " Image was too big. Maximum size is 10 MB.",
"EditListingPhotosForm.showListingFailed": "Fetching listing data failed",
"EditListingPhotosForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingPhotosForm.updated": "Updated!",
"EditListingPhotosPanel.payoutModalInfo": "Since this was the first listing you created, we need to know bit more information about you in order to send you money. We only ask these once.",
"EditListingPhotosPanel.payoutModalTitleOneMoreThing": "One more thing:",
"EditListingPhotosPanel.payoutModalTitlePayoutPreferences": "Payout preferences",
@ -76,6 +82,8 @@
"EditListingPricingForm.priceInputPlaceholder": "Choose your price…",
"EditListingPricingForm.pricePerNight": "Price per night in euros",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
"EditListingPricingForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingPricingForm.updated": "Updated!",
"EditListingPricingPanel.title": "How much does it cost?",
"EditListingWizard.saveEditDescription": "Save description",
"EditListingWizard.saveEditLocation": "Save location",