mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
commit
f7aa288c19
12 changed files with 405 additions and 66 deletions
50
src/components/Promised/Promised.js
Normal file
50
src/components/Promised/Promised.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Promised component makes it easier to render content that
|
||||
* depends on resolution of a Promise.
|
||||
*
|
||||
* How to use:
|
||||
* <Promised promise={givenPromise} renderFulfilled={v => <b>{v}</b>} renderRejected={v => <b>v</b>} />
|
||||
*/
|
||||
|
||||
import { Component, PropTypes } from 'react';
|
||||
|
||||
class Promised extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// success value is string to be more useful when rendering texts.
|
||||
this.state = {
|
||||
value: '',
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.promise
|
||||
.then(value => {
|
||||
this.setState({ value });
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({ error });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { renderFulfilled, renderRejected } = this.props;
|
||||
return this.state.error ? renderRejected(this.state.error) : renderFulfilled(this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
Promised.defaultProps = { renderRejected: e => e };
|
||||
|
||||
const { func, shape } = PropTypes;
|
||||
|
||||
Promised.propTypes = {
|
||||
promise: shape({
|
||||
then: func.isRequired, // usually promises are detected from this single function alone
|
||||
}).isRequired,
|
||||
renderFulfilled: func.isRequired,
|
||||
renderRejected: func.isRequired,
|
||||
};
|
||||
|
||||
export default Promised;
|
||||
|
|
@ -11,6 +11,7 @@ import NamedRedirect from './NamedRedirect/NamedRedirect';
|
|||
import OrderDetailsPanel from './OrderDetailsPanel/OrderDetailsPanel';
|
||||
import OrderDiscussionPanel from './OrderDiscussionPanel/OrderDiscussionPanel';
|
||||
import PageLayout from './PageLayout/PageLayout';
|
||||
import Promised from './Promised/Promised';
|
||||
import RoutesProvider from './RoutesProvider/RoutesProvider';
|
||||
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ export {
|
|||
OrderDetailsPanel,
|
||||
OrderDiscussionPanel,
|
||||
PageLayout,
|
||||
Promised,
|
||||
RoutesProvider,
|
||||
SearchResultsPanel,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,61 @@
|
|||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.imagesContainer {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ import EditListingForm from './EditListingForm';
|
|||
export const Empty = {
|
||||
component: EditListingForm,
|
||||
props: {
|
||||
images: [],
|
||||
onImageUpload(values) {
|
||||
console.log(`onImageUpload with id (${values.id}) and file name (${values.file.name})`);
|
||||
},
|
||||
onSubmit(values) {
|
||||
console.log('submit new password form values:', values);
|
||||
console.log('Submit EditListingForm with (unformatted) values:', values);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
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 { maxLength, required } from '../../util/validators';
|
||||
import { Promised } from '../../components';
|
||||
import css from './EditListingForm.css';
|
||||
|
||||
const MAX_LENGTH = 60;
|
||||
const ACCEPT_IMAGES = 'image/*';
|
||||
const TITLE_MAX_LENGTH = 60;
|
||||
|
||||
const RenderField = ({ input, label, type, meta: { touched, error } }) => {
|
||||
// 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;
|
||||
const component = type === 'textarea'
|
||||
? <textarea {...input} placeholder={label} />
|
||||
: <input {...input} placeholder={label} type={type} />;
|
||||
|
|
@ -15,16 +33,19 @@ const RenderField = ({ input, label, type, meta: { touched, error } }) => {
|
|||
<label htmlFor={input.name}>{label}</label>
|
||||
<div>
|
||||
{component}
|
||||
{touched && (error && <span className={css.error}>{error}</span>)}
|
||||
{touched && error ? <span className={css.error}>{error}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { any, bool, shape, string } = PropTypes;
|
||||
const { bool, func, object, shape, string } = PropTypes;
|
||||
|
||||
RenderField.propTypes = {
|
||||
input: any.isRequired,
|
||||
input: shape({
|
||||
onChange: func.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
label: string.isRequired,
|
||||
type: string.isRequired,
|
||||
meta: shape({
|
||||
|
|
@ -33,11 +54,62 @@ RenderField.propTypes = {
|
|||
}).isRequired,
|
||||
};
|
||||
|
||||
// Add image wrapper. Label is the only visible element, file input is hidden.
|
||||
const RenderAddImage = props => {
|
||||
const { accept, input, label, type, meta } = props;
|
||||
const { name, onChange } = input;
|
||||
const { touched, warning } = meta;
|
||||
const inputProps = { accept, id: name, name, onChange, type };
|
||||
return (
|
||||
<div className={css.addImageWrapper}>
|
||||
<input {...inputProps} style={{ display: 'none' }} />
|
||||
<div>
|
||||
<label htmlFor={name} className={css.addImage}>{label}</label>
|
||||
{touched && warning ? <span className={css.warning}>{warning}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RenderAddImage.propTypes = {
|
||||
accept: string.isRequired,
|
||||
input: shape({
|
||||
value: object,
|
||||
onChange: func.isRequired,
|
||||
name: string.isRequired,
|
||||
}).isRequired,
|
||||
label: string.isRequired,
|
||||
type: string.isRequired,
|
||||
meta: shape({
|
||||
touched: bool,
|
||||
warning: string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
class EditListingForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleInitialize = this.handleInitialize.bind(this);
|
||||
this.onImageUploadHandler = this.onImageUploadHandler.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleInitialize();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!isEqual(this.props.images, nextProps.images)) {
|
||||
nextProps.change('images', nextProps.images);
|
||||
}
|
||||
}
|
||||
|
||||
onImageUploadHandler(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
this.props.onImageUpload({ id: `${file.name}_${Date.now()}`, file });
|
||||
}
|
||||
}
|
||||
|
||||
handleInitialize() {
|
||||
const { initData = {}, initialize } = this.props;
|
||||
initialize(initData);
|
||||
|
|
@ -47,6 +119,7 @@ class EditListingForm extends Component {
|
|||
const {
|
||||
disabled,
|
||||
handleSubmit,
|
||||
images,
|
||||
intl,
|
||||
pristine,
|
||||
saveActionMsg = 'Save listing',
|
||||
|
|
@ -54,9 +127,9 @@ class EditListingForm extends Component {
|
|||
} = this.props;
|
||||
const requiredStr = intl.formatMessage({ id: 'EditListingForm.required' });
|
||||
const maxLengthStr = intl.formatMessage({ id: 'EditListingForm.maxLength' }, {
|
||||
maxLength: MAX_LENGTH,
|
||||
maxLength: TITLE_MAX_LENGTH,
|
||||
});
|
||||
const maxLength60 = maxLength(maxLengthStr, MAX_LENGTH);
|
||||
const maxLength60 = maxLength(maxLengthStr, TITLE_MAX_LENGTH);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
|
@ -68,6 +141,48 @@ class EditListingForm extends Component {
|
|||
validate={[required(requiredStr), maxLength60]}
|
||||
/>
|
||||
|
||||
<h3>Images</h3>
|
||||
<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>}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Field
|
||||
accept={ACCEPT_IMAGES}
|
||||
component={RenderAddImage}
|
||||
label="+ Add image"
|
||||
name="addImage"
|
||||
onChange={this.onImageUploadHandler}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
component={props => {
|
||||
const { input, type } = props;
|
||||
return <input {...input} type={type} />;
|
||||
}}
|
||||
name="images"
|
||||
type="hidden"
|
||||
/>
|
||||
|
||||
<Field
|
||||
name="description"
|
||||
label="Description"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import EditListingForm from './EditListingForm';
|
|||
|
||||
describe('EditListingForm', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderDeep(<EditListingForm />);
|
||||
const tree = renderDeep(<EditListingForm images={[]} />);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,42 @@ exports[`EditListingForm matches snapshot 1`] = `
|
|||
value="" />
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
Images
|
||||
</h3>
|
||||
<div
|
||||
className={undefined}>
|
||||
<div
|
||||
className={undefined}>
|
||||
<input
|
||||
accept="image/*"
|
||||
id="addImage"
|
||||
name="addImage"
|
||||
onChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
type="file" />
|
||||
<div>
|
||||
<label
|
||||
className={undefined}
|
||||
htmlFor="addImage">
|
||||
+ Add image
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
name="images"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onDragStart={[Function]}
|
||||
onDrop={[Function]}
|
||||
onFocus={[Function]}
|
||||
type="hidden"
|
||||
value="" />
|
||||
<div>
|
||||
<label
|
||||
htmlFor="description">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ 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';
|
||||
|
||||
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';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -25,6 +29,8 @@ const initialState = {
|
|||
showListingsError: null,
|
||||
submittedListingId: null,
|
||||
redirectToListing: false,
|
||||
images: {},
|
||||
imageOrder: [],
|
||||
};
|
||||
|
||||
export default function reducer(state = initialState, action = {}) {
|
||||
|
|
@ -51,6 +57,32 @@ export default function reducer(state = initialState, action = {}) {
|
|||
console.error(payload);
|
||||
return { ...state, showListingsError: payload, redirectToListing: false };
|
||||
|
||||
case UPLOAD_IMAGE_REQUEST: {
|
||||
// payload.params: { id: 'tempId', file }
|
||||
const images = {
|
||||
...state.images,
|
||||
[payload.params.id]: { ...payload.params, uploadImageError: false },
|
||||
};
|
||||
return { ...state, images, imageOrder: state.imageOrder.concat([payload.params.id]) };
|
||||
}
|
||||
case UPLOAD_IMAGE_SUCCESS: {
|
||||
// payload.params: { id: 'tempId', imageId: 'some-real-id'}
|
||||
const { id, imageId } = payload;
|
||||
const file = state.images[id].file;
|
||||
const images = { ...state.images, [id]: { id, imageId, file, uploadImageError: false } };
|
||||
return { ...state, images };
|
||||
}
|
||||
case UPLOAD_IMAGE_ERROR: {
|
||||
console.error(payload);
|
||||
const { id, error } = payload;
|
||||
const { file } = state.images[id];
|
||||
const images = {
|
||||
...state.images,
|
||||
[id]: { id, file, imageId: null, uploadImageError: error },
|
||||
};
|
||||
return { ...state, images };
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
@ -74,58 +106,54 @@ export const showListings = requestAction(SHOW_LISTINGS_REQUEST);
|
|||
export const showListingsSuccess = successAction(SHOW_LISTINGS_SUCCESS);
|
||||
export const showListingsError = errorAction(SHOW_LISTINGS_ERROR);
|
||||
|
||||
// SDK method: listings.uploadImage
|
||||
export const uploadImage = requestAction(UPLOAD_IMAGE_REQUEST);
|
||||
export const uploadImageSuccess = successAction(UPLOAD_IMAGE_SUCCESS);
|
||||
export const uploadImageError = errorAction(UPLOAD_IMAGE_ERROR);
|
||||
|
||||
// ================ Thunk ================ //
|
||||
|
||||
const endpoints = {
|
||||
[CREATE_LISTING_REQUEST]: {
|
||||
method: sdk => sdk.listings.create,
|
||||
success: createListingSuccess,
|
||||
error: createListingError,
|
||||
},
|
||||
[SHOW_LISTINGS_REQUEST]: {
|
||||
method: sdk => sdk.listings.show,
|
||||
success: showListingsSuccess,
|
||||
error: showListingsError,
|
||||
},
|
||||
};
|
||||
|
||||
function callEndpoint(sdkMethod, dispatchSuccess, dispatchError, action) {
|
||||
const params = action.payload.params;
|
||||
return sdkMethod(params).then(resp => dispatchSuccess(resp)).catch(e => dispatchError(e));
|
||||
export function requestShowListing(actionPayload) {
|
||||
return (dispatch, getState, sdk) => {
|
||||
dispatch(showListings(actionPayload));
|
||||
return sdk.listings
|
||||
.show(actionPayload)
|
||||
.then(response => {
|
||||
// EditListingPage fetches new listing data, which also needs to be added to global data
|
||||
dispatch(globalShowListingsSuccess(response));
|
||||
// In case of success, we'll clear state.EditListingPage (user will be redirected away)
|
||||
dispatch(showListingsSuccess(response));
|
||||
return response;
|
||||
})
|
||||
.catch(e => dispatch(showListingsError(e)));
|
||||
};
|
||||
}
|
||||
|
||||
export function requestCreateListing(actionPayload) {
|
||||
return (dispatch, getState, sdk) => {
|
||||
// Create request action
|
||||
const action = createListing(actionPayload);
|
||||
// Notify store that we are sending a request
|
||||
dispatch(action);
|
||||
// Select SDK endpoint and call it
|
||||
const { method, success, error } = endpoints[action.type];
|
||||
return callEndpoint(
|
||||
method(sdk),
|
||||
response => dispatch(success(response)),
|
||||
e => dispatch(error(e)),
|
||||
action,
|
||||
);
|
||||
dispatch(createListing(actionPayload));
|
||||
return sdk.listings
|
||||
.create(actionPayload)
|
||||
.then(response => {
|
||||
const id = response.data.data.id.uuid;
|
||||
// Modify store to understand that we have created listing and can redirect away
|
||||
dispatch(createListingSuccess(response));
|
||||
// Fetch listing data so that redirection is smooth
|
||||
dispatch(requestShowListing({ id, include: ['author', 'images'] }));
|
||||
return response;
|
||||
})
|
||||
.catch(e => dispatch(createListingError(e)));
|
||||
};
|
||||
}
|
||||
|
||||
export function requestShowListing(actionPayload) {
|
||||
// Images return imageId which we need to map with previously generated temporary id
|
||||
export function requestImageUpload(actionPayload) {
|
||||
return (dispatch, getState, sdk) => {
|
||||
const action = showListings(actionPayload);
|
||||
dispatch(action);
|
||||
const { method, success, error } = endpoints[action.type];
|
||||
return callEndpoint(
|
||||
method(sdk),
|
||||
response => {
|
||||
// EditListingPage fetches new listing data, which also needs to be added to global data
|
||||
dispatch(globalShowListingsSuccess(response));
|
||||
// In case of success, we'll clear state.EditListingPage (user will be redirected away)
|
||||
dispatch(success(response));
|
||||
},
|
||||
e => dispatch(error(e)),
|
||||
action,
|
||||
);
|
||||
const id = actionPayload.id;
|
||||
dispatch(uploadImage(actionPayload));
|
||||
return sdk.listings
|
||||
.uploadImage({ image: actionPayload.file })
|
||||
.then(resp => dispatch(uploadImageSuccess({ data: { id, imageId: resp.data.data.id } })))
|
||||
.catch(e => dispatch(uploadImageError({ data: { id, error: e } })));
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,27 +6,28 @@ import { NamedRedirect, PageLayout } from '../../components';
|
|||
import { EditListingForm } from '../../containers';
|
||||
import { getListingsById } from '../../ducks/sdk.duck';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import { requestCreateListing, requestShowListing } from './EditListingPage.duck';
|
||||
import {
|
||||
requestCreateListing,
|
||||
requestShowListing,
|
||||
requestImageUpload,
|
||||
} from './EditListingPage.duck';
|
||||
|
||||
const formatRequestData = values => {
|
||||
const { title, description } = values;
|
||||
const { description, images, title } = values;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
address: 'Bulevardi 14, 00200 Helsinki, Finland',
|
||||
description,
|
||||
geolocation: {
|
||||
lat: 40.6,
|
||||
lng: 73.9,
|
||||
},
|
||||
images: images.map(i => i.imageId),
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
const onSubmit = (submitListing, onLoadListing) => {
|
||||
return values =>
|
||||
submitListing(values).then(resp => onLoadListing(resp.payload.data.id.uuid)).catch(e => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
const onSubmit = submitListing => {
|
||||
return values => submitListing(values);
|
||||
};
|
||||
|
||||
// N.B. All the presentational content needs to be extracted to their own components
|
||||
|
|
@ -38,9 +39,9 @@ export class EditListingPageComponent extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { data, intl, onCreateListing, onLoadListing, page, params, type } = this.props;
|
||||
const { data, intl, onCreateListing, onImageUpload, page, params, type } = this.props;
|
||||
const isNew = type === 'new';
|
||||
const id = page.submittedListingId || new types.UUID(params.id);
|
||||
const id = page.submittedListingId || (params && new types.UUID(params.id));
|
||||
const listingsById = getListingsById(data, [id]);
|
||||
const currentListing = listingsById.length > 0 ? listingsById[0] : null;
|
||||
|
||||
|
|
@ -48,9 +49,12 @@ export class EditListingPageComponent extends Component {
|
|||
const showForm = isNew || currentListing;
|
||||
|
||||
if (shouldRedirect) {
|
||||
// If page has already listingId (after submit) and current listings exist
|
||||
// redirect to listing page
|
||||
const slug = currentListing ? createSlug(currentListing.attributes.title) : null;
|
||||
return <NamedRedirect name="ListingPage" params={{ id: id.uuid, slug }} />;
|
||||
} else if (showForm) {
|
||||
// Show form if user is posting a new listing or editing existing one
|
||||
const saveActionMsg = page.redirectToListing ? 'Redirecting to listing' : 'Create listing';
|
||||
const disableForm = page.redirectToListing && !page.showListingsError;
|
||||
|
||||
|
|
@ -62,17 +66,24 @@ export class EditListingPageComponent extends Component {
|
|||
}
|
||||
: {};
|
||||
|
||||
// Images are passed to EditListingForm so that it can generate thumbnails out of them
|
||||
const images = page.imageOrder.map(i => page.images[i]);
|
||||
|
||||
return (
|
||||
<PageLayout title={'Edit listing'}>
|
||||
<EditListingForm
|
||||
onSubmit={onSubmit(onCreateListing, onLoadListing)}
|
||||
saveActionMsg={saveActionMsg}
|
||||
disabled={disableForm}
|
||||
images={images}
|
||||
initData={initData}
|
||||
onImageUpload={onImageUpload}
|
||||
onSubmit={onSubmit(onCreateListing)}
|
||||
saveActionMsg={saveActionMsg}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
} else {
|
||||
// If user has come to this page through a direct linkto edit existing listing,
|
||||
// we need to load it first.
|
||||
const loadingPageMsg = {
|
||||
id: 'ListingPage.loadingListingData',
|
||||
};
|
||||
|
|
@ -92,6 +103,7 @@ const { func, object, shape, string } = PropTypes;
|
|||
EditListingPageComponent.propTypes = {
|
||||
data: object.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onImageUpload: func.isRequired,
|
||||
onLoadListing: func.isRequired,
|
||||
onCreateListing: func.isRequired,
|
||||
page: object.isRequired,
|
||||
|
|
@ -111,6 +123,7 @@ const mapDispatchToProps = dispatch => {
|
|||
return {
|
||||
onLoadListing: id => dispatch(requestShowListing({ id, include: ['author', 'images'] })),
|
||||
onCreateListing: values => dispatch(requestCreateListing(formatRequestData(values))),
|
||||
onImageUpload: data => dispatch(requestImageUpload(data)),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
21
src/containers/EditListingPage/EditListingPage.test.js
Normal file
21
src/containers/EditListingPage/EditListingPage.test.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import { fakeIntl, renderShallow } from '../../util/test-helpers';
|
||||
import { EditListingPageComponent } from './EditListingPage';
|
||||
|
||||
describe('EditListingPageComponent', () => {
|
||||
it('matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<EditListingPageComponent
|
||||
data={{ entities: {} }}
|
||||
images={[]}
|
||||
intl={fakeIntl}
|
||||
onCreateListing={v => v}
|
||||
onLoadListing={v => v}
|
||||
onImageUpload={v => v}
|
||||
page={{ imageOrder: [], images: {} }}
|
||||
type="new"
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
exports[`EditListingPageComponent matches snapshot 1`] = `
|
||||
<Connect(withRouter(PageLayout))
|
||||
title="Edit listing">
|
||||
<ReduxForm
|
||||
images={Array []}
|
||||
initData={Object {}}
|
||||
onImageUpload={[Function]}
|
||||
onSubmit={[Function]}
|
||||
saveActionMsg="Create listing" />
|
||||
</Connect(withRouter(PageLayout))>
|
||||
`;
|
||||
|
|
@ -272,6 +272,7 @@ describe('data utils', () => {
|
|||
listing2,
|
||||
]);
|
||||
});
|
||||
|
||||
it('denormalises multiple relationships when relationship data is empty', () => {
|
||||
const user1 = createUser('user1');
|
||||
const listing1 = createListing('listing1');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue