New URL schema for creating a listing

The URL will now always have an id and a slug so that new/edit listing
wizards can be generalised. When creating a new listing, temporary id
and slug are put to the URL.
This commit is contained in:
Kimmo Puputti 2017-08-10 12:04:09 +03:00
parent 9378b6ff45
commit f0ca0564dd
8 changed files with 75 additions and 82 deletions

View file

@ -63,7 +63,7 @@ describe('Application', () => {
const defaultAuthPath = '/signup';
const urlRedirects = {
'/l/new': defaultAuthPath,
'/l/listing-title-slug/1234/edit': defaultAuthPath,
'/l/listing-title-slug/1234/new/description': defaultAuthPath,
'/l/listing-title-slug/1234/checkout': defaultAuthPath,
'/u/1234/edit': defaultAuthPath,
'/inbox': defaultAuthPath,

View file

@ -8,10 +8,15 @@ const noop = () => null;
export const NoPhotos = {
component: EditListingWizard,
props: {
params: {
id: 'some-id',
slug: 'some-slug',
type: 'edit',
tab: 'pricing',
},
fetchInProgress: false,
flattenedRoutes: flattenRoutes(routesConfiguration),
history: { push: noop },
selectedTab: 'pricing',
images: [],
listing: createListing('listing1'),
stripeConnected: true,

View file

@ -2,7 +2,6 @@ import React, { PropTypes } from 'react';
import classNames from 'classnames';
import * as propTypes from '../../util/propTypes';
import { ensureListing } from '../../util/data';
import { createSlug } from '../../util/urlHelpers';
import { createResourceLocatorString } from '../../util/routes';
import {
EditListingDescriptionPanel,
@ -58,6 +57,7 @@ TestPanel.propTypes = {
const EditListingWizard = props => {
const {
className,
params,
errors,
fetchInProgress,
flattenedRoutes,
@ -71,34 +71,28 @@ const EditListingWizard = props => {
onUpdateImageOrder,
onUpdateListingDraft,
rootClassName,
selectedTab,
currentUser,
onManageDisableScrolling,
} = props;
const selectedTab = params.tab;
const rootClasses = rootClassName || css.root;
const classes = classNames(rootClasses, className);
const currentListing = ensureListing(listing);
const stepsStatus = stepsActive(currentListing);
const tabParams = tab => {
return { ...params, tab };
};
const tabLink = tab => {
return { name: 'EditListingPage', params: tabParams(tab) };
};
// If selectedStep is not active, redirect to the beginning of wizard
if (!stepsStatus[selectedTab]) {
if (currentListing.id) {
const slug = currentListing.id ? createSlug(currentListing.attributes.title) : null;
return (
<NamedRedirect
name="EditListingDescriptionPage"
params={{ id: currentListing.id.uuid, slug }}
/>
);
}
return <NamedRedirect name="NewListingPage" />;
return <NamedRedirect name="EditListingPage" params={tabParams(DESCRIPTION)} />;
}
const descriptionLinkProps = currentListing.id
? { name: 'EditListingDescriptionPage' }
: { name: 'NewListingPage' };
const onUpsertListingDraft = currentListing.id ? onUpdateListingDraft : onCreateListingDraft;
return (
@ -106,22 +100,24 @@ const EditListingWizard = props => {
<EditListingDescriptionPanel
className={css.panel}
tabLabel="Description"
tabLinkProps={descriptionLinkProps}
tabLinkProps={tabLink(DESCRIPTION)}
selected={selectedTab === DESCRIPTION}
disabled={!stepsStatus[DESCRIPTION]}
listing={listing}
onSubmit={values => {
onUpsertListingDraft(values);
// Redirect to EditListingLocationPage
const pathParams = tabParams(LOCATION);
// Redirect to location tab
history.push(
createResourceLocatorString('EditListingLocationPage', flattenedRoutes, {}, {})
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
}}
/>
<EditListingLocationPanel
className={css.panel}
tabLabel="Location"
tabLinkProps={{ name: 'EditListingLocationPage' }}
tabLinkProps={tabLink(LOCATION)}
selected={selectedTab === LOCATION}
disabled={!stepsStatus[LOCATION]}
listing={listing}
@ -135,31 +131,34 @@ const EditListingWizard = props => {
geolocation: origin,
});
// Redirect to EditListingPricingPage
const pathParams = tabParams(PRICING);
// Redirect to pricing tab
history.push(
createResourceLocatorString('EditListingPricingPage', flattenedRoutes, {}, {})
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
}}
/>
<EditListingPricingPanel
className={css.panel}
tabLabel="Pricing"
tabLinkProps={{ name: 'EditListingPricingPage' }}
tabLinkProps={tabLink(PRICING)}
selected={selectedTab === PRICING}
disabled={!stepsStatus[PRICING]}
listing={listing}
onSubmit={values => {
onUpdateListingDraft(values);
// Redirect to EditListingPhotosPage
const pathParams = tabParams(PHOTOS);
// Redirect to photos tab
history.push(
createResourceLocatorString('EditListingPhotosPage', flattenedRoutes, {}, {})
createResourceLocatorString('EditListingPage', flattenedRoutes, pathParams, {})
);
}}
/>
<EditListingPhotosPanel
className={css.panel}
tabLabel="Photos"
tabLinkProps={{ name: 'EditListingPhotosPage' }}
tabLinkProps={tabLink(PHOTOS)}
selected={selectedTab === PHOTOS}
disabled={!stepsStatus[PHOTOS]}
errors={errors}
@ -192,6 +191,12 @@ const { array, arrayOf, bool, func, object, oneOf, shape, string } = PropTypes;
EditListingWizard.propTypes = {
className: string,
params: shape({
id: string.isRequired,
slug: string.isRequired,
type: oneOf(['new', 'edit']).isRequired,
tab: oneOf(STEPS).isRequired,
}).isRequired,
errors: shape({
createListingsError: object,
showListingsError: object,
@ -221,7 +226,6 @@ EditListingWizard.propTypes = {
onUpdateImageOrder: func.isRequired,
onUpdateListingDraft: func.isRequired,
rootClassName: string,
selectedTab: oneOf(STEPS).isRequired,
currentUser: propTypes.currentUser,
onManageDisableScrolling: func.isRequired,
};

View file

@ -262,10 +262,7 @@ export class CheckoutPageComponent extends Component {
<div className={css.detailsHeadings}>
<h2 className={css.detailsTitle}>{listingTitle}</h2>
<p className={css.detailsSubtitle}>
<FormattedMessage
id="CheckoutPage.hostedBy"
values={{ name: authorFirstName }}
/>
<FormattedMessage id="CheckoutPage.hostedBy" values={{ name: authorFirstName }} />
</p>
</div>
<h3 className={css.bookingBreakdownTitle}>

View file

@ -69,13 +69,13 @@ export const EditListingPageComponent = props => {
page,
params,
scrollingDisabled,
tab,
type,
} = props;
const { id, type } = params;
const isNew = type === 'new';
const hasIdParam = params && params.id;
const id = page.submittedListingId || (hasIdParam ? new types.UUID(params.id) : null);
const currentListing = getListing(id);
const listingId = page.submittedListingId || (id ? new types.UUID(id) : null);
const currentListing = getListing(listingId);
const shouldRedirect = page.submittedListingId && currentListing;
const showForm = isNew || currentListing;
@ -83,8 +83,8 @@ export const EditListingPageComponent = props => {
if (shouldRedirect) {
// If page has already listingId (after submit) and current listings exist
// redirect to listing page
const slug = currentListing ? createSlug(currentListing.attributes.title) : null;
return <NamedRedirect name="ListingPage" params={{ id: id.uuid, slug }} />;
const listingSlug = currentListing ? createSlug(currentListing.attributes.title) : null;
return <NamedRedirect name="ListingPage" params={{ id: listingId.uuid, slug: listingSlug }} />;
} else if (showForm) {
const { createListingsError = null, showListingsError = null, uploadImageError = null } = page;
const errors = { createListingsError, showListingsError, uploadImageError };
@ -120,6 +120,7 @@ export const EditListingPageComponent = props => {
/>
<EditListingWizard
className={css.wizard}
params={params}
disabled={disableForm}
errors={errors}
fetchInProgress={fetchInProgress}
@ -133,7 +134,6 @@ export const EditListingPageComponent = props => {
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
onImageUpload={onImageUpload}
onUpdateImageOrder={onUpdateImageOrder}
selectedTab={tab}
currentUser={currentUser}
onManageDisableScrolling={onManageDisableScrolling}
/>
@ -160,11 +160,9 @@ EditListingPageComponent.defaultProps = {
listingDraft: null,
logoutError: null,
notificationCount: 0,
params: null,
type: 'edit',
};
const { arrayOf, bool, func, instanceOf, number, object, shape, string } = PropTypes;
const { arrayOf, bool, func, instanceOf, number, object, shape, string, oneOf } = PropTypes;
EditListingPageComponent.propTypes = {
authInfoError: instanceOf(Error),
@ -187,12 +185,12 @@ EditListingPageComponent.propTypes = {
onUpdateListingDraft: func.isRequired,
page: object.isRequired,
params: shape({
id: string,
slug: string,
}),
id: string.isRequired,
slug: string.isRequired,
type: oneOf(['new', 'edit']).isRequired,
tab: string.isRequired,
}).isRequired,
scrollingDisabled: bool.isRequired,
tab: string.isRequired,
type: string,
/* from withRouter */
history: shape({

View file

@ -10,6 +10,7 @@ describe('EditListingPageComponent', () => {
const getListing = () => null;
const tree = renderShallow(
<EditListingPageComponent
params={{ id: 'id', slug: 'slug', type: 'new', tab: 'description' }}
currentUserHasListings={false}
isAuthenticated={false}
authInProgress={false}

View file

@ -48,7 +48,14 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
onPayoutDetailsSubmit={[Function]}
onUpdateImageOrder={[Function]}
onUpdateListingDraft={[Function]}
rootClassName={null}
selectedTab="description" />
params={
Object {
"id": "id",
"slug": "slug",
"tab": "description",
"type": "new",
}
}
rootClassName={null} />
</withRouter(PageLayout)>
`;

View file

@ -23,6 +23,10 @@ import {
EmailVerificationPage,
} from './containers';
// 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 = [
@ -90,42 +94,19 @@ const routesConfiguration = [
path: '/l/new',
exact: true,
name: 'NewListingPage',
component: props => <EditListingPage {...props} type={'new'} tab={'description'} />,
component: () => (
<NamedRedirect
name="EditListingPage"
params={{ slug: draftSlug, id: draftId, type: 'new', tab: 'description' }}
/>
),
},
{
auth: true,
path: '/l/new-description', // TODO should be l/:slug/:id/description
exact: true,
name: 'EditListingDescriptionPage',
component: props => <EditListingPage {...props} type={'new'} tab={'description'} />,
},
{
auth: true,
path: '/l/new-location', // TODO should be l/:slug/:id/location
exact: true,
name: 'EditListingLocationPage',
component: props => <EditListingPage {...props} type={'new'} tab={'location'} />,
},
{
auth: true,
path: '/l/new-pricing', // TODO should be l/:slug/:id/pricing
exact: true,
name: 'EditListingPricingPage',
component: props => <EditListingPage {...props} type={'new'} tab={'pricing'} />,
},
{
auth: true,
path: '/l/new-photos', // TODO should be l/:slug/:id/photos
exact: true,
name: 'EditListingPhotosPage',
component: props => <EditListingPage {...props} type={'new'} tab={'photos'} />,
},
{
auth: true,
path: '/l/:slug/:id/edit',
path: '/l/:slug/:id/:type/:tab',
exact: true,
name: 'EditListingPage',
component: props => <EditListingPage {...props} type={'edit'} />,
component: props => <EditListingPage {...props} />,
},
],
},