Merge pull request #484 from sharetribe/refactor-routesconfiguration

Refactor routesconfiguration
This commit is contained in:
Vesa Luusua 2017-10-11 14:23:43 +03:00 committed by GitHub
commit d5bb389dc6
56 changed files with 706 additions and 1052 deletions

View file

@ -4,7 +4,7 @@ const log = require('./log');
exports.loadData = function(requestUrl, sdk) {
const { pathname, query } = url.parse(requestUrl);
const matchedRoutes = matchPathname(pathname, routeConfiguration);
const matchedRoutes = matchPathname(pathname, routeConfiguration());
const store = configureStore(sdk);

View file

@ -2,13 +2,13 @@ import React, { Component, PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { Switch, Route, withRouter } from 'react-router-dom';
import routeConfiguration from './routeConfiguration';
import { NotFoundPage } from './containers';
import { NamedRedirect } from './components';
import { withFlattenedRoutes } from './util/contextHelpers';
import * as propTypes from './util/propTypes';
import * as log from './util/log';
const { bool, arrayOf, object, func, shape, string, any } = PropTypes;
const { bool, object, func, shape, string } = PropTypes;
const canShowComponent = props => {
const { isAuthenticated, route } = props;
@ -49,18 +49,14 @@ class RouteComponentRenderer extends Component {
}
render() {
const { route, match, location, staticContext, flattenedRoutes } = this.props;
const { route, match, location, staticContext } = this.props;
const { component: RouteComponent, authPage = 'SignupPage' } = route;
const canShow = canShowComponent(this.props);
if (!canShow) {
staticContext.unauthorized = true;
}
return canShow
? <RouteComponent
params={match.params}
location={location}
flattenedRoutes={flattenedRoutes}
/>
? <RouteComponent params={match.params} location={location} />
: <NamedRedirect
name={authPage}
state={{ from: `${location.pathname}${location.search}${location.hash}` }}
@ -71,7 +67,6 @@ class RouteComponentRenderer extends Component {
RouteComponentRenderer.propTypes = {
isAuthenticated: bool.isRequired, // eslint-disable-line react/no-unused-prop-types
logoutInProgress: bool.isRequired, // eslint-disable-line react/no-unused-prop-types
flattenedRoutes: any.isRequired,
route: propTypes.route.isRequired,
match: shape({
params: object.isRequired,
@ -86,7 +81,7 @@ RouteComponentRenderer.propTypes = {
};
const Routes = props => {
const { isAuthenticated, logoutInProgress, flattenedRoutes, staticContext, dispatch } = props;
const { isAuthenticated, logoutInProgress, staticContext, dispatch } = props;
const toRouteComponent = route => {
const renderProps = {
@ -95,7 +90,6 @@ const Routes = props => {
route,
staticContext,
dispatch,
flattenedRoutes,
};
return (
<Route
@ -115,7 +109,7 @@ const Routes = props => {
return (
<Switch>
{flattenedRoutes.map(toRouteComponent)}
{routeConfiguration().map(toRouteComponent)}
<Route component={NotFoundPage} />
</Switch>
);
@ -127,9 +121,6 @@ Routes.propTypes = {
isAuthenticated: bool.isRequired,
logoutInProgress: bool.isRequired,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withRouter
staticContext: object,
@ -148,4 +139,4 @@ const mapStateToProps = state => {
// lifecycle hook.
//
// See: https://github.com/ReactTraining/react-router/issues/4671
export default compose(withRouter, connect(mapStateToProps), withFlattenedRoutes)(Routes);
export default compose(withRouter, connect(mapStateToProps))(Routes);

View file

@ -7,23 +7,17 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import en from 'react-intl/locale-data/en';
import configureStore from './store';
import Routes from './Routes';
import { RoutesProvider } from './components';
import routesConfiguration from './routesConfiguration';
import { flattenRoutes } from './util/routes';
import localeData from './translations/en.json';
export const ClientApp = props => {
const { store } = props;
addLocaleData([...en]);
const flattenedRoutes = flattenRoutes(routesConfiguration);
return (
<IntlProvider locale="en" messages={localeData}>
<Provider store={store}>
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<BrowserRouter>
<Routes />
</BrowserRouter>
</RoutesProvider>
<BrowserRouter>
<Routes />
</BrowserRouter>
</Provider>
</IntlProvider>
);
@ -36,15 +30,12 @@ ClientApp.propTypes = { store: any.isRequired };
export const ServerApp = props => {
const { url, context, store } = props;
addLocaleData([...en]);
const flattenedRoutes = flattenRoutes(routesConfiguration);
return (
<IntlProvider locale="en" messages={localeData}>
<Provider store={store}>
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<StaticRouter location={url} context={context}>
<Routes />
</StaticRouter>
</RoutesProvider>
<StaticRouter location={url} context={context}>
<Routes />
</StaticRouter>
</Provider>
</IntlProvider>
);

View file

@ -1,6 +1,4 @@
import { createListing } from '../../util/test-data';
import { flattenRoutes } from '../../util/routes';
import routesConfiguration from '../../routesConfiguration';
import EditListingWizard from './EditListingWizard';
const noop = () => null;
@ -16,7 +14,6 @@ export const NoPhotos = {
},
fetchInProgress: false,
newListingCreated: false,
flattenedRoutes: flattenRoutes(routesConfiguration),
history: { push: noop },
images: [],
listing: createListing('listing1'),

View file

@ -1,6 +1,7 @@
import React, { PropTypes } from 'react';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import routeConfiguration from '../../routeConfiguration';
import * as propTypes from '../../util/propTypes';
import { ensureListing } from '../../util/data';
import { createResourceLocatorString } from '../../util/routes';
@ -76,7 +77,6 @@ const EditListingWizard = props => {
errors,
fetchInProgress,
newListingCreated,
flattenedRoutes,
history,
images,
listing,
@ -142,7 +142,7 @@ const EditListingWizard = props => {
const pathParams = tabParams(LOCATION);
// Redirect to location tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(DESCRIPTION, values);
@ -175,7 +175,7 @@ const EditListingWizard = props => {
const pathParams = tabParams(PRICING);
// Redirect to pricing tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(LOCATION, updateValues);
@ -200,7 +200,7 @@ const EditListingWizard = props => {
const pathParams = tabParams(PHOTOS);
// Redirect to photos tab
history.push(
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
update(PRICING, values);
@ -253,7 +253,7 @@ EditListingWizard.defaultProps = {
updatedTab: null,
};
const { array, arrayOf, bool, func, object, oneOf, shape, string } = PropTypes;
const { array, bool, func, object, oneOf, shape, string } = PropTypes;
EditListingWizard.propTypes = {
className: string,
@ -271,7 +271,6 @@ EditListingWizard.propTypes = {
}).isRequired,
fetchInProgress: bool.isRequired,
newListingCreated: bool.isRequired,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
history: shape({
push: func.isRequired,
}).isRequired,

View file

@ -3,13 +3,13 @@ exports[`FilterPanel matches snapshot 1`] = `
<h1>
Filters
</h1>
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
name="SearchListingsPage">
See studios
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
name="SearchListingsPage">
X
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</div>
`;

View file

@ -1,16 +1,16 @@
import React, { PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { IconSearch, Button } from '../../components';
import { LocationSearchForm } from '../../containers';
import { stringify } from '../../util/urlHelpers';
import { createResourceLocatorString } from '../../util/routes';
import * as propTypes from '../../util/propTypes';
import routeConfiguration from '../../routeConfiguration';
import { IconSearch, Button } from '../../components';
import { LocationSearchForm } from '../../containers';
import css from './HeroSection.css';
const HeroSection = props => {
const { rootClassName, className, flattenedRoutes, history, location } = props;
const { rootClassName, className, history, location } = props;
const handleMobileSearchClick = () => {
const params = { mobilesearch: 'open' };
@ -22,7 +22,7 @@ const HeroSection = props => {
const { search, selectedPlace } = values.location;
const { origin, bounds, country } = selectedPlace;
const searchParams = { address: search, origin, bounds, country };
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, searchParams));
};
const classes = classNames(rootClassName || css.root, className);
@ -46,13 +46,12 @@ const HeroSection = props => {
HeroSection.defaultProps = { rootClassName: null, className: null };
const { string, shape, func, arrayOf } = PropTypes;
const { string, shape, func } = PropTypes;
HeroSection.propTypes = {
rootClassName: string,
className: string,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
history: shape({
push: func.isRequired,
}).isRequired,

View file

@ -8,7 +8,6 @@ describe('HeroSection', () => {
it('matches snapshot', () => {
window.google = { maps: {} };
const heroProps = {
flattenedRoutes: [],
history: { push: noop },
location: { search: '' },
};

View file

@ -1,5 +1,5 @@
exports[`ListingCard matches snapshot 1`] = `
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
className=""
name="ListingPage"
params={
@ -58,5 +58,5 @@ exports[`ListingCard matches snapshot 1`] = `
</div>
</div>
</div>
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
`;

View file

@ -22,7 +22,6 @@ export const ManageListingCardWrapped = {
onCloseListing: noop,
onOpenListing: noop,
onToggleMenu: noop,
flattenedRoutes: [],
history: { push: noop },
},
};

View file

@ -3,11 +3,11 @@ import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import routeConfiguration from '../../routeConfiguration';
import * as propTypes from '../../util/propTypes';
import { formatMoney } from '../../util/currency';
import { ensureListing } from '../../util/data';
import { createSlug } from '../../util/urlHelpers';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { createResourceLocatorString } from '../../util/routes';
import {
InlineTextButton,
@ -46,19 +46,18 @@ const priceData = (price, intl) => {
return {};
};
const createURL = (flattenedRoutes, listing) => {
const createURL = (routes, listing) => {
const id = listing.id.uuid;
const slug = createSlug(listing.attributes.title);
const pathParams = { id, slug, type: 'edit', tab: 'description' };
return createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {});
return createResourceLocatorString('EditListingPage', routes, pathParams, {});
};
export const ManageListingCardComponent = props => {
const {
className,
rootClassName,
flattenedRoutes,
hasClosingError,
hasOpeningError,
history,
@ -232,7 +231,7 @@ export const ManageListingCardComponent = props => {
onClick={event => {
event.preventDefault();
event.stopPropagation();
history.push(createURL(flattenedRoutes, listing));
history.push(createURL(routeConfiguration(), listing));
}}
>
<FormattedMessage id="ManageListingCard.edit" />
@ -248,7 +247,7 @@ ManageListingCardComponent.defaultProps = {
actionsInProgressListingId: null,
};
const { arrayOf, bool, func, shape, string } = PropTypes;
const { bool, func, shape, string } = PropTypes;
ManageListingCardComponent.propTypes = {
className: string,
@ -263,13 +262,10 @@ ManageListingCardComponent.propTypes = {
onOpenListing: func.isRequired,
onToggleMenu: func.isRequired,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
};
export default compose(withRouter, withFlattenedRoutes, injectIntl)(ManageListingCardComponent);
export default compose(withRouter, injectIntl)(ManageListingCardComponent);

View file

@ -9,7 +9,6 @@ describe('ManageListingCard', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<ManageListingCardComponent
flattenedRoutes={[]}
history={{ push: noop }}
listing={createListing('listing1')}
intl={fakeIntl}

View file

@ -1,5 +1,5 @@
exports[`ManageListingCard matches snapshot 1`] = `
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
className=""
name="ListingPage"
params={
@ -111,5 +111,5 @@ exports[`ManageListingCard matches snapshot 1`] = `
values={Object {}} />
</button>
</div>
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
`;

View file

@ -4,13 +4,13 @@ exports[`MapPanel matches snapshot 1`] = `
Map
</div>
<div />
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
name="SearchFiltersPage">
Filters
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
name="SearchListingsPage">
X
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</div>
`;

View file

@ -2,7 +2,7 @@
* This component wraps React-Router's Link by providing name-based routing.
*
* The `name` prop should match a route in the flattened
* routesConfiguration passed in context by the RoutesProvider
* routeConfiguration passed in context by the RoutesProvider
* component. The `params` props is the route params for the route
* path of the given route name.
*
@ -20,17 +20,15 @@
import React, { PropTypes } from 'react';
import { Link, withRouter } from 'react-router-dom';
import classNames from 'classnames';
import routeConfiguration from '../../routeConfiguration';
import { pathByRouteName } from '../../util/routes';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import * as propTypes from '../../util/propTypes';
export const NamedLinkComponent = props => {
const { name, params, flattenedRoutes, title } = props;
const { name, params, title } = props;
// Link props
const { to, children } = props;
const pathname = pathByRouteName(name, flattenedRoutes, params);
const pathname = pathByRouteName(name, routeConfiguration(), params);
const { match } = props;
const active = match.url && match.url === pathname;
@ -45,7 +43,7 @@ export const NamedLinkComponent = props => {
return <Link to={{ pathname, ...to }} {...aElemProps}>{children}</Link>;
};
const { arrayOf, object, string, shape, any } = PropTypes;
const { object, string, shape, any } = PropTypes;
NamedLinkComponent.defaultProps = {
params: {},
@ -62,7 +60,7 @@ NamedLinkComponent.defaultProps = {
NamedLinkComponent.displayName = 'NamedLink';
NamedLinkComponent.propTypes = {
// name of the route in routesConfiguration
// name of the route in routeConfiguration
name: string.isRequired,
// params object for the named route
params: object,
@ -76,11 +74,11 @@ NamedLinkComponent.propTypes = {
activeClassName: string,
title: string,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withRouter
match: object,
};
export default withFlattenedRoutes(withRouter(NamedLinkComponent));
const NamedLink = withRouter(NamedLinkComponent);
NamedLink.displayName = 'NamedLink';
export default NamedLink;

View file

@ -1,33 +1,27 @@
import React from 'react';
import { RoutesProvider } from '../index';
import { renderShallow, renderDeep } from '../../util/test-helpers';
import NamedLink, { NamedLinkComponent } from './NamedLink';
describe('NamedLinkComponent', () => {
it('should mark the link as active if the current URL matches', () => {
const routesConf = [
{ path: '/a', name: 'APage', component: () => null },
{ path: '/b', name: 'BPage', component: () => null },
];
const activeClassName = 'my-active-class';
const aProps = {
name: 'APage',
const landingPageProps = {
name: 'LandingPage',
activeClassName,
flattenedRoutes: routesConf,
match: { url: '/a' },
match: { url: '/' },
};
const bProps = {
name: 'BPage',
const searchPageProps = {
name: 'SearchPage',
activeClassName,
flattenedRoutes: routesConf,
match: { url: '/a' },
match: { url: '/' },
};
const tree = renderDeep(
<div>
<NamedLinkComponent {...aProps}>link to a</NamedLinkComponent>
<NamedLinkComponent {...bProps}>link to b</NamedLinkComponent>
<NamedLinkComponent {...landingPageProps}>link to a</NamedLinkComponent>
<NamedLinkComponent {...searchPageProps}>link to b</NamedLinkComponent>
</div>
);
const aLink = tree.children[0];
const bLink = tree.children[1];
expect(aLink.type).toEqual('a');
@ -40,16 +34,11 @@ describe('NamedLinkComponent', () => {
describe('NamedLink', () => {
it('should contain correct link', () => {
const id = 12;
const routesConf = [
{ path: '/somepage/:id', name: 'SomePage', component: () => <div>blaa</div> },
];
const tree = renderDeep(
<RoutesProvider flattenedRoutes={routesConf}>
<NamedLink name="SomePage" params={{ id }}>to SomePage</NamedLink>
</RoutesProvider>
<NamedLink name="ListingPageCanonical" params={{ id }}>to ListingPage</NamedLink>
);
expect(tree.type).toEqual('a');
expect(tree.props.href).toEqual(`/somepage/${id}`);
expect(tree.children).toEqual(['to SomePage']);
expect(tree.props.href).toEqual(`/l/${id}`);
expect(tree.children).toEqual(['to ListingPage']);
});
});

View file

@ -4,17 +4,16 @@
*/
import React, { PropTypes } from 'react';
import { Redirect } from 'react-router-dom';
import routeConfiguration from '../../routeConfiguration';
import { pathByRouteName } from '../../util/routes';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import * as propTypes from '../../util/propTypes';
const NamedRedirect = props => {
const { name, search, state, params, flattenedRoutes, push } = props;
const pathname = pathByRouteName(name, flattenedRoutes, params);
const { name, search, state, params, push } = props;
const pathname = pathByRouteName(name, routeConfiguration(), params);
return <Redirect to={{ pathname, search, state }} push={push} />;
};
const { arrayOf, object, string, bool } = PropTypes;
const { bool, object, string } = PropTypes;
NamedRedirect.defaultProps = { search: '', state: {}, push: false, params: {} };
@ -24,7 +23,6 @@ NamedRedirect.propTypes = {
state: object,
push: bool,
params: object,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
};
export default withFlattenedRoutes(NamedRedirect);
export default NamedRedirect;

View file

@ -60,7 +60,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
id="OrderDetailsPanel.orderAcceptedSubtitle"
values={
Object {
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -69,7 +69,7 @@ exports[`OrderDetailsPanel accepted matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</span>
@ -452,7 +452,7 @@ exports[`OrderDetailsPanel autorejected matches snapshot 1`] = `
id="OrderDetailsPanel.orderAutoRejectedTitle"
values={
Object {
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -461,7 +461,7 @@ exports[`OrderDetailsPanel autorejected matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>
@ -853,7 +853,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
id="OrderDetailsPanel.orderPreauthorizedSubtitle"
values={
Object {
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -862,7 +862,7 @@ exports[`OrderDetailsPanel canceled matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</span>
@ -1238,7 +1238,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
id="OrderDetailsPanel.orderDeliveredTitle"
values={
Object {
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -1247,7 +1247,7 @@ exports[`OrderDetailsPanel delivered matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>
@ -1639,7 +1639,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
id="OrderDetailsPanel.orderPreauthorizedSubtitle"
values={
Object {
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -1648,7 +1648,7 @@ exports[`OrderDetailsPanel preauthorized matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</span>
@ -2024,7 +2024,7 @@ exports[`OrderDetailsPanel rejected matches snapshot 1`] = `
id="OrderDetailsPanel.orderRejectedTitle"
values={
Object {
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -2033,7 +2033,7 @@ exports[`OrderDetailsPanel rejected matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>

View file

@ -8,7 +8,7 @@ exports[`PaginationLinks should match snapshot with both links disabled 1`] = `
</div>
<div
className="">
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
className="undefined"
name="SomePage"
params={
@ -23,7 +23,7 @@ exports[`PaginationLinks should match snapshot with both links disabled 1`] = `
}
}>
1
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</div>
<div
className="">
@ -36,7 +36,7 @@ exports[`PaginationLinks should match snapshot with both links disabled 1`] = `
exports[`PaginationLinks should match snapshot with both links enabled 1`] = `
<nav
className="">
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
name="SomePage"
params={
Object {
@ -51,10 +51,10 @@ exports[`PaginationLinks should match snapshot with both links enabled 1`] = `
}>
<PrevPageIcon
className={null} />
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
<div
className="">
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
className=""
name="SomePage"
params={
@ -69,8 +69,8 @@ exports[`PaginationLinks should match snapshot with both links enabled 1`] = `
}
}>
1
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
className="undefined"
name="SomePage"
params={
@ -85,8 +85,8 @@ exports[`PaginationLinks should match snapshot with both links enabled 1`] = `
}
}>
2
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
className=""
name="SomePage"
params={
@ -101,9 +101,9 @@ exports[`PaginationLinks should match snapshot with both links enabled 1`] = `
}
}>
3
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</div>
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
name="SomePage"
params={
Object {
@ -118,7 +118,7 @@ exports[`PaginationLinks should match snapshot with both links enabled 1`] = `
}>
<NextPageIcon
className={null} />
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</nav>
`;
@ -132,7 +132,7 @@ exports[`PaginationLinks should match snapshot with prev disabled and next enabl
</div>
<div
className="">
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
className="undefined"
name="SomePage"
params={
@ -147,8 +147,8 @@ exports[`PaginationLinks should match snapshot with prev disabled and next enabl
}
}>
1
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
className=""
name="SomePage"
params={
@ -163,8 +163,8 @@ exports[`PaginationLinks should match snapshot with prev disabled and next enabl
}
}>
2
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
className=""
name="SomePage"
params={
@ -179,9 +179,9 @@ exports[`PaginationLinks should match snapshot with prev disabled and next enabl
}
}>
3
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</div>
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
name="SomePage"
params={
Object {
@ -196,14 +196,14 @@ exports[`PaginationLinks should match snapshot with prev disabled and next enabl
}>
<NextPageIcon
className={null} />
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</nav>
`;
exports[`PaginationLinks should match snapshot with prev enabled and next disabled 1`] = `
<nav
className="">
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
name="SomePage"
params={
Object {
@ -218,10 +218,10 @@ exports[`PaginationLinks should match snapshot with prev enabled and next disabl
}>
<PrevPageIcon
className={null} />
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
<div
className="">
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
className=""
name="SomePage"
params={
@ -236,8 +236,8 @@ exports[`PaginationLinks should match snapshot with prev enabled and next disabl
}
}>
1
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
className=""
name="SomePage"
params={
@ -252,8 +252,8 @@ exports[`PaginationLinks should match snapshot with prev enabled and next disabl
}
}>
2
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
</NamedLink>
<NamedLink
className="undefined"
name="SomePage"
params={
@ -268,7 +268,7 @@ exports[`PaginationLinks should match snapshot with prev enabled and next disabl
}
}>
3
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</div>
<div
className="">

View file

@ -1,22 +0,0 @@
import React, { Component, PropTypes } from 'react';
import * as propTypes from '../../util/propTypes';
class RoutesProvider extends Component {
getChildContext() {
return { flattenedRoutes: this.props.flattenedRoutes };
}
render() {
return React.Children.only(this.props.children);
}
}
const { arrayOf, node } = PropTypes;
RoutesProvider.childContextTypes = { flattenedRoutes: arrayOf(propTypes.route).isRequired };
RoutesProvider.defaultProps = { children: {} };
RoutesProvider.propTypes = { flattenedRoutes: arrayOf(propTypes.route).isRequired, children: node };
export default RoutesProvider;

View file

@ -1,18 +0,0 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import RoutesProvider from './RoutesProvider';
describe('RoutesProvider', () => {
it('should contain routes from context', () => {
const routesConf = [{ name: 'SomePage', path: 'some-page', component: () => null }];
const Child = (props, context) => {
return <div>{context.flattenedRoutes[0].name}</div>;
};
Child.contextTypes = { flattenedRoutes: React.PropTypes.array };
const tree = renderDeep(
<RoutesProvider flattenedRoutes={routesConf}><Child /></RoutesProvider>
);
expect(tree.children).toContain('SomePage');
});
});

View file

@ -68,7 +68,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = `
values={
Object {
"customerName": "customer1 display name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -77,7 +77,7 @@ exports[`SaleDetailsPanel accepted matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>
@ -422,7 +422,7 @@ exports[`SaleDetailsPanel autorejected matches snapshot 1`] = `
values={
Object {
"customerName": "customer1 display name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -431,7 +431,7 @@ exports[`SaleDetailsPanel autorejected matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>
@ -776,7 +776,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = `
values={
Object {
"customerName": "customer1 display name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -785,7 +785,7 @@ exports[`SaleDetailsPanel canceled matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>
@ -1142,7 +1142,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = `
values={
Object {
"customerName": "customer1 display name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -1151,7 +1151,7 @@ exports[`SaleDetailsPanel delivered matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>
@ -1496,7 +1496,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
values={
Object {
"customerName": "customer1 display name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -1505,7 +1505,7 @@ exports[`SaleDetailsPanel preauthorized matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>
@ -1862,7 +1862,7 @@ exports[`SaleDetailsPanel rejected matches snapshot 1`] = `
values={
Object {
"customerName": "customer1 display name",
"listingLink": <withFlattenedRoutes(withRouter(NamedLink))
"listingLink": <NamedLink
name="ListingPage"
params={
Object {
@ -1871,7 +1871,7 @@ exports[`SaleDetailsPanel rejected matches snapshot 1`] = `
}
}>
listing1 title
</withFlattenedRoutes(withRouter(NamedLink))>,
</NamedLink>,
}
} />
</h1>

View file

@ -5,10 +5,10 @@ import { withRouter } from 'react-router-dom';
import { injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
import * as propTypes from '../../util/propTypes';
import { formatMoney } from '../../util/currency';
import { ensureListing } from '../../util/data';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { createResourceLocatorString } from '../../util/routes';
import { createSlug } from '../../util/urlHelpers';
import { ResponsiveImage } from '../../components';
@ -21,23 +21,23 @@ const getPixelPositionOffset = (width, height) => {
return { x: -1 * (width / 2), y: -1 * (height + 3) };
};
const createURL = (flattenedRoutes, history, listing) => {
const createURL = (routes, history, listing) => {
const id = listing.id.uuid;
const slug = createSlug(listing.attributes.title);
const pathParams = { id, slug };
return createResourceLocatorString('ListingPage', flattenedRoutes, pathParams, {});
return createResourceLocatorString('ListingPage', routes, pathParams, {});
};
// ListingCard is the listing info without overlayview or carousel controls
const ListingCard = props => {
const { className, clickHandler, flattenedRoutes, history, intl, isInCarousel, listing } = props;
const { className, clickHandler, history, intl, isInCarousel, listing } = props;
const { title, price } = listing.attributes;
const formattedPrice = price && price.currency === config.currency
? formatMoney(intl, price)
: price.currency;
const firstImage = listing.images && listing.images.length > 0 ? listing.images[0] : null;
const urlToListing = createURL(flattenedRoutes, history, listing);
const urlToListing = createURL(routeConfiguration(), history, listing);
// listing card anchor needs sometimes inherited border radius.
const classes = classNames(
@ -100,7 +100,6 @@ ListingCard.propTypes = {
className: string,
listing: propTypes.listing.isRequired,
clickHandler: func.isRequired,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
history: shape({
push: func.isRequired,
}).isRequired,
@ -127,7 +126,7 @@ class SearchMapInfoCard extends Component {
}
render() {
const { className, rootClassName, intl, flattenedRoutes, history, listings } = this.props;
const { className, rootClassName, intl, history, listings } = this.props;
const currentListing = ensureListing(listings[this.state.currentListingIndex]);
const geolocation = currentListing.attributes.geolocation;
@ -179,7 +178,6 @@ class SearchMapInfoCard extends Component {
<ListingCard
clickHandler={this.clickHandler}
listing={currentListing}
flattenedRoutes={flattenedRoutes}
history={history}
intl={intl}
isInCarousel={hasCarousel}
@ -204,9 +202,6 @@ SearchMapInfoCard.propTypes = {
listings: arrayOf(propTypes.listing).isRequired,
onClickCallback: func,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withRouter
history: shape({
push: func.isRequired,
@ -216,4 +211,4 @@ SearchMapInfoCard.propTypes = {
intl: intlShape.isRequired,
};
export default compose(withRouter, withFlattenedRoutes, injectIntl)(SearchMapInfoCard);
export default compose(withRouter, injectIntl)(SearchMapInfoCard);

View file

@ -4,8 +4,9 @@ import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import { pickBy } from 'lodash';
import classNames from 'classnames';
import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
import { ensureCurrentUser } from '../../util/data';
import { withFlattenedRoutes, withViewport } from '../../util/contextHelpers';
import { withViewport } from '../../util/contextHelpers';
import { parse, stringify } from '../../util/urlHelpers';
import { createResourceLocatorString, pathByRouteName } from '../../util/routes';
import * as propTypes from '../../util/propTypes';
@ -98,8 +99,9 @@ class TopbarComponent extends Component {
const hasOrders = currentUserHasOrders === true;
const hasListingsOrOrders = currentUserHasListings || hasOrders;
const routes = routeConfiguration();
const whitelistedPaths = VERIFY_EMAIL_MODAL_WHITELIST.map(page =>
pathByRouteName(page, this.props.flattenedRoutes));
pathByRouteName(page, routes));
const isNotWhitelisted = !whitelistedPaths.includes(newLocation.pathname);
// Show reminder
@ -126,16 +128,16 @@ class TopbarComponent extends Component {
handleSubmit(values) {
const { search, selectedPlace } = values.location;
const { flattenedRoutes, history } = this.props;
const { history } = this.props;
const { origin, bounds, country } = selectedPlace;
const searchParams = { address: search, origin, bounds, country };
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, searchParams));
}
handleLogout() {
const { onLogout, history, flattenedRoutes } = this.props;
const { onLogout, history } = this.props;
onLogout().then(() => {
const path = pathByRouteName('LandingPage', flattenedRoutes);
const path = pathByRouteName('LandingPage', routeConfiguration());
// In production we ensure that data is really lost,
// but in development mode we use stored values for debugging
@ -349,7 +351,7 @@ TopbarComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { arrayOf, bool, func, instanceOf, number, shape, string } = PropTypes;
const { bool, func, instanceOf, number, shape, string } = PropTypes;
TopbarComponent.propTypes = {
className: string,
@ -384,14 +386,11 @@ TopbarComponent.propTypes = {
height: number.isRequired,
}).isRequired,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const Topbar = compose(withViewport, withFlattenedRoutes, injectIntl)(TopbarComponent);
const Topbar = compose(withViewport, injectIntl)(TopbarComponent);
Topbar.displayName = 'Topbar';

View file

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react';
import { FormattedMessage, intlShape } from 'react-intl';
import classNames from 'classnames';
import { ACCOUNT_SETTINGS_PAGES } from '../../routesConfiguration';
import { ACCOUNT_SETTINGS_PAGES } from '../../routeConfiguration';
import * as propTypes from '../../util/propTypes';
import {
Avatar,

View file

@ -1,9 +1,6 @@
import React from 'react';
import { fakeIntl } from '../../util/test-data';
import { renderDeep } from '../../util/test-helpers';
import { RoutesProvider } from '../../components';
import routesConfiguration from '../../routesConfiguration';
import { flattenRoutes } from '../../util/routes';
import TopbarDesktop from './TopbarDesktop';
const noop = () => null;
@ -11,7 +8,6 @@ const noop = () => null;
describe('TopbarDesktop', () => {
it('data matches snapshot', () => {
window.google = { maps: {} };
const flattenedRoutes = flattenRoutes(routesConfiguration);
const topbarProps = {
isAuthenticated: true,
currentUserHasListings: true,
@ -20,11 +16,7 @@ describe('TopbarDesktop', () => {
intl: fakeIntl,
onLogout: noop,
};
const tree = renderDeep(
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<TopbarDesktop {...topbarProps} />
</RoutesProvider>
);
const tree = renderDeep(<TopbarDesktop {...topbarProps} />);
delete window.google;
expect(tree).toMatchSnapshot();
});

View file

@ -5,7 +5,7 @@
import React, { PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { ACCOUNT_SETTINGS_PAGES } from '../../routesConfiguration';
import { ACCOUNT_SETTINGS_PAGES } from '../../routeConfiguration';
import * as propTypes from '../../util/propTypes';
import { ensureCurrentUser } from '../../util/data';
import { AvatarLarge, InlineTextButton, NamedLink, NotificationBadge } from '../../components';

View file

@ -1,7 +1,7 @@
import React, { PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { ACCOUNT_SETTINGS_PAGES } from '../../routesConfiguration';
import { ACCOUNT_SETTINGS_PAGES } from '../../routeConfiguration';
import { TabNavHorizontal } from '../../components';
import css from './UserNav.css';

View file

@ -1,159 +1,90 @@
import AddImages from './AddImages/AddImages';
import Avatar, { AvatarMedium, AvatarLarge } from './Avatar/Avatar';
import BirthdayInputField from './BirthdayInputField/BirthdayInputField';
import BookingBreakdown from './BookingBreakdown/BookingBreakdown';
import Button, { PrimaryButton, SecondaryButton, InlineTextButton } from './Button/Button';
import CurrencyInputField from './CurrencyInputField/CurrencyInputField';
import DateInputField from './DateInputField/DateInputField';
import DateRangeInputField from './DateRangeInputField/DateRangeInputField';
import Discussion from './Discussion/Discussion';
import EditListingDescriptionPanel from './EditListingDescriptionPanel/EditListingDescriptionPanel';
import EditListingLocationPanel from './EditListingLocationPanel/EditListingLocationPanel';
import EditListingPhotosPanel from './EditListingPhotosPanel/EditListingPhotosPanel';
import EditListingPricingPanel from './EditListingPricingPanel/EditListingPricingPanel';
import EditListingWizard from './EditListingWizard/EditListingWizard';
import ExpandingTextarea from './ExpandingTextarea/ExpandingTextarea';
import ExternalLink from './ExternalLink/ExternalLink';
import FilterPanel from './FilterPanel/FilterPanel';
import Form from './Form/Form';
import HeroSection from './HeroSection/HeroSection';
import IconBannedUser from './IconBannedUser/IconBannedUser';
import IconCheckmark from './IconCheckmark/IconCheckmark';
import IconClose from './IconClose/IconClose';
import IconEmailAttention from './IconEmailAttention/IconEmailAttention';
import IconEmailSent from './IconEmailSent/IconEmailSent';
import IconEmailSuccess from './IconEmailSuccess/IconEmailSuccess';
import IconKeys from './IconKeys/IconKeys';
import IconKeysSuccess from './IconKeysSuccess/IconKeysSuccess';
import IconSearch from './IconSearch/IconSearch';
import IconSpinner from './IconSpinner/IconSpinner';
import ImageCarousel from './ImageCarousel/ImageCarousel';
import ImageFromFile from './ImageFromFile/ImageFromFile';
import LayoutSideNavigation, {
export { default as AddImages } from './AddImages/AddImages';
export { default as Avatar, AvatarMedium, AvatarLarge } from './Avatar/Avatar';
export { default as BirthdayInputField } from './BirthdayInputField/BirthdayInputField';
export { default as BookingBreakdown } from './BookingBreakdown/BookingBreakdown';
export {
default as Button,
PrimaryButton,
SecondaryButton,
InlineTextButton,
} from './Button/Button';
export { default as CurrencyInputField } from './CurrencyInputField/CurrencyInputField';
export { default as DateInputField } from './DateInputField/DateInputField';
export { default as DateRangeInputField } from './DateRangeInputField/DateRangeInputField';
export { default as Discussion } from './Discussion/Discussion';
export {
default as EditListingDescriptionPanel,
} from './EditListingDescriptionPanel/EditListingDescriptionPanel';
export {
default as EditListingLocationPanel,
} from './EditListingLocationPanel/EditListingLocationPanel';
export { default as EditListingPhotosPanel } from './EditListingPhotosPanel/EditListingPhotosPanel';
export {
default as EditListingPricingPanel,
} from './EditListingPricingPanel/EditListingPricingPanel';
export { default as EditListingWizard } from './EditListingWizard/EditListingWizard';
export { default as ExpandingTextarea } from './ExpandingTextarea/ExpandingTextarea';
export { default as ExternalLink } from './ExternalLink/ExternalLink';
export { default as FilterPanel } from './FilterPanel/FilterPanel';
export { default as Form } from './Form/Form';
export { default as HeroSection } from './HeroSection/HeroSection';
export { default as IconBannedUser } from './IconBannedUser/IconBannedUser';
export { default as IconCheckmark } from './IconCheckmark/IconCheckmark';
export { default as IconClose } from './IconClose/IconClose';
export { default as IconEmailAttention } from './IconEmailAttention/IconEmailAttention';
export { default as IconEmailSent } from './IconEmailSent/IconEmailSent';
export { default as IconEmailSuccess } from './IconEmailSuccess/IconEmailSuccess';
export { default as IconKeys } from './IconKeys/IconKeys';
export { default as IconKeysSuccess } from './IconKeysSuccess/IconKeysSuccess';
export { default as IconSearch } from './IconSearch/IconSearch';
export { default as IconSpinner } from './IconSpinner/IconSpinner';
export { default as ImageCarousel } from './ImageCarousel/ImageCarousel';
export { default as ImageFromFile } from './ImageFromFile/ImageFromFile';
export {
default as LayoutSideNavigation,
ContentWrapper,
SideNavWrapper,
TopbarWrapper,
} from './LayoutSideNavigation/LayoutSideNavigation';
import ListingCard from './ListingCard/ListingCard';
import LocationAutocompleteInput, {
export { default as ListingCard } from './ListingCard/ListingCard';
export {
default as LocationAutocompleteInput,
LocationAutocompleteInputField,
} from './LocationAutocompleteInput/LocationAutocompleteInput';
import ManageListingCard from './ManageListingCard/ManageListingCard';
import Map from './Map/Map';
import MapPanel from './MapPanel/MapPanel';
import Menu from './Menu/Menu';
import MenuContent from './MenuContent/MenuContent';
import MenuItem from './MenuItem/MenuItem';
import MenuLabel from './MenuLabel/MenuLabel';
import Modal from './Modal/Modal';
import ModalInMobile from './ModalInMobile/ModalInMobile';
import NamedLink from './NamedLink/NamedLink';
import NamedRedirect from './NamedRedirect/NamedRedirect';
import NotificationBadge from './NotificationBadge/NotificationBadge';
import OrderDetailsPanel from './OrderDetailsPanel/OrderDetailsPanel';
import OrderDiscussionPanel from './OrderDiscussionPanel/OrderDiscussionPanel';
import Page from './Page/Page';
import PaginationLinks from './PaginationLinks/PaginationLinks';
import Promised from './Promised/Promised';
import ResponsiveImage from './ResponsiveImage/ResponsiveImage';
import RoutesProvider from './RoutesProvider/RoutesProvider';
import SaleDetailsPanel from './SaleDetailsPanel/SaleDetailsPanel';
import SearchMap from './SearchMap/SearchMap';
import SearchMapGroupLabel from './SearchMapGroupLabel/SearchMapGroupLabel';
import SearchMapInfoCard from './SearchMapInfoCard/SearchMapInfoCard';
import SearchMapPriceLabel from './SearchMapPriceLabel/SearchMapPriceLabel';
import SearchResultsPanel from './SearchResultsPanel/SearchResultsPanel';
import SelectField from './SelectField/SelectField';
import StripeBankAccountTokenInputField
from './StripeBankAccountTokenInputField/StripeBankAccountTokenInputField';
import TabNav from './TabNav/TabNav';
import TabNavHorizontal from './TabNavHorizontal/TabNavHorizontal';
import Tabs from './Tabs/Tabs';
import TextInputField from './TextInputField/TextInputField';
import Topbar from './Topbar/Topbar';
import TopbarDesktop from './TopbarDesktop/TopbarDesktop';
import TopbarMobileMenu from './TopbarMobileMenu/TopbarMobileMenu';
import UserNav from './UserNav/UserNav';
import ValidationError from './ValidationError/ValidationError';
export { default as ManageListingCard } from './ManageListingCard/ManageListingCard';
export { default as Map } from './Map/Map';
export { default as MapPanel } from './MapPanel/MapPanel';
export { default as Menu } from './Menu/Menu';
export { default as MenuContent } from './MenuContent/MenuContent';
export { default as MenuItem } from './MenuItem/MenuItem';
export { default as MenuLabel } from './MenuLabel/MenuLabel';
export { default as Modal } from './Modal/Modal';
export { default as ModalInMobile } from './ModalInMobile/ModalInMobile';
export { default as NamedLink } from './NamedLink/NamedLink';
export { default as NamedRedirect } from './NamedRedirect/NamedRedirect';
export { default as NotificationBadge } from './NotificationBadge/NotificationBadge';
export { default as OrderDetailsPanel } from './OrderDetailsPanel/OrderDetailsPanel';
export { default as OrderDiscussionPanel } from './OrderDiscussionPanel/OrderDiscussionPanel';
export { default as Page } from './Page/Page';
export { default as PaginationLinks } from './PaginationLinks/PaginationLinks';
export { default as Promised } from './Promised/Promised';
export { default as ResponsiveImage } from './ResponsiveImage/ResponsiveImage';
export { default as SaleDetailsPanel } from './SaleDetailsPanel/SaleDetailsPanel';
export { default as SearchMap } from './SearchMap/SearchMap';
export { default as SearchMapGroupLabel } from './SearchMapGroupLabel/SearchMapGroupLabel';
export { default as SearchMapInfoCard } from './SearchMapInfoCard/SearchMapInfoCard';
export { default as SearchMapPriceLabel } from './SearchMapPriceLabel/SearchMapPriceLabel';
export { default as SearchResultsPanel } from './SearchResultsPanel/SearchResultsPanel';
export { default as SelectField } from './SelectField/SelectField';
export {
AddImages,
Avatar,
AvatarLarge,
AvatarMedium,
BirthdayInputField,
BookingBreakdown,
Button,
ContentWrapper,
CurrencyInputField,
DateInputField,
DateRangeInputField,
Discussion,
EditListingDescriptionPanel,
EditListingLocationPanel,
EditListingPhotosPanel,
EditListingPricingPanel,
EditListingWizard,
ExpandingTextarea,
ExternalLink,
FilterPanel,
Form,
HeroSection,
IconBannedUser,
IconCheckmark,
IconClose,
IconEmailAttention,
IconEmailSent,
IconEmailSuccess,
IconKeys,
IconKeysSuccess,
IconSearch,
IconSpinner,
ImageCarousel,
ImageFromFile,
InlineTextButton,
LayoutSideNavigation,
ListingCard,
LocationAutocompleteInput,
LocationAutocompleteInputField,
ManageListingCard,
Map,
MapPanel,
Menu,
MenuContent,
MenuItem,
MenuLabel,
Modal,
ModalInMobile,
NamedLink,
NamedRedirect,
NotificationBadge,
OrderDetailsPanel,
OrderDiscussionPanel,
Page,
PaginationLinks,
PrimaryButton,
Promised,
ResponsiveImage,
RoutesProvider,
SaleDetailsPanel,
SearchMap,
SearchMapGroupLabel,
SearchMapInfoCard,
SearchMapPriceLabel,
SearchResultsPanel,
SecondaryButton,
SelectField,
SideNavWrapper,
StripeBankAccountTokenInputField,
TabNav,
TabNavHorizontal,
Tabs,
TextInputField,
Topbar,
TopbarDesktop,
TopbarMobileMenu,
TopbarWrapper,
UserNav,
ValidationError,
};
default as StripeBankAccountTokenInputField,
} from './StripeBankAccountTokenInputField/StripeBankAccountTokenInputField';
export { default as TabNav } from './TabNav/TabNav';
export { default as TabNavHorizontal } from './TabNavHorizontal/TabNavHorizontal';
export { default as Tabs } from './Tabs/Tabs';
export { default as TextInputField } from './TextInputField/TextInputField';
export { default as Topbar } from './Topbar/Topbar';
export { default as TopbarDesktop } from './TopbarDesktop/TopbarDesktop';
export { default as TopbarMobileMenu } from './TopbarMobileMenu/TopbarMobileMenu';
export { default as UserNav } from './UserNav/UserNav';
export { default as ValidationError } from './ValidationError/ValidationError';

View file

@ -4,10 +4,10 @@ import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { withRouter } from 'react-router-dom';
import classNames from 'classnames';
import routeConfiguration from '../../routeConfiguration';
import { pathByRouteName } from '../../util/routes';
import * as propTypes from '../../util/propTypes';
import { ensureListing, ensureUser, ensureTransaction, ensureBooking } from '../../util/data';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import { createSlug } from '../../util/urlHelpers';
import {
isTransactionInitiateAmountTooLowError,
@ -99,7 +99,7 @@ export class CheckoutPageComponent extends Component {
}
this.setState({ submitting: true });
const { flattenedRoutes, history, sendOrderRequest, speculatedTransaction } = this.props;
const { history, sendOrderRequest, speculatedTransaction } = this.props;
const requestParams = {
listingId: this.state.pageData.listing.id,
cardToken,
@ -110,7 +110,7 @@ export class CheckoutPageComponent extends Component {
sendOrderRequest(requestParams)
.then(orderId => {
this.setState({ submitting: false });
const orderDetailsPath = pathByRouteName('OrderDetailsPage', flattenedRoutes, {
const orderDetailsPath = pathByRouteName('OrderDetailsPage', routeConfiguration(), {
id: orderId.uuid,
});
clearData(STORAGE_KEY);
@ -375,7 +375,7 @@ CheckoutPageComponent.defaultProps = {
currentUser: null,
};
const { arrayOf, func, instanceOf, shape, string, bool } = PropTypes;
const { func, instanceOf, shape, string, bool } = PropTypes;
CheckoutPageComponent.propTypes = {
authInfoError: instanceOf(Error),
@ -404,9 +404,6 @@ CheckoutPageComponent.propTypes = {
history: shape({
push: func.isRequired,
}).isRequired,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
};
const mapStateToProps = state => {
@ -439,12 +436,9 @@ const mapDispatchToProps = dispatch => ({
dispatch(speculateTransaction(listingId, bookingStart, bookingEnd)),
});
const CheckoutPage = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
withFlattenedRoutes,
injectIntl
)(CheckoutPageComponent);
const CheckoutPage = compose(withRouter, connect(mapStateToProps, mapDispatchToProps), injectIntl)(
CheckoutPageComponent
);
CheckoutPage.setInitialValues = initialValues => setInitialValues(initialValues);

View file

@ -14,7 +14,6 @@ describe('CheckoutPage', () => {
bookingStart: new Date(Date.UTC(2017, 3, 14)),
bookingEnd: new Date(Date.UTC(2017, 3, 16)),
},
flattenedRoutes: [],
history: { push: noop },
intl: fakeIntl,
listing,

View file

@ -4,7 +4,7 @@ exports[`CheckoutPage matches snapshot 1`] = `
logoutError={null}
title="CheckoutPage.title">
<div>
<withFlattenedRoutes(withRouter(NamedLink))
<NamedLink
name="LandingPage">
<LogoIcon
alt="Logo"
@ -15,7 +15,7 @@ exports[`CheckoutPage matches snapshot 1`] = `
alt="CheckoutPage.goToLandingPage"
className={null}
isMobile={false} />
</withFlattenedRoutes(withRouter(NamedLink))>
</NamedLink>
</div>
<div>
<div>

View file

@ -59,7 +59,6 @@ export const EditListingPageComponent = props => {
currentUserHasOrders,
createStripeAccountError,
fetchInProgress,
flattenedRoutes,
getListing,
history,
intl,
@ -168,7 +167,6 @@ export const EditListingPageComponent = props => {
errors={errors}
fetchInProgress={fetchInProgress}
newListingCreated={newListingCreated}
flattenedRoutes={flattenedRoutes}
history={history}
images={images}
listing={isNew ? page.listingDraft : currentListing}
@ -211,7 +209,7 @@ EditListingPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { arrayOf, bool, func, instanceOf, number, object, shape, string, oneOf } = PropTypes;
const { bool, func, instanceOf, number, object, shape, string, oneOf } = PropTypes;
EditListingPageComponent.propTypes = {
authInfoError: instanceOf(Error),
@ -221,7 +219,6 @@ EditListingPageComponent.propTypes = {
currentUserHasListings: bool.isRequired,
currentUserHasOrders: bool,
fetchInProgress: bool.isRequired,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
getListing: func.isRequired,
isAuthenticated: bool.isRequired,
logoutError: instanceOf(Error),

View file

@ -15,7 +15,6 @@ describe('EditListingPageComponent', () => {
isAuthenticated={false}
authInProgress={false}
fetchInProgress={false}
flattenedRoutes={[]}
location={{ search: '' }}
history={{ push: noop }}
getListing={getListing}

View file

@ -38,7 +38,6 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
}
}
fetchInProgress={false}
flattenedRoutes={Array []}
history={
Object {
"push": [Function],

View file

@ -9,15 +9,13 @@ import {
createBooking,
} from '../../util/test-data';
import { InboxPageComponent, InboxItem } from './InboxPage';
import routesConfiguration from '../../routesConfiguration';
import { flattenRoutes } from '../../util/routes';
import routeConfiguration from '../../routeConfiguration';
import { TX_TRANSITION_PREAUTHORIZE } from '../../util/propTypes';
const noop = () => null;
describe('InboxPage', () => {
it('matches snapshot', () => {
const flattenedRoutes = flattenRoutes(routesConfiguration);
const provider = createUser('provider-user-id');
const customer = createUser('customer-user-id');
const currentUserProvider = createCurrentUser('provider-user-id');
@ -76,9 +74,7 @@ describe('InboxPage', () => {
// Deeply render one InboxItem
const orderItem = renderDeep(
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<InboxItem type="order" tx={ordersProps.transactions[0]} intl={fakeIntl} />
</RoutesProvider>
<InboxItem type="order" tx={ordersProps.transactions[0]} intl={fakeIntl} />
);
expect(orderItem).toMatchSnapshot();
@ -126,9 +122,7 @@ describe('InboxPage', () => {
// Deeply render one InboxItem
const saleItem = renderDeep(
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<InboxItem type="sale" tx={salesProps.transactions[0]} intl={fakeIntl} />
</RoutesProvider>
<InboxItem type="sale" tx={salesProps.transactions[0]} intl={fakeIntl} />
);
expect(saleItem).toMatchSnapshot();
});

View file

@ -6,10 +6,9 @@ import { injectIntl, intlShape } from 'react-intl';
import { sendVerificationEmail } from '../../ducks/user.duck';
import { logout, authenticationInProgress } from '../../ducks/Auth.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
import { Page, HeroSection, Topbar } from '../../components';
import * as propTypes from '../../util/propTypes';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import config from '../../config';
import { Page, HeroSection, Topbar } from '../../components';
import facebookImage from '../../assets/saunatimeFacebook-1200x630.jpg';
import twitterImage from '../../assets/saunatimeTwitter-600x314.jpg';
@ -22,7 +21,6 @@ export const LandingPageComponent = props => {
currentUser,
currentUserHasListings,
currentUserHasOrders,
flattenedRoutes,
history,
intl,
isAuthenticated,
@ -87,12 +85,7 @@ export const LandingPageComponent = props => {
sendVerificationEmailError={sendVerificationEmailError}
/>
<div className={css.heroContainer}>
<HeroSection
className={css.hero}
flattenedRoutes={flattenedRoutes}
history={history}
location={location}
/>
<HeroSection className={css.hero} history={history} location={location} />
</div>
</Page>
);
@ -107,7 +100,7 @@ LandingPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { array, bool, func, instanceOf, number, object } = PropTypes;
const { bool, func, instanceOf, number, object } = PropTypes;
LandingPageComponent.propTypes = {
authInfoError: instanceOf(Error),
@ -125,9 +118,6 @@ LandingPageComponent.propTypes = {
sendVerificationEmailError: instanceOf(Error),
onResendVerificationEmail: func.isRequired,
// from withFlattenedRoutes
flattenedRoutes: array.isRequired,
// from withRouter
history: object.isRequired,
location: object.isRequired,
@ -168,11 +158,8 @@ const mapDispatchToProps = dispatch => ({
onResendVerificationEmail: () => dispatch(sendVerificationEmail()),
});
const LandingPage = compose(
connect(mapStateToProps, mapDispatchToProps),
injectIntl,
withRouter,
withFlattenedRoutes
)(LandingPageComponent);
const LandingPage = compose(connect(mapStateToProps, mapDispatchToProps), injectIntl, withRouter)(
LandingPageComponent
);
export default LandingPage;

View file

@ -3,7 +3,7 @@ import { fakeIntl } from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
import { LandingPageComponent } from './LandingPage';
import { RoutesProvider } from '../../components';
import routesConfiguration from '../../routesConfiguration';
import routeConfiguration from '../../routeConfiguration';
const noop = () => null;
@ -11,7 +11,6 @@ describe('LandingPage', () => {
it('matches snapshot', () => {
const tree = renderShallow(
<LandingPageComponent
flattenedRoutes={[]}
history={{ push: noop }}
location={{ search: '' }}
scrollingDisabled={false}

View file

@ -60,7 +60,6 @@ exports[`LandingPage matches snapshot 1`] = `
<div>
<HeroSection
className={null}
flattenedRoutes={Array []}
history={
Object {
"push": [Function],

View file

@ -6,6 +6,7 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import classNames from 'classnames';
import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
import * as propTypes from '../../util/propTypes';
import { types } from '../../util/sdkLoader';
import { createSlug } from '../../util/urlHelpers';
@ -88,7 +89,7 @@ export const ActionBar = props => {
}
};
const { arrayOf, bool, func, instanceOf, number, object, oneOf, shape, string } = PropTypes;
const { bool, func, instanceOf, number, object, oneOf, shape, string } = PropTypes;
ActionBar.propTypes = {
isOwnListing: bool.isRequired,
@ -114,7 +115,7 @@ export class ListingPageComponent extends Component {
}
handleSubmit(values) {
const { flattenedRoutes, history, getListing, params, useInitialValues } = this.props;
const { history, getListing, params, useInitialValues } = this.props;
const listingId = new UUID(params.id);
const listing = getListing(listingId);
@ -130,15 +131,16 @@ export class ListingPageComponent extends Component {
},
};
const routes = routeConfiguration();
// Customize checkout page state with current listing and selected bookingDates
const { setInitialValues } = findRouteByRouteName('CheckoutPage', flattenedRoutes);
const { setInitialValues } = findRouteByRouteName('CheckoutPage', routes);
useInitialValues(setInitialValues, initialValues);
// Redirect to CheckoutPage
history.push(
createResourceLocatorString(
'CheckoutPage',
flattenedRoutes,
routes,
{ id: listing.id.uuid, slug: createSlug(listing.attributes.title) },
{}
)
@ -167,7 +169,6 @@ export class ListingPageComponent extends Component {
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
flattenedRoutes,
} = this.props;
const listingId = new UUID(params.id);
const currentListing = ensureListing(getListing(listingId));
@ -345,9 +346,13 @@ export class ListingPageComponent extends Component {
{ title, price: formattedPrice, siteTitle }
);
const canonicalPath = createResourceLocatorString('ListingPageCanonical', flattenedRoutes, {
id: listingId.uuid,
});
const canonicalPath = createResourceLocatorString(
'ListingPageCanonical',
routeConfiguration(),
{
id: listingId.uuid,
}
);
return (
<Page
@ -517,7 +522,6 @@ ListingPageComponent.propTypes = {
push: func.isRequired,
}).isRequired,
location: object.isRequired,
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from injectIntl
intl: intlShape.isRequired,
params: shape({

View file

@ -4,10 +4,15 @@ import { FormattedMessage } from 'react-intl';
import { types } from '../../util/sdkLoader';
import { createUser, createCurrentUser, createListing, fakeIntl } from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
import { ListingPageComponent, ActionBar } from './ListingPage';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { showListingRequest, showListingError, showListing } from './ListingPage.duck';
// routeConfiguration needs to be imported before tests for ListingPageComponent can be made.
// Otherwise, ListingPage itself is not initialized correctly when routeConfiguration is imported
// (loadData call fails).
import routeConfiguration from '../../routeConfiguration';
import { ListingPageComponent, ActionBar } from './ListingPage';
const { UUID } = types;
const noop = () => null;
@ -16,17 +21,8 @@ describe('ListingPage', () => {
const currentUser = createCurrentUser('user-2');
const listing1 = createListing('listing1', {}, { author: createUser('user-1') });
const getListing = () => listing1;
const props = {
flattenedRoutes: [
// Fake route since a circular dependency prevents using the
// full routesConfiguration here
{
path: '/l/:id',
exact: true,
name: 'ListingPageCanonical',
component: noop,
},
],
location: { search: '' },
history: {
push: () => console.log('HistoryPush called'),

View file

@ -1,18 +1,10 @@
import React from 'react';
import { renderDeep } from '../../util/test-helpers';
import { RoutesProvider } from '../../components';
import routesConfiguration from '../../routesConfiguration';
import { flattenRoutes } from '../../util/routes';
import LoginForm from './LoginForm';
describe('LoginForm', () => {
it('matches snapshot', () => {
const flattenedRoutes = flattenRoutes(routesConfiguration);
const tree = renderDeep(
<RoutesProvider flattenedRoutes={flattenedRoutes}>
<LoginForm />
</RoutesProvider>
);
const tree = renderDeep(<LoginForm />);
expect(tree).toMatchSnapshot();
});
});

View file

@ -3,7 +3,7 @@ import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import routeConfiguration from '../../routeConfiguration';
import { createResourceLocatorString } from '../../util/routes';
import * as propTypes from '../../util/propTypes';
import { sendVerificationEmail } from '../../ducks/user.duck';
@ -40,7 +40,6 @@ export class NotFoundPageComponent extends Component {
sendVerificationEmailInProgress,
sendVerificationEmailError,
onResendVerificationEmail,
flattenedRoutes,
intl,
} = this.props;
@ -52,7 +51,9 @@ export class NotFoundPageComponent extends Component {
const { search, selectedPlace } = values.location;
const { origin, bounds, country } = selectedPlace;
const searchParams = { address: search, origin, bounds, country };
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
history.push(
createResourceLocatorString('SearchPage', routeConfiguration(), {}, searchParams)
);
};
return (
@ -99,7 +100,7 @@ NotFoundPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { bool, func, instanceOf, number, object, shape, array } = PropTypes;
const { bool, func, instanceOf, number, object, shape } = PropTypes;
NotFoundPageComponent.propTypes = {
authInfoError: instanceOf(Error),
@ -127,9 +128,6 @@ NotFoundPageComponent.propTypes = {
push: func.isRequired,
}).isRequired,
location: shape({ state: object }).isRequired,
// from withFlattenedRoutes
flattenedRoutes: array.isRequired,
};
const mapStateToProps = state => {
@ -166,11 +164,8 @@ const mapDispatchToProps = dispatch => ({
onResendVerificationEmail: () => dispatch(sendVerificationEmail()),
});
const NotFoundPage = compose(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
injectIntl,
withFlattenedRoutes
)(NotFoundPageComponent);
const NotFoundPage = compose(connect(mapStateToProps, mapDispatchToProps), withRouter, injectIntl)(
NotFoundPageComponent
);
export default NotFoundPage;

View file

@ -21,7 +21,6 @@ describe('NotFoundPageComponent', () => {
sendVerificationEmailInProgress={false}
onResendVerificationEmail={noop}
intl={fakeIntl}
flattenedRoutes={[]}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -6,7 +6,7 @@ import { withRouter } from 'react-router-dom';
import { debounce, isEqual, unionWith } from 'lodash';
import classNames from 'classnames';
import config from '../../config';
import { withFlattenedRoutes } from '../../util/contextHelpers';
import routeConfiguration from '../../routeConfiguration';
import { googleLatLngToSDKLatLng, googleBoundsToSDKBounds } from '../../util/googleMaps';
import { createResourceLocatorString } from '../../util/routes';
import { createSlug, parse, stringify } from '../../util/urlHelpers';
@ -77,7 +77,7 @@ export class SearchPageComponent extends Component {
// We are using Google Maps idle event instead of bounds_changed, since it will not be fired
// too often (in the middle of map's pan or zoom activity)
onIdle(googleMap) {
const { flattenedRoutes, history, location } = this.props;
const { history, location } = this.props;
const { address, country, boundsChanged } = parse(location.search, {
latlng: ['origin'],
@ -93,7 +93,9 @@ export class SearchPageComponent extends Component {
const origin = googleLatLngToSDKLatLng(viewportBounds.getCenter());
const searchParams = { address, origin, bounds, country, boundsChanged: true };
history.push(createResourceLocatorString('SearchPage', flattenedRoutes, {}, searchParams));
history.push(
createResourceLocatorString('SearchPage', routeConfiguration(), {}, searchParams)
);
} else {
this.useLocationSearchBounds = false;
this.modalOpenedBoundsChange = false;
@ -137,7 +139,6 @@ export class SearchPageComponent extends Component {
currentUser,
currentUserHasListings,
currentUserHasOrders,
flattenedRoutes,
history,
intl,
isAuthenticated,
@ -245,7 +246,7 @@ export class SearchPageComponent extends Component {
const schemaDescription = intl.formatMessage({ id: 'SearchPage.schemaDescription' });
const schemaListings = listings.map((l, i) => {
const title = l.attributes.title;
const pathToItem = createResourceLocatorString('ListingPage', flattenedRoutes, {
const pathToItem = createResourceLocatorString('ListingPage', routeConfiguration(), {
id: l.id.uuid,
slug: createSlug(title),
});
@ -368,7 +369,7 @@ SearchPageComponent.defaultProps = {
sendVerificationEmailError: null,
};
const { array, arrayOf, bool, func, instanceOf, number, oneOf, object, shape, string } = PropTypes;
const { array, bool, func, instanceOf, number, oneOf, object, shape, string } = PropTypes;
SearchPageComponent.propTypes = {
authInfoError: instanceOf(Error),
@ -394,9 +395,6 @@ SearchPageComponent.propTypes = {
sendVerificationEmailError: instanceOf(Error),
onResendVerificationEmail: func.isRequired,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withRouter
history: shape({
push: func.isRequired,
@ -462,12 +460,9 @@ const mapDispatchToProps = dispatch => ({
onSearchMapListings: searchParams => dispatch(searchMapListings(searchParams)),
});
const SearchPage = compose(
connect(mapStateToProps, mapDispatchToProps),
injectIntl,
withFlattenedRoutes,
withRouter
)(SearchPageComponent);
const SearchPage = compose(connect(mapStateToProps, mapDispatchToProps), injectIntl, withRouter)(
SearchPageComponent
);
SearchPage.loadData = (params, search) => {
const queryParams = parse(search, {

View file

@ -39,7 +39,6 @@ describe('SearchPageComponent', () => {
onLogout: noop,
onManageDisableScrolling: noop,
onSearchMapListings: noop,
flattenedRoutes: [],
sendVerificationEmailInProgress: false,
onResendVerificationEmail: noop,
};

View file

@ -2,7 +2,6 @@ import React, { PropTypes } from 'react';
import { isEmpty } from 'lodash';
import classNames from 'classnames';
import { NamedLink } from '../../components';
import * as allExamples from '../../examples';
import css from './StyleguidePage.css';
@ -161,6 +160,17 @@ const examplesFor = (examples, group, componentName, exampleName) => {
};
const StyleguidePage = props => {
// TODO: importing all the examples will affect the module bundling
// since examples call routeConfiguration without function wrapping
// Wurthermore, it would be nice to exclude styleguide away from actual app
let allExamples = [];
try {
allExamples = require('../../examples'); // eslint-disable-line global-require
} catch (e) {
// eslint-disable-next-line no-console
console.warn('require(): The file "../../examples.js" could not be loaded.');
}
const { params, raw } = props;
const group = params.group ? decodeURIComponent(params.group) : ALL;
const componentName = params.component || ALL;

View file

@ -1,79 +1,42 @@
import AuthenticationPage from './AuthenticationPage/AuthenticationPage';
import BookingDatesForm from './BookingDatesForm/BookingDatesForm';
import CheckoutPage from './CheckoutPage/CheckoutPage';
import ContactDetailsForm from './ContactDetailsForm/ContactDetailsForm';
import ContactDetailsPage from './ContactDetailsPage/ContactDetailsPage';
import EditListingDescriptionForm from './EditListingDescriptionForm/EditListingDescriptionForm';
import EditListingLocationForm from './EditListingLocationForm/EditListingLocationForm';
import EditListingPage from './EditListingPage/EditListingPage';
import EditListingPhotosForm from './EditListingPhotosForm/EditListingPhotosForm';
import EditListingPricingForm from './EditListingPricingForm/EditListingPricingForm';
import EmailVerificationForm from './EmailVerificationForm/EmailVerificationForm';
import EmailVerificationPage from './EmailVerificationPage/EmailVerificationPage';
import InboxPage from './InboxPage/InboxPage';
import LandingPage from './LandingPage/LandingPage';
import ListingPage from './ListingPage/ListingPage';
import LocationSearchForm from './LocationSearchForm/LocationSearchForm';
import LoginForm from './LoginForm/LoginForm';
import ManageListingsPage from './ManageListingsPage/ManageListingsPage';
import NotFoundPage from './NotFoundPage/NotFoundPage';
import OrderPage from './OrderPage/OrderPage';
import PasswordChangeForm from './PasswordChangeForm/PasswordChangeForm';
import PasswordChangePage from './PasswordChangePage/PasswordChangePage';
import PasswordRecoveryForm from './PasswordRecoveryForm/PasswordRecoveryForm';
import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage';
import PasswordResetForm from './PasswordResetForm/PasswordResetForm';
import PasswordResetPage from './PasswordResetPage/PasswordResetPage';
import PayoutDetailsForm from './PayoutDetailsForm/PayoutDetailsForm';
import PayoutPreferencesPage from './PayoutPreferencesPage/PayoutPreferencesPage';
import ProfilePage from './ProfilePage/ProfilePage';
import ProfileSettingsForm from './ProfileSettingsForm/ProfileSettingsForm';
import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage';
import SalePage from './SalePage/SalePage';
import SearchPage from './SearchPage/SearchPage';
import SecurityPage from './SecurityPage/SecurityPage';
import SignupForm from './SignupForm/SignupForm';
import StripePaymentForm from './StripePaymentForm/StripePaymentForm';
import StyleguidePage from './StyleguidePage/StyleguidePage';
import TopbarSearchForm from './TopbarSearchForm/TopbarSearchForm';
export { default as AuthenticationPage } from './AuthenticationPage/AuthenticationPage';
export { default as BookingDatesForm } from './BookingDatesForm/BookingDatesForm';
export { default as CheckoutPage } from './CheckoutPage/CheckoutPage';
export { default as ContactDetailsForm } from './ContactDetailsForm/ContactDetailsForm';
export { default as ContactDetailsPage } from './ContactDetailsPage/ContactDetailsPage';
export {
AuthenticationPage,
BookingDatesForm,
CheckoutPage,
ContactDetailsForm,
ContactDetailsPage,
EditListingDescriptionForm,
EditListingLocationForm,
EditListingPage,
EditListingPhotosForm,
EditListingPricingForm,
EmailVerificationForm,
EmailVerificationPage,
InboxPage,
LandingPage,
ListingPage,
LocationSearchForm,
LoginForm,
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordChangeForm,
PasswordChangePage,
PasswordRecoveryForm,
PasswordRecoveryPage,
PasswordResetForm,
PasswordResetPage,
PayoutDetailsForm,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsForm,
ProfileSettingsPage,
SalePage,
SearchPage,
SecurityPage,
SignupForm,
StripePaymentForm,
StyleguidePage,
TopbarSearchForm,
};
default as EditListingDescriptionForm,
} from './EditListingDescriptionForm/EditListingDescriptionForm';
export {
default as EditListingLocationForm,
} from './EditListingLocationForm/EditListingLocationForm';
export { default as EditListingPage } from './EditListingPage/EditListingPage';
export { default as EditListingPhotosForm } from './EditListingPhotosForm/EditListingPhotosForm';
export { default as EditListingPricingForm } from './EditListingPricingForm/EditListingPricingForm';
export { default as EmailVerificationForm } from './EmailVerificationForm/EmailVerificationForm';
export { default as EmailVerificationPage } from './EmailVerificationPage/EmailVerificationPage';
export { default as InboxPage } from './InboxPage/InboxPage';
export { default as LandingPage } from './LandingPage/LandingPage';
export { default as ListingPage } from './ListingPage/ListingPage';
export { default as LocationSearchForm } from './LocationSearchForm/LocationSearchForm';
export { default as LoginForm } from './LoginForm/LoginForm';
export { default as ManageListingsPage } from './ManageListingsPage/ManageListingsPage';
export { default as NotFoundPage } from './NotFoundPage/NotFoundPage';
export { default as OrderPage } from './OrderPage/OrderPage';
export { default as PasswordChangeForm } from './PasswordChangeForm/PasswordChangeForm';
export { default as PasswordChangePage } from './PasswordChangePage/PasswordChangePage';
export { default as PasswordRecoveryForm } from './PasswordRecoveryForm/PasswordRecoveryForm';
export { default as PasswordRecoveryPage } from './PasswordRecoveryPage/PasswordRecoveryPage';
export { default as PasswordResetForm } from './PasswordResetForm/PasswordResetForm';
export { default as PasswordResetPage } from './PasswordResetPage/PasswordResetPage';
export { default as PayoutDetailsForm } from './PayoutDetailsForm/PayoutDetailsForm';
export { default as PayoutPreferencesPage } from './PayoutPreferencesPage/PayoutPreferencesPage';
export { default as ProfilePage } from './ProfilePage/ProfilePage';
export { default as ProfileSettingsForm } from './ProfileSettingsForm/ProfileSettingsForm';
export { default as ProfileSettingsPage } from './ProfileSettingsPage/ProfileSettingsPage';
export { default as SalePage } from './SalePage/SalePage';
export { default as SearchPage } from './SearchPage/SearchPage';
export { default as SecurityPage } from './SecurityPage/SecurityPage';
export { default as SignupForm } from './SignupForm/SignupForm';
export { default as StripePaymentForm } from './StripePaymentForm/StripePaymentForm';
export { default as StyleguidePage } from './StyleguidePage/StyleguidePage';
export { default as TopbarSearchForm } from './TopbarSearchForm/TopbarSearchForm';

View file

@ -22,7 +22,7 @@ import * as sample from './util/sample';
import config from './config';
import { authInfo } from './ducks/Auth.duck';
import { fetchCurrentUser } from './ducks/user.duck';
import routeConfiguration from './routesConfiguration';
import routeConfiguration from './routeConfiguration';
import * as log from './util/log';
import './marketplaceIndex.css';
@ -81,7 +81,14 @@ if (typeof window !== 'undefined') {
if (config.dev) {
// Expose stuff for the browser REPL
window.app = { config, sdk, sdkTypes: types, store, sample, routeConfiguration };
window.app = {
config,
sdk,
sdkTypes: types,
store,
sample,
routeConfiguration: routeConfiguration(),
};
}
}

331
src/routeConfiguration.js Normal file
View file

@ -0,0 +1,331 @@
import React from 'react';
import {
AuthenticationPage,
CheckoutPage,
ContactDetailsPage,
EditListingPage,
InboxPage,
LandingPage,
ListingPage,
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordChangePage,
PasswordResetPage,
PasswordRecoveryPage,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsPage,
SalePage,
SearchPage,
SecurityPage,
StyleguidePage,
EmailVerificationPage,
} from './containers';
// routeConfiguration needs to initialize containers first
// Otherwise, components will import form container eventually and
// at that point css bundling / imports will happen in wrong order.
import { NamedRedirect } from './components';
export const ACCOUNT_SETTINGS_PAGES = ['ContactDetailsPage', 'PasswordChangePage'];
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID
const draftId = '00000000-0000-0000-0000-000000000000';
const draftSlug = 'draft';
const RedirectToLandingPage = () => <NamedRedirect name="LandingPage" />;
const routeConfiguration = () => {
return [
{ path: '/', exact: true, name: 'LandingPage', component: props => <LandingPage {...props} /> },
{
path: '/s',
exact: true,
name: 'SearchPage',
component: props => <SearchPage {...props} />,
loadData: SearchPage.loadData,
},
{
path: '/s/filters',
exact: true,
name: 'SearchFiltersPage',
component: props => <SearchPage {...props} tab="filters" />,
loadData: SearchPage.loadData,
},
{
path: '/s/listings',
exact: true,
name: 'SearchListingsPage',
component: props => <SearchPage {...props} tab="listings" />,
loadData: SearchPage.loadData,
},
{
path: '/s/map',
exact: true,
name: 'SearchMapPage',
component: props => <SearchPage {...props} tab="map" />,
loadData: SearchPage.loadData,
},
{
path: '/l',
exact: true,
name: 'ListingBasePage',
component: RedirectToLandingPage,
},
{
path: '/l/:slug/:id',
exact: true,
name: 'ListingPage',
component: props => <ListingPage {...props} tab="listing" />,
loadData: ListingPage.loadData,
},
{
path: '/l/:slug/:id/book',
exact: true,
name: 'ListingPage',
component: props => <ListingPage {...props} tab="book" />,
loadData: ListingPage.loadData,
},
{
path: '/l/:slug/:id/checkout',
auth: true,
exact: true,
name: 'CheckoutPage',
component: props => <CheckoutPage {...props} />,
setInitialValues: CheckoutPage.setInitialValues,
},
{
auth: true,
path: '/l/new',
exact: true,
name: 'NewListingPage',
component: () => (
<NamedRedirect
name="EditListingPage"
params={{ slug: draftSlug, id: draftId, type: 'new', tab: 'description' }}
/>
),
},
{
auth: true,
path: '/l/:slug/:id/:type/:tab',
exact: true,
name: 'EditListingPage',
component: props => <EditListingPage {...props} />,
loadData: EditListingPage.loadData,
},
// Canonical path should be after the `/l/new` path since they
// conflict and `new` is not a valid listing UUID.
{
path: '/l/:id',
exact: true,
name: 'ListingPageCanonical',
component: props => <ListingPage {...props} tab="listing" />,
loadData: ListingPage.loadData,
},
{
path: '/u',
exact: true,
name: 'ProfileBasePage',
component: RedirectToLandingPage,
},
{
path: '/u/:displayName',
exact: true,
name: 'ProfilePage',
component: props => <ProfilePage {...props} />,
},
{
path: '/profile-settings',
auth: true,
exact: true,
name: 'ProfileSettingsPage',
component: props => <ProfileSettingsPage {...props} />,
},
{
path: '/login',
exact: true,
name: 'LoginPage',
component: props => <AuthenticationPage {...props} tab="login" />,
},
{
path: '/signup',
exact: true,
name: 'SignupPage',
component: props => <AuthenticationPage {...props} tab="signup" />,
},
{
path: '/recover-password',
exact: true,
name: 'PasswordRecoveryPage',
component: props => <PasswordRecoveryPage {...props} />,
},
{
path: '/inbox',
auth: true,
exact: true,
name: 'InboxBasePage',
component: () => <NamedRedirect name="InboxPage" params={{ tab: 'sales' }} />,
},
{
path: '/inbox/:tab',
auth: true,
exact: true,
name: 'InboxPage',
component: props => <InboxPage {...props} />,
loadData: InboxPage.loadData,
},
{
path: '/order/:id',
auth: true,
exact: true,
name: 'OrderPage',
component: RedirectToLandingPage,
},
{
path: '/order/:id/details',
auth: true,
exact: true,
name: 'OrderDetailsPage',
component: props => <OrderPage {...props} tab="details" />,
loadData: OrderPage.loadData,
},
{
path: '/order/:id/discussion',
auth: true,
exact: true,
name: 'OrderDiscussionPage',
component: props => <OrderPage {...props} tab="discussion" />,
loadData: OrderPage.loadData,
},
{
path: '/sale/:id',
auth: true,
exact: true,
name: 'SalePage',
component: props => <SalePage {...props} tab="discussion" />,
},
{
path: '/sale/:id/details',
auth: true,
exact: true,
name: 'SaleDetailsPage',
component: props => <SalePage {...props} tab="details" />,
loadData: SalePage.loadData,
},
{
path: '/sale/:id/discussion',
auth: true,
exact: true,
name: 'SaleDiscussionPage',
component: props => <SalePage {...props} tab="discussion" />,
loadData: SalePage.loadData,
},
{
path: '/listings',
auth: true,
exact: true,
name: 'ManageListingsPage',
component: props => <ManageListingsPage {...props} />,
loadData: ManageListingsPage.loadData,
},
{
path: '/account',
auth: true,
exact: true,
name: 'AccountSettingsPage',
component: () => <NamedRedirect name="ContactDetailsPage" />,
},
{
path: '/account/contact-details',
auth: true,
exact: true,
name: 'ContactDetailsPage',
component: props => <ContactDetailsPage {...props} />,
loadData: ContactDetailsPage.loadData,
},
{
path: '/account/change-password',
auth: true,
exact: true,
name: 'PasswordChangePage',
component: props => <PasswordChangePage {...props} />,
},
{
path: '/account/payout-preferences',
auth: true,
exact: true,
name: 'PayoutPreferencesPage',
component: props => <PayoutPreferencesPage {...props} />,
},
{
path: '/account/security',
auth: true,
exact: true,
name: 'SecurityPage',
component: props => <SecurityPage {...props} />,
},
{
path: '/styleguide',
exact: true,
name: 'Styleguide',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/g/:group',
exact: true,
name: 'StyleguideGroup',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/c/:component',
exact: true,
name: 'StyleguideComponent',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/c/:component/:example',
exact: true,
name: 'StyleguideComponentExample',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/c/:component/:example/raw',
exact: true,
name: 'StyleguideComponentExampleRaw',
component: props => <StyleguidePage raw {...props} />,
},
{
path: '/notfound',
exact: true,
name: 'NotFoundPage',
component: props => <NotFoundPage {...props} />,
},
// Do not change this path!
//
// The API expects that the Starter App implements /reset-password endpoint
{
path: '/reset-password',
exact: true,
name: 'PasswordResetPage',
component: props => <PasswordResetPage {...props} />,
},
// Do not change this path!
//
// The API expects that the Starter App implements /verify-email endpoint
{
path: '/verify-email',
auth: true,
authPage: 'LoginPage',
exact: true,
name: 'EmailVerificationPage',
component: props => <EmailVerificationPage {...props} />,
},
];
};
export default routeConfiguration;

View file

@ -1,337 +0,0 @@
import React from 'react';
import { NamedRedirect } from './components';
import {
AuthenticationPage,
CheckoutPage,
ContactDetailsPage,
EditListingPage,
InboxPage,
LandingPage,
ListingPage,
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordChangePage,
PasswordResetPage,
PasswordRecoveryPage,
PayoutPreferencesPage,
ProfilePage,
ProfileSettingsPage,
SalePage,
SearchPage,
SecurityPage,
StyleguidePage,
EmailVerificationPage,
} from './containers';
export const ACCOUNT_SETTINGS_PAGES = ['ContactDetailsPage', 'PasswordChangePage'];
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID
const draftId = '00000000-0000-0000-0000-000000000000';
const draftSlug = 'draft';
const RedirectToLandingPage = () => <NamedRedirect name="LandingPage" />;
const routesConfiguration = [
{ path: '/', exact: true, name: 'LandingPage', component: props => <LandingPage {...props} /> },
{
path: '/s',
exact: true,
name: 'SearchPage',
component: props => <SearchPage {...props} />,
loadData: (params, search) => SearchPage.loadData(params, search),
routes: [
{
path: '/s/filters',
exact: true,
name: 'SearchFiltersPage',
component: props => <SearchPage {...props} tab="filters" />,
loadData: (params, search) => SearchPage.loadData(params, search),
},
{
path: '/s/listings',
exact: true,
name: 'SearchListingsPage',
component: props => <SearchPage {...props} tab="listings" />,
loadData: (params, search) => SearchPage.loadData(params, search),
},
{
path: '/s/map',
exact: true,
name: 'SearchMapPage',
component: props => <SearchPage {...props} tab="map" />,
loadData: (params, search) => SearchPage.loadData(params, search),
},
],
},
{
path: '/l',
exact: true,
name: 'ListingBasePage',
component: RedirectToLandingPage,
routes: [
{
path: '/l/:slug/:id',
exact: true,
name: 'ListingPage',
loadData: (params, search) => ListingPage.loadData(params, search),
component: props => <ListingPage {...props} tab="listing" />,
},
{
path: '/l/:slug/:id/book',
exact: true,
name: 'ListingPage',
loadData: (params, search) => ListingPage.loadData(params, search),
component: props => <ListingPage {...props} tab="book" />,
},
{
path: '/l/:slug/:id/checkout',
auth: true,
exact: true,
name: 'CheckoutPage',
setInitialValues: initialValues => CheckoutPage.setInitialValues(initialValues),
component: props => <CheckoutPage {...props} />,
},
{
auth: true,
path: '/l/new',
exact: true,
name: 'NewListingPage',
component: () => (
<NamedRedirect
name="EditListingPage"
params={{ slug: draftSlug, id: draftId, type: 'new', tab: 'description' }}
/>
),
},
{
auth: true,
path: '/l/:slug/:id/:type/:tab',
exact: true,
name: 'EditListingPage',
component: props => <EditListingPage {...props} />,
loadData: (params, search) => EditListingPage.loadData(params, search),
},
// Canonical path should be after the `/l/new` path since they
// conflict and `new` is not a valid listing UUID.
{
path: '/l/:id',
exact: true,
name: 'ListingPageCanonical',
loadData: (params, search) => ListingPage.loadData(params, search),
component: props => <ListingPage {...props} tab="listing" />,
},
],
},
{
path: '/u',
exact: true,
name: 'ProfileBasePage',
component: RedirectToLandingPage,
routes: [
{
path: '/u/:displayName',
exact: true,
name: 'ProfilePage',
component: props => <ProfilePage {...props} />,
},
],
},
{
path: '/profile-settings',
auth: true,
exact: true,
name: 'ProfileSettingsPage',
component: props => <ProfileSettingsPage {...props} />,
},
{
path: '/login',
exact: true,
name: 'LoginPage',
component: props => <AuthenticationPage {...props} tab="login" />,
},
{
path: '/signup',
exact: true,
name: 'SignupPage',
component: props => <AuthenticationPage {...props} tab="signup" />,
},
{
path: '/recover-password',
exact: true,
name: 'PasswordRecoveryPage',
component: props => <PasswordRecoveryPage {...props} />,
},
{
path: '/inbox',
auth: true,
exact: true,
name: 'InboxBasePage',
component: () => <NamedRedirect name="InboxPage" params={{ tab: 'sales' }} />,
},
{
path: '/inbox/:tab',
auth: true,
exact: true,
name: 'InboxPage',
component: props => <InboxPage {...props} />,
loadData: (params, search) => InboxPage.loadData(params, search),
},
{
path: '/order/:id',
auth: true,
exact: true,
name: 'OrderPage',
component: RedirectToLandingPage,
routes: [
{
path: '/order/:id/details',
auth: true,
exact: true,
name: 'OrderDetailsPage',
component: props => <OrderPage {...props} tab="details" />,
loadData: params => OrderPage.loadData(params),
},
{
path: '/order/:id/discussion',
auth: true,
exact: true,
name: 'OrderDiscussionPage',
component: props => <OrderPage {...props} tab="discussion" />,
loadData: params => OrderPage.loadData(params),
},
],
},
{
path: '/sale/:id',
auth: true,
exact: true,
name: 'SalePage',
component: props => <SalePage {...props} tab="discussion" />,
routes: [
{
path: '/sale/:id/details',
auth: true,
exact: true,
name: 'SaleDetailsPage',
component: props => <SalePage {...props} tab="details" />,
loadData: params => SalePage.loadData(params),
},
{
path: '/sale/:id/discussion',
auth: true,
exact: true,
name: 'SaleDiscussionPage',
component: props => <SalePage {...props} tab="discussion" />,
loadData: params => SalePage.loadData(params),
},
],
},
{
path: '/listings',
auth: true,
exact: true,
name: 'ManageListingsPage',
component: props => <ManageListingsPage {...props} />,
loadData: (params, search) => ManageListingsPage.loadData(params, search),
},
{
path: '/account',
auth: true,
exact: true,
name: 'AccountSettingsPage',
component: () => <NamedRedirect name="ContactDetailsPage" />,
routes: [
{
path: '/account/contact-details',
auth: true,
exact: true,
name: 'ContactDetailsPage',
component: props => <ContactDetailsPage {...props} />,
loadData: params => ContactDetailsPage.loadData(params),
},
{
path: '/account/change-password',
auth: true,
exact: true,
name: 'PasswordChangePage',
component: props => <PasswordChangePage {...props} />,
},
{
path: '/account/payout-preferences',
auth: true,
exact: true,
name: 'PayoutPreferencesPage',
component: props => <PayoutPreferencesPage {...props} />,
},
{
path: '/account/security',
auth: true,
exact: true,
name: 'SecurityPage',
component: props => <SecurityPage {...props} />,
},
],
},
{
path: '/styleguide',
exact: true,
name: 'Styleguide',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/g/:group',
exact: true,
name: 'StyleguideGroup',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/c/:component',
exact: true,
name: 'StyleguideComponent',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/c/:component/:example',
exact: true,
name: 'StyleguideComponentExample',
component: props => <StyleguidePage {...props} />,
},
{
path: '/styleguide/c/:component/:example/raw',
exact: true,
name: 'StyleguideComponentExampleRaw',
component: props => <StyleguidePage raw {...props} />,
},
{
path: '/notfound',
exact: true,
name: 'NotFoundPage',
component: props => <NotFoundPage {...props} />,
},
// Do not change this path!
//
// The API expects that the Starter App implements /reset-password endpoint
{
path: '/reset-password',
exact: true,
name: 'PasswordResetPage',
component: props => <PasswordResetPage {...props} />,
},
// Do not change this path!
//
// The API expects that the Starter App implements /verify-email endpoint
{
path: '/verify-email',
auth: true,
authPage: 'LoginPage',
exact: true,
name: 'EmailVerificationPage',
component: props => <EmailVerificationPage {...props} />,
},
];
export default routesConfiguration;

View file

@ -1,7 +0,0 @@
exports[`util/contextHelpers.js withFlattenedRoutes should inject the provided routes 1`] = `<withFlattenedRoutes(CompComp) />`;
exports[`util/contextHelpers.js withFlattenedRoutes should inject the provided routes 2`] = `
<div>
SomePage
</div>
`;

View file

@ -1,29 +1,5 @@
import React, { PropTypes, Component as ReactComponent } from 'react';
import { throttle } from 'lodash';
import * as propTypes from './propTypes';
/**
* A higher order component (HOC) to take the flattened routes from
* the context that the RoutesProvider component has provided.
*
* Injects the routes as the `flattenedRoutes` prop in the given
* component. Works similarly as `withRouter` in React Router.
*/
export const withFlattenedRoutes = Component => {
const WithFlattenedRoutesComponent = (props, context) => (
<Component flattenedRoutes={context.flattenedRoutes} {...props} />
);
WithFlattenedRoutesComponent.displayName = `withFlattenedRoutes(${Component.displayName || Component.name})`;
const { arrayOf } = PropTypes;
WithFlattenedRoutesComponent.contextTypes = {
flattenedRoutes: arrayOf(propTypes.route).isRequired,
};
return WithFlattenedRoutesComponent;
};
/**
* A higher order component (HOC) to take the togglePageClassNames function from

View file

@ -1,32 +0,0 @@
import React, { PropTypes } from 'react';
import { Page, RoutesProvider } from '../components';
import routesConfiguration from '../routesConfiguration';
import { flattenRoutes } from './routes';
import { renderDeep, renderShallow } from './test-helpers';
import * as propTypes from './propTypes';
import { withFlattenedRoutes, withTogglePageClassNames } from './contextHelpers';
const { arrayOf, func } = PropTypes;
describe('util/contextHelpers.js', () => {
describe('withFlattenedRoutes', () => {
it('should inject the provided routes', () => {
const CompComp = props => <div>{props.flattenedRoutes[0].name}</div>;
CompComp.propTypes = { flattenedRoutes: arrayOf(propTypes.route).isRequired };
const Comp = withFlattenedRoutes(CompComp);
const routes = [{ name: 'SomePage', path: 'some-page', component: () => null }];
const shallowTree = renderShallow(
<RoutesProvider flattenedRoutes={routes}>
<Comp />
</RoutesProvider>
);
expect(shallowTree).toMatchSnapshot();
const deepTree = renderDeep(
<RoutesProvider flattenedRoutes={routes}>
<Comp />
</RoutesProvider>
);
expect(deepTree).toMatchSnapshot();
});
});
});

View file

@ -3,31 +3,14 @@ import { matchPath } from 'react-router-dom';
import pathToRegexp from 'path-to-regexp';
import { stringify } from './urlHelpers';
// Flatten the routes config.
// TODO: flatten the original config and remove this function
export const flattenRoutes = routes =>
routes.reduce(
(flatRoutes, route) => {
const r = { ...route };
delete r.routes;
flatRoutes.push(r);
if (route.routes) {
return flatRoutes.concat(flattenRoutes(route.routes));
}
return flatRoutes;
},
[]
);
const findRouteByName = (nameToFind, flattenedRoutes) =>
find(flattenedRoutes, route => route.name === nameToFind);
const findRouteByName = (nameToFind, routes) => find(routes, route => route.name === nameToFind);
/**
* E.g. ```const toListingPath = toPathByRouteName('ListingPage', routes);```
* Then we can generate listing paths with given params (```toListingPath({ id: uuidX })```)
*/
const toPathByRouteName = (nameToFind, flattenedRoutes) => {
const route = findRouteByName(nameToFind, flattenedRoutes);
const toPathByRouteName = (nameToFind, routes) => {
const route = findRouteByName(nameToFind, routes);
if (!route) {
throw new Error(`Path "${nameToFind}" was not found.`);
}
@ -37,8 +20,8 @@ const toPathByRouteName = (nameToFind, flattenedRoutes) => {
/**
* Shorthand for single path call. (```pathByRouteName('ListingPage', routes, { id: uuidX });```)
*/
export const pathByRouteName = (nameToFind, flattenedRoutes, params = {}) =>
toPathByRouteName(nameToFind, flattenedRoutes)(params);
export const pathByRouteName = (nameToFind, routes, params = {}) =>
toPathByRouteName(nameToFind, routes)(params);
/**
* Find the matching routes and their params for the given pathname
@ -48,11 +31,8 @@ export const pathByRouteName = (nameToFind, flattenedRoutes, params = {}) =>
*
* @return {Array<{ route, params }>} - All matches as { route, params } objects
*/
export const matchPathname = (pathname, routesConfiguration) => {
// TODO: remove flattening when routesConfiguration is flat
const flattenedRoutes = flattenRoutes(routesConfiguration);
return flattenedRoutes.reduce(
export const matchPathname = (pathname, routeConfiguration) => {
return routeConfiguration.reduce(
(matches, route) => {
const { path, exact = false } = route;
const match = matchPath(pathname, { path, exact });
@ -74,13 +54,13 @@ export const matchPathname = (pathname, routesConfiguration) => {
*/
export const createResourceLocatorString = (
routeName,
flattenedRoutes,
routes,
pathParams = {},
searchParams = {}
) => {
const searchQuery = stringify(searchParams);
const includeSearchQuery = searchQuery.length > 0 ? `?${searchQuery}` : '';
const path = pathByRouteName(routeName, flattenedRoutes, pathParams);
const path = pathByRouteName(routeName, routes, pathParams);
return `${path}${includeSearchQuery}`;
};
@ -91,12 +71,12 @@ export const createResourceLocatorString = (
* `dispatch(PageComponent.setInitialValues({ listing, bookingDates }));`
*
* @param {String} nameToFind - Route name
* @param {Array<{ route }>} flattenedRoutes - Route configuration as flattened array.
* @param {Array<{ route }>} routes - Route configuration as flat array.
*
* @return {Route} - Route that matches the given route name.
*/
export const findRouteByRouteName = (nameToFind, flattenedRoutes) => {
const route = findRouteByName(nameToFind, flattenedRoutes);
export const findRouteByRouteName = (nameToFind, routes) => {
const route = findRouteByName(nameToFind, routes);
if (!route) {
throw new Error(`Component "${nameToFind}" was not found.`);
}

View file

@ -1,64 +1,48 @@
import React, { PropTypes } from 'react';
import { RoutesProvider } from '../components';
import routesConfiguration from '../routesConfiguration';
import routeConfiguration from '../routeConfiguration';
import { renderDeep, renderShallow } from './test-helpers';
import * as propTypes from './propTypes';
import { createResourceLocatorString, flattenRoutes, findRouteByRouteName } from './routes';
import { createResourceLocatorString, findRouteByRouteName } from './routes';
const { arrayOf } = PropTypes;
describe('util/routes.js', () => {
describe('createResourceLocatorString', () => {
const flattenedRoutes = flattenRoutes(routesConfiguration);
const routes = routeConfiguration();
it('should return meaningful strings if parameters are not needed', () => {
// default links without params in path or search query
expect(
createResourceLocatorString('SearchPage', flattenedRoutes, undefined, undefined)
).toEqual('/s');
expect(createResourceLocatorString('SearchPage', flattenedRoutes, {}, {})).toEqual('/s');
expect(createResourceLocatorString('SearchPage', routes, undefined, undefined)).toEqual('/s');
expect(createResourceLocatorString('SearchPage', routes, {}, {})).toEqual('/s');
});
it('should return meaningful strings with path parameters', () => {
expect(
createResourceLocatorString(
'ListingPage',
flattenedRoutes,
{ id: '1234', slug: 'nice-listing' },
{}
)
createResourceLocatorString('ListingPage', routes, { id: '1234', slug: 'nice-listing' }, {})
).toEqual('/l/nice-listing/1234');
expect(() => createResourceLocatorString('ListingPage', routes, {}, {})).toThrowError(
TypeError('Expected "slug" to be defined')
);
expect(() =>
createResourceLocatorString('ListingPage', flattenedRoutes, {}, {})).toThrowError(
createResourceLocatorString('ListingPage', routes, { id: '1234' }, {})).toThrowError(
TypeError('Expected "slug" to be defined')
);
expect(() =>
createResourceLocatorString(
'ListingPage',
flattenedRoutes,
{ id: '1234' },
{}
)).toThrowError(TypeError('Expected "slug" to be defined'));
expect(() =>
createResourceLocatorString(
'ListingPage',
flattenedRoutes,
routes,
{ slug: 'nice-listing' },
{}
)).toThrowError(TypeError('Expected "id" to be defined'));
});
it('should return meaningful strings with search parameters', () => {
expect(createResourceLocatorString('SearchPage', flattenedRoutes, {}, { page: 2 })).toEqual(
expect(createResourceLocatorString('SearchPage', routes, {}, { page: 2 })).toEqual(
'/s?page=2'
);
expect(
createResourceLocatorString(
'SearchPage',
flattenedRoutes,
{},
{ address: 'Helsinki', page: 2 }
)
createResourceLocatorString('SearchPage', routes, {}, { address: 'Helsinki', page: 2 })
).toEqual('/s?address=Helsinki&page=2');
});
@ -66,7 +50,7 @@ describe('util/routes.js', () => {
expect(
createResourceLocatorString(
'ListingPage',
flattenedRoutes,
routes,
{ id: '1234', slug: 'nice-listing' },
{ extrainfo: true }
)
@ -75,15 +59,15 @@ describe('util/routes.js', () => {
});
describe('findRouteByRouteName', () => {
const flattenedRoutes = flattenRoutes(routesConfiguration);
const routes = routeConfiguration();
it('should return CheckoutPage route', () => {
const foundRoute = findRouteByRouteName('CheckoutPage', flattenedRoutes);
const foundRoute = findRouteByRouteName('CheckoutPage', routes);
expect(foundRoute.name).toEqual('CheckoutPage');
expect(typeof foundRoute.setInitialValues).toEqual('function');
});
it('should throw exception for non-existing route (BlaaBlaaPage)', () => {
expect(() => findRouteByRouteName('BlaaBlaaPage', flattenedRoutes)).toThrowError(
expect(() => findRouteByRouteName('BlaaBlaaPage', routes)).toThrowError(
'Component "BlaaBlaaPage" was not found.'
);
});