mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-28 03:43:28 +10:00
Merge pull request #345 from sharetribe/remove-listing-images
Remove listing images
This commit is contained in:
commit
f80689fbb0
14 changed files with 140 additions and 7 deletions
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export const NoPhotos = {
|
|||
stripeConnected: true,
|
||||
onImageUpload: noop,
|
||||
onUpdateImageOrder: noop,
|
||||
onRemoveImage: noop,
|
||||
onUpdateListing: noop,
|
||||
onCreateListing: noop,
|
||||
onCreateListingDraft: noop,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ describe('EditListingPageComponent', () => {
|
|||
onUpdateListingDraft={noop}
|
||||
onUpdateListing={noop}
|
||||
onImageUpload={noop}
|
||||
onRemoveListingImage={noop}
|
||||
onManageDisableScrolling={noop}
|
||||
onPayoutDetailsSubmit={noop}
|
||||
onUpdateImageOrder={noop}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
|
|||
onImageUpload={[Function]}
|
||||
onManageDisableScrolling={[Function]}
|
||||
onPayoutDetailsSubmit={[Function]}
|
||||
onRemoveImage={[Function]}
|
||||
onUpdateImageOrder={[Function]}
|
||||
onUpdateListing={[Function]}
|
||||
onUpdateListingDraft={[Function]}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ describe('EditListingPhotosForm', () => {
|
|||
stripeConnected={false}
|
||||
updated={false}
|
||||
updateInProgress={false}
|
||||
onRemoveImage={noop}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ exports[`EditListingPhotosForm matches snapshot 1`] = `
|
|||
<AddImages
|
||||
className={null}
|
||||
images={Array []}
|
||||
onRemoveImage={[Function]}
|
||||
onSortEnd={[Function]}
|
||||
savedImageAltText="EditListingPhotosForm.savedImageAltText"
|
||||
thumbnailClassName={null}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue