mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 13:06:03 +10:00
Initial client side data loading for ListingPage
This commit is contained in:
parent
a9e5f98476
commit
735adb21d5
6 changed files with 198 additions and 54 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
|
|
@ -7,29 +7,80 @@ import { NamedRedirect } from './components';
|
|||
import { withFlattenedRoutes } from './util/routes';
|
||||
import * as propTypes from './util/propTypes';
|
||||
|
||||
const Routes = props => {
|
||||
const { isAuthenticated, flattenedRoutes, staticContext } = props;
|
||||
const { bool, arrayOf, object, func, shape, string, any } = PropTypes;
|
||||
|
||||
const renderComponent = (route, matchProps) => {
|
||||
const { auth, component: RouteComponent } = route;
|
||||
const { match, location } = matchProps;
|
||||
const canShowComponent = !auth || isAuthenticated;
|
||||
if (!canShowComponent) {
|
||||
class RouteComponentRenderer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.canShowComponent = this.canShowComponent.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
const { match, route, dispatch } = this.props;
|
||||
const { loadData, name } = route;
|
||||
const shouldLoadData = typeof loadData === 'function' && this.canShowComponent();
|
||||
|
||||
if (shouldLoadData) {
|
||||
dispatch(loadData(match.params, {}))
|
||||
.then(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`loadData success for ${name} route`);
|
||||
})
|
||||
.catch(e => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`loadData error for ${name} route`, e);
|
||||
});
|
||||
}
|
||||
}
|
||||
canShowComponent() {
|
||||
const { isAuthenticated, route } = this.props;
|
||||
const { auth } = route;
|
||||
return !auth || isAuthenticated;
|
||||
}
|
||||
render() {
|
||||
const { route, match, location, staticContext } = this.props;
|
||||
const { component: RouteComponent } = route;
|
||||
const canShow = this.canShowComponent();
|
||||
if (!canShow) {
|
||||
staticContext.forbidden = true;
|
||||
}
|
||||
return canShowComponent
|
||||
return canShow
|
||||
? <RouteComponent params={match.params} location={location} />
|
||||
: <NamedRedirect name="LogInPage" state={{ from: match.url }} />;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const toRouteComponent = route => (
|
||||
<Route
|
||||
key={route.name}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
render={matchProps => renderComponent(route, matchProps)}
|
||||
/>
|
||||
);
|
||||
RouteComponentRenderer.propTypes = {
|
||||
isAuthenticated: bool.isRequired,
|
||||
route: propTypes.route.isRequired,
|
||||
match: shape({
|
||||
params: object.isRequired,
|
||||
url: string.isRequired,
|
||||
}).isRequired,
|
||||
location: any.isRequired,
|
||||
staticContext: object.isRequired,
|
||||
dispatch: func.isRequired,
|
||||
};
|
||||
|
||||
const Routes = props => {
|
||||
const { isAuthenticated, flattenedRoutes, staticContext, dispatch } = props;
|
||||
|
||||
const toRouteComponent = route => {
|
||||
const renderProps = { isAuthenticated, route, staticContext, dispatch };
|
||||
return (
|
||||
<Route
|
||||
key={route.name}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
render={matchProps => (
|
||||
<RouteComponentRenderer
|
||||
{...renderProps}
|
||||
match={matchProps.match}
|
||||
location={matchProps.location}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
|
|
@ -41,12 +92,11 @@ const Routes = props => {
|
|||
|
||||
Routes.defaultProps = { staticContext: {} };
|
||||
|
||||
const { bool, arrayOf, object } = PropTypes;
|
||||
|
||||
Routes.propTypes = {
|
||||
isAuthenticated: bool.isRequired,
|
||||
flattenedRoutes: arrayOf(propTypes.route).isRequired,
|
||||
staticContext: object,
|
||||
dispatch: func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({ isAuthenticated: state.Auth.isAuthenticated });
|
||||
|
|
|
|||
55
src/containers/ListingPage/ListingPage.duck.js
Normal file
55
src/containers/ListingPage/ListingPage.duck.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { showListingsSuccess } from '../../ducks/sdk.duck';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const SHOW_LISTING_REQUEST = 'app/ListingPage/SHOW_LISTING_REQUEST';
|
||||
export const SHOW_LISTING_ERROR = 'app/ListingPage/SHOW_LISTING_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
id: null,
|
||||
showListingError: null,
|
||||
};
|
||||
|
||||
const listingPageReducer = (state = initialState, action = {}) => {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case SHOW_LISTING_REQUEST:
|
||||
return { id: payload.id, showListingError: null };
|
||||
case SHOW_LISTING_ERROR:
|
||||
return { showListingError: payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default listingPageReducer;
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const showListingRequest = id => ({
|
||||
type: SHOW_LISTING_REQUEST,
|
||||
payload: { id },
|
||||
});
|
||||
|
||||
export const showListingError = e => ({
|
||||
type: SHOW_LISTING_ERROR,
|
||||
error: true,
|
||||
payload: e,
|
||||
});
|
||||
|
||||
export const showListing = listingId =>
|
||||
(dispatch, getState, sdk) => {
|
||||
dispatch(showListingRequest(listingId));
|
||||
return sdk.listings
|
||||
.show({ id: listingId, include: ['author', 'images'] })
|
||||
.then(data => {
|
||||
dispatch(showListingsSuccess(data));
|
||||
return data;
|
||||
})
|
||||
.catch(e => {
|
||||
dispatch(showListingError(e));
|
||||
throw e;
|
||||
});
|
||||
};
|
||||
|
|
@ -3,9 +3,12 @@ import { intlShape, injectIntl } from 'react-intl';
|
|||
import { connect } from 'react-redux';
|
||||
import { types } from 'sharetribe-sdk';
|
||||
import { NamedLink, PageLayout } from '../../components';
|
||||
import { showListings, getListingsById } from '../../ducks/sdk.duck';
|
||||
import { getListingsById } from '../../ducks/sdk.duck';
|
||||
import { showListing } from './ListingPage.duck';
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const { UUID } = types;
|
||||
|
||||
// TODO this hard coded info needs to be removed when API supports these features
|
||||
const info = {
|
||||
//title: 'Banyan Studios',
|
||||
|
|
@ -45,14 +48,10 @@ const info = {
|
|||
|
||||
// N.B. All the presentational content needs to be extracted to their own components
|
||||
export class ListingPageComponent extends Component {
|
||||
componentDidMount() {
|
||||
ListingPageComponent.loadData(this.props.params.id, this.props.onLoadListing);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { entitiesData, intl, params } = this.props;
|
||||
const id = new types.UUID(params.id);
|
||||
const listingsById = getListingsById(entitiesData, [id]);
|
||||
const { params, marketplaceData, showListingError, intl } = this.props;
|
||||
const id = new UUID(params.id);
|
||||
const listingsById = getListingsById(marketplaceData, [id]);
|
||||
const currentListing = listingsById.length > 0 ? listingsById[0] : null;
|
||||
|
||||
const title = currentListing ? currentListing.attributes.title : '';
|
||||
|
|
@ -140,40 +139,35 @@ export class ListingPageComponent extends Component {
|
|||
|
||||
const noDataMsg = { id: 'ListingPage.noListingData' };
|
||||
const noDataError = <PageLayout title={intl.formatMessage(noDataMsg)} />;
|
||||
const loadingOrError = entitiesData.showListingsError ? noDataError : loadingContent;
|
||||
const loadingOrError = showListingError ? noDataError : loadingContent;
|
||||
|
||||
return currentListing ? pageContent : loadingOrError;
|
||||
}
|
||||
}
|
||||
|
||||
ListingPageComponent.loadData = (id, onLoadListing) => {
|
||||
onLoadListing(id);
|
||||
};
|
||||
ListingPageComponent.defaultProps = { showListingError: null };
|
||||
|
||||
ListingPageComponent.defaultProps = { listing: null };
|
||||
|
||||
const { func, object, shape, string } = PropTypes;
|
||||
const { shape, string, object, instanceOf } = PropTypes;
|
||||
|
||||
ListingPageComponent.propTypes = {
|
||||
entitiesData: object.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
onLoadListing: func.isRequired,
|
||||
params: shape({
|
||||
id: string.isRequired,
|
||||
slug: string.isRequired,
|
||||
}).isRequired,
|
||||
marketplaceData: object.isRequired,
|
||||
showListingError: instanceOf(Error),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { data, ListingPage } = state;
|
||||
const entitiesData = data || {};
|
||||
return { ListingPage, entitiesData };
|
||||
const mapStateToProps = state => ({
|
||||
marketplaceData: state.data,
|
||||
showListingError: state.ListingPage.showListingError,
|
||||
});
|
||||
|
||||
const ListingPage = connect(mapStateToProps)(injectIntl(ListingPageComponent));
|
||||
|
||||
ListingPage.loadData = params => {
|
||||
return showListing(new UUID(params.id));
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onLoadListing: id => dispatch(showListings({ id, include: ['author', 'images'] })),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListingPageComponent));
|
||||
export default ListingPage;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,64 @@
|
|||
import React from 'react';
|
||||
import { types } from 'sharetribe-sdk';
|
||||
import { createListing } from '../../util/test-data';
|
||||
import { fakeIntl, renderShallow } from '../../util/test-helpers';
|
||||
import { ListingPageComponent } from './ListingPage';
|
||||
import { showListingsSuccess } from '../../ducks/sdk.duck';
|
||||
import { showListingRequest, showListingError, showListing } from './ListingPage.duck';
|
||||
|
||||
const { UUID } = types;
|
||||
|
||||
describe('ListingPage', () => {
|
||||
it('matches snapshot', () => {
|
||||
const entitiesData = { entities: { listing: { listing1: createListing('listing1') } } };
|
||||
const marketplaceData = { entities: { listing: { listing1: createListing('listing1') } } };
|
||||
const tree = renderShallow(
|
||||
<ListingPageComponent
|
||||
params={{ slug: 'listing1-title', id: 'listing1' }}
|
||||
entitiesData={entitiesData}
|
||||
marketplaceData={marketplaceData}
|
||||
intl={fakeIntl}
|
||||
onLoadListing={l => l}
|
||||
/>,
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Duck', () => {
|
||||
it('showListing() success', () => {
|
||||
const id = new UUID('00000000-0000-0000-0000-000000000000');
|
||||
const dispatch = jest.fn(action => action);
|
||||
const response = { status: 200 };
|
||||
const show = jest.fn(() => Promise.resolve(response));
|
||||
const sdk = { listings: { show } };
|
||||
|
||||
return showListing(id)(dispatch, null, sdk).then(data => {
|
||||
expect(data).toEqual(response);
|
||||
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'images'] }]]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
[showListingRequest(id)],
|
||||
[showListingsSuccess(data)],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('showListing() error', () => {
|
||||
const id = new UUID('00000000-0000-0000-0000-000000000000');
|
||||
const dispatch = jest.fn(action => action);
|
||||
const error = new Error('fail');
|
||||
const show = jest.fn(() => Promise.reject(error));
|
||||
const sdk = { listings: { show } };
|
||||
|
||||
// Calling sdk.listings.show is expected to fail now
|
||||
|
||||
return showListing(id)(dispatch, null, sdk).then(
|
||||
() => {
|
||||
throw new Error('sdk.listings.show was supposed to fail!');
|
||||
},
|
||||
e => {
|
||||
expect(e).toEqual(error);
|
||||
expect(show.mock.calls).toEqual([[{ id, include: ['author', 'images'] }]]);
|
||||
expect(dispatch.mock.calls).toEqual([[showListingRequest(id)], [showListingError(e)]]);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
* We are following Ducks module proposition:
|
||||
* https://github.com/erikras/ducks-modular-redux
|
||||
*/
|
||||
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import EditListingPage from './EditListingPage/EditListingPage.duck';
|
||||
import ListingPage from './ListingPage/ListingPage.duck';
|
||||
import SearchPage from './SearchPage/SearchPage.duck';
|
||||
|
||||
export { EditListingPage, SearchPage };
|
||||
export { EditListingPage, ListingPage, SearchPage };
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const routesConfiguration = [
|
|||
path: '/l/:slug/:id',
|
||||
exact: true,
|
||||
name: 'ListingPage',
|
||||
loadData: (params, search) => ListingPage.loadData(params, search),
|
||||
component: props => <ListingPage {...props} />,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue