Merge pull request #345 from sharetribe/remove-listing-images

Remove listing images
This commit is contained in:
Kimmo Puputti 2017-08-18 08:14:50 +03:00 committed by GitHub
commit f80689fbb0
14 changed files with 140 additions and 7 deletions

View file

@ -28,6 +28,35 @@
position: relative;
}
.removeImage {
position: absolute;
top: 0;
right: 0;
width: 33px;
height: 33px;
padding: 0;
background-color: rgba(0, 0, 0, 0.5);
border: none;
border-radius: 0;
border-bottom-left-radius: 2px;
cursor: pointer;
& svg {
position: absolute;
top: 12px;
left: 12px;
width: 10px;
height: 10px;
fill: var(--matterColorLight);
stroke: var(--matterColorLight);
}
&:hover svg {
fill: var(--matterColorAnti);
stroke: var(--matterColorAnti);
}
}
.aspectWrapper {
padding-bottom: calc(100% * (2 / 3));
}

View file

@ -1,3 +1,4 @@
/* eslint-disable no-console */
import React, { Component } from 'react';
import { findIndex, uniqueId } from 'lodash';
import { arrayMove } from 'react-sortable-hoc';
@ -65,6 +66,7 @@ class AddImagesTest extends Component {
images={this.state.images}
onSortEnd={this.onSortEnd}
savedImageAltText="Saved image"
onRemoveImage={imageId => console.log('remove image:', imageId)}
>
<div className={css.addImageWrapper}>
<div className={css.aspectRatioWrapper}>

View file

@ -15,6 +15,36 @@ import { Promised, ResponsiveImage } from '../../components';
import { uuid } from '../../util/propTypes';
import css from './AddImages.css';
const RemoveImageButton = props => {
const { onClick } = props;
return (
<button className={css.removeImage} onClick={onClick}>
<svg
width="10px"
height="10px"
viewBox="0 0 10 10"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<g strokeWidth="1" fillRule="evenodd">
<g transform="translate(-821.000000, -311.000000)">
<g transform="translate(809.000000, 299.000000)">
<path
d="M21.5833333,16.5833333 L17.4166667,16.5833333 L17.4166667,12.4170833 C17.4166667,12.1866667 17.2391667,12 17.00875,12 C16.77875,12 16.5920833,12.18625 16.5920833,12.41625 L16.5883333,16.5833333 L12.4166667,16.5833333 C12.18625,16.5833333 12,16.7695833 12,17 C12,17.23 12.18625,17.4166667 12.4166667,17.4166667 L16.5875,17.4166667 L16.5833333,21.5829167 C16.5829167,21.8129167 16.7691667,21.9995833 16.9991667,22 L16.9995833,22 C17.2295833,22 17.41625,21.81375 17.4166667,21.58375 L17.4166667,17.4166667 L21.5833333,17.4166667 C21.8133333,17.4166667 22,17.23 22,17 C22,16.7695833 21.8133333,16.5833333 21.5833333,16.5833333"
transform="translate(17.000000, 17.000000) rotate(-45.000000) translate(-17.000000, -17.000000) "
/>
</g>
</g>
</g>
</svg>
</button>
);
};
const { any, array, func, node, string, object } = PropTypes;
RemoveImageButton.propTypes = { onClick: func.isRequired };
// readImage returns a promise which is resolved
// when FileReader has loaded given file as dataURL
const readImage = file =>
@ -39,11 +69,18 @@ class Thumbnail extends Component {
}
render() {
const { className, file, id, imageId } = this.props;
const { className, file, id, imageId, onRemoveImage } = this.props;
const handleRemoveClick = e => {
e.preventDefault();
onRemoveImage(id);
};
// While image is uploading we show overlay on top of thumbnail
const uploadingOverlay = !imageId
? <div className={css.thumbnailLoading}><FormattedMessage id="AddImages.upload" /></div>
: null;
const removeButton = imageId ? <RemoveImageButton onClick={handleRemoveClick} /> : null;
const classes = classNames(css.thumbnail, className);
return (
<Promised
@ -55,6 +92,7 @@ class Thumbnail extends Component {
<div className={css.aspectWrapper}>
<img src={dataURL} alt={file.name} className={css.rootForImage} />
</div>
{removeButton}
{uploadingOverlay}
</div>
);
@ -69,20 +107,23 @@ class Thumbnail extends Component {
Thumbnail.defaultProps = { className: null, imageId: null };
const { any, array, func, node, string, object } = PropTypes;
Thumbnail.propTypes = {
className: string,
file: any.isRequired,
id: string.isRequired,
imageId: uuid,
onRemoveImage: func.isRequired,
};
const ThumbnailWrapper = props => {
const { className, image, savedImageAltText } = props;
const { className, image, savedImageAltText, onRemoveImage } = props;
if (image.file) {
return <Thumbnail className={className} {...image} />;
return <Thumbnail className={className} onRemoveImage={onRemoveImage} {...image} />;
} else {
const handleRemoveClick = e => {
e.preventDefault();
onRemoveImage(image.id);
};
const classes = classNames(css.thumbnail, className);
return (
<div className={classes}>
@ -99,6 +140,7 @@ const ThumbnailWrapper = props => {
sizes="100%"
/>
</div>
<RemoveImageButton onClick={handleRemoveClick} />
</div>
</div>
);
@ -111,6 +153,7 @@ ThumbnailWrapper.propTypes = {
className: string,
image: object.isRequired,
savedImageAltText: string.isRequired,
onRemoveImage: func.isRequired,
};
// Sorting is disabled temporarily.
@ -128,7 +171,14 @@ const SortableImage = ThumbnailWrapper;
// Create container where there are sortable images and passed children like "Add image" input etc.
const SortableImages = SortableContainer(props => {
const { children, className, thumbnailClassName, images, savedImageAltText } = props;
const {
children,
className,
thumbnailClassName,
images,
savedImageAltText,
onRemoveImage,
} = props;
const classes = classNames(css.root, className);
return (
<div className={classes}>
@ -140,6 +190,7 @@ const SortableImages = SortableContainer(props => {
key={image.id.uuid || image.id}
className={thumbnailClassName}
savedImageAltText={savedImageAltText}
onRemoveImage={onRemoveImage}
/>
);
})}
@ -164,6 +215,7 @@ AddImages.propTypes = {
thumbnailClassName: string,
onSortEnd: func.isRequired,
savedImageAltText: string.isRequired,
onRemoveImage: func.isRequired,
};
export default AddImages;

View file

@ -86,6 +86,7 @@ class EditListingPhotosPanel extends Component {
panelUpdated,
updateInProgress,
onChange,
onRemoveImage,
} = this.props;
const rootClass = rootClassName || css.root;
@ -113,6 +114,7 @@ class EditListingPhotosPanel extends Component {
onSubmit={this.handlePhotosSubmit}
onChange={onChange}
onUpdateImageOrder={onUpdateImageOrder}
onRemoveImage={onRemoveImage}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
@ -180,6 +182,7 @@ EditListingPhotosPanel.propTypes = {
onManageDisableScrolling: func.isRequired,
panelUpdated: bool.isRequired,
updateInProgress: bool.isRequired,
onRemoveImage: func.isRequired,
};
export default EditListingPhotosPanel;

View file

@ -22,6 +22,7 @@ export const NoPhotos = {
stripeConnected: true,
onImageUpload: noop,
onUpdateImageOrder: noop,
onRemoveImage: noop,
onUpdateListing: noop,
onCreateListing: noop,
onCreateListingDraft: noop,

View file

@ -85,6 +85,7 @@ const EditListingWizard = props => {
onImageUpload,
onPayoutDetailsSubmit,
onUpdateImageOrder,
onRemoveImage,
onUpdateListingDraft,
onChange,
rootClassName,
@ -217,6 +218,7 @@ const EditListingWizard = props => {
listing={listing}
images={images}
onImageUpload={onImageUpload}
onRemoveImage={onRemoveImage}
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
submitButtonText={submitText(intl, isNew, PHOTOS)}
onChange={onChange}
@ -286,6 +288,7 @@ EditListingWizard.propTypes = {
onImageUpload: func.isRequired,
onPayoutDetailsSubmit: func.isRequired,
onUpdateImageOrder: func.isRequired,
onRemoveImage: func.isRequired,
onUpdateListingDraft: func.isRequired,
onChange: func.isRequired,
rootClassName: string,

View file

@ -35,6 +35,8 @@ export const UPDATE_IMAGE_ORDER = 'app/EditListingPage/UPDATE_IMAGE_ORDER';
export const CREATE_LISTING_DRAFT = 'app/EditListingPage/CREATE_LISTING_DRAFT';
export const UPDATE_LISTING_DRAFT = 'app/EditListingPage/UPDATE_LISTING_DRAFT';
export const REMOVE_LISTING_IMAGE = 'app/EditListingPage/REMOVE_LISTING_IMAGE';
// ================ Reducer ================ //
const initialState = {
@ -47,6 +49,7 @@ const initialState = {
redirectToListing: false,
images: {},
imageOrder: [],
removedImageIds: [],
listingDraft: null,
updatedTab: null,
updateInProgress: false,
@ -133,6 +136,23 @@ export default function reducer(state = initialState, action = {}) {
return { ...state, listingDraft };
}
case REMOVE_LISTING_IMAGE: {
const id = payload.imageId;
// Only mark the image removed if it hasn't been added to the
// listing already
const removedImageIds = state.images[id]
? state.removedImageIds
: state.removedImageIds.concat(id);
// Always remove from the draft since it might be a new image to
// an existing listing.
const images = omit(state.images, id);
const imageOrder = state.imageOrder.filter(i => i !== id);
return { ...state, images, imageOrder, removedImageIds };
}
default:
return state;
}
@ -173,6 +193,11 @@ export const updateListingDraft = listingData => {
};
};
export const removeListingImage = imageId => ({
type: REMOVE_LISTING_IMAGE,
payload: { imageId },
});
// 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

@ -18,6 +18,7 @@ import {
requestUpdateListing,
requestImageUpload,
updateImageOrder,
removeListingImage,
loadData,
clearUpdatedTab,
} from './EditListingPage.duck';
@ -64,6 +65,7 @@ export const EditListingPageComponent = props => {
onUpdateListing,
onCreateListingDraft,
onImageUpload,
onRemoveListingImage,
onLogout,
onManageDisableScrolling,
onPayoutDetailsSubmit,
@ -107,7 +109,10 @@ export const EditListingPageComponent = props => {
// Images not yet connected to the listing
const unattachedImages = page.imageOrder.map(i => page.images[i]);
const images = currentListingImages.concat(unattachedImages);
const allImages = currentListingImages.concat(unattachedImages);
const images = allImages.filter(img => {
return !page.removedImageIds.includes(img.id);
});
const title = isNew
? intl.formatMessage({ id: 'EditListingPage.titleCreateListing' })
@ -150,6 +155,7 @@ export const EditListingPageComponent = props => {
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
onImageUpload={onImageUpload}
onUpdateImageOrder={onUpdateImageOrder}
onRemoveImage={onRemoveListingImage}
onChange={onChange}
currentUser={currentUser}
onManageDisableScrolling={onManageDisableScrolling}
@ -197,6 +203,7 @@ EditListingPageComponent.propTypes = {
onManageDisableScrolling: func.isRequired,
onPayoutDetailsSubmit: func.isRequired,
onUpdateImageOrder: func.isRequired,
onRemoveListingImage: func.isRequired,
onUpdateListingDraft: func.isRequired,
onUpdateListing: func.isRequired,
onChange: func.isRequired,
@ -259,6 +266,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(manageDisableScrolling(componentId, disableScrolling)),
onPayoutDetailsSubmit: values => dispatch(createStripeAccount(values)),
onUpdateImageOrder: imageOrder => dispatch(updateImageOrder(imageOrder)),
onRemoveListingImage: imageId => dispatch(removeListingImage(imageId)),
onUpdateListingDraft: values => dispatch(updateListingDraft(values)),
onChange: () => dispatch(clearUpdatedTab()),
});

View file

@ -28,6 +28,7 @@ describe('EditListingPageComponent', () => {
onUpdateListingDraft={noop}
onUpdateListing={noop}
onImageUpload={noop}
onRemoveListingImage={noop}
onManageDisableScrolling={noop}
onPayoutDetailsSubmit={noop}
onUpdateImageOrder={noop}

View file

@ -46,6 +46,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
onImageUpload={[Function]}
onManageDisableScrolling={[Function]}
onPayoutDetailsSubmit={[Function]}
onRemoveImage={[Function]}
onUpdateImageOrder={[Function]}
onUpdateListing={[Function]}
onUpdateListingDraft={[Function]}

View file

@ -18,6 +18,9 @@ export const Empty = {
onUpdateImageOrder: imageOrder => {
console.log('onUpdateImageOrder with new imageOrder:', imageOrder);
},
onRemoveImage: imageId => {
console.log('remove image:', imageId);
},
},
group: 'forms',
};

View file

@ -88,6 +88,7 @@ export class EditListingPhotosFormComponent extends Component {
updated,
updateError,
updateInProgress,
onRemoveImage,
} = this.props;
const chooseImageText = (
@ -150,6 +151,7 @@ export class EditListingPhotosFormComponent extends Component {
onSortEnd={this.onSortEnd}
thumbnailClassName={css.thumbnail}
savedImageAltText={intl.formatMessage({ id: 'EditListingPhotosForm.savedImageAltText' })}
onRemoveImage={onRemoveImage}
>
<Field
id="EditListingPhotosForm.AddImages"
@ -209,6 +211,7 @@ EditListingPhotosFormComponent.propTypes = {
updated: bool.isRequired,
updateError: instanceOf(Error),
updateInProgress: bool.isRequired,
onRemoveImage: func.isRequired,
};
const formName = 'EditListingPhotosForm';

View file

@ -24,6 +24,7 @@ describe('EditListingPhotosForm', () => {
stripeConnected={false}
updated={false}
updateInProgress={false}
onRemoveImage={noop}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -5,6 +5,7 @@ exports[`EditListingPhotosForm matches snapshot 1`] = `
<AddImages
className={null}
images={Array []}
onRemoveImage={[Function]}
onSortEnd={[Function]}
savedImageAltText="EditListingPhotosForm.savedImageAltText"
thumbnailClassName={null}>