Merge pull request #947 from sharetribe/draft-listing

Draft listing
This commit is contained in:
Jenni Nurmi 2018-11-13 10:10:08 +02:00 committed by GitHub
commit ac287cfffb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 927 additions and 553 deletions

View file

@ -14,6 +14,11 @@ way to update this template, but currently, we follow a pattern:
## Upcoming version 2018-11-XX
## v2.3.0 2018-11-13
* [add] Draft listing is used in EditListingWizard, ManageListingCard and ListingPage.
From now on description panel creates a draft listing and photos panel publishes it. You can also view your current draft listings from 'your listings' page.
[#947](https://github.com/sharetribe/flex-template-web/pull/947)
* [fix] Firefox showed select options with the same color as select itself. Now options have their
own color set and *placeholder option needs to be disabled*.
[#946](https://github.com/sharetribe/flex-template-web/pull/946)

View file

@ -1,6 +1,6 @@
{
"name": "app",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"license": "Apache-2.0",
"dependencies": {
@ -49,7 +49,7 @@
"redux-thunk": "^2.2.0",
"sanitize.css": "^5.0.0",
"seedrandom": "^2.4.3",
"sharetribe-flex-sdk": "^1.0.0",
"sharetribe-flex-sdk": "^1.1.0",
"sharetribe-scripts": "1.1.5",
"smoothscroll-polyfill": "^0.4.0",
"source-map-support": "^0.5.4",

View file

@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bool, func, object, string } from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { EditListingDescriptionForm } from '../../forms';
import config from '../../config';
@ -26,7 +27,8 @@ const EditListingDescriptionPanel = props => {
const currentListing = ensureOwnListing(listing);
const { description, title, publicData } = currentListing.attributes;
const panelTitle = currentListing.id ? (
const isPublished = currentListing.id && currentListing.attributes.state !== LISTING_STATE_DRAFT;
const panelTitle = isPublished ? (
<FormattedMessage
id="EditListingDescriptionPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
@ -54,19 +56,18 @@ const EditListingDescriptionPanel = props => {
}}
onChange={onChange}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
fetchErrors={errors}
categories={config.custom.categories}
/>
</div>
);
};
const { func, object, string, bool } = PropTypes;
EditListingDescriptionPanel.defaultProps = {
className: null,
rootClassName: null,
errors: null,
listing: null,
};

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ensureListing } from '../../util/data';
import { EditListingFeaturesForm } from '../../forms';
import { ListingLink } from '../../components';
@ -28,7 +29,8 @@ const EditListingFeaturesPanel = props => {
const currentListing = ensureListing(listing);
const { publicData } = currentListing.attributes;
const panelTitle = currentListing.id ? (
const isPublished = currentListing.id && currentListing.attributes.state !== LISTING_STATE_DRAFT;
const panelTitle = isPublished ? (
<FormattedMessage
id="EditListingFeaturesPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
@ -58,8 +60,8 @@ const EditListingFeaturesPanel = props => {
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
fetchErrors={errors}
/>
</div>
);

View file

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';
import { EditListingLocationForm } from '../../forms';
@ -58,7 +59,9 @@ class EditListingLocationPanel extends Component {
const classes = classNames(rootClassName || css.root, className);
const currentListing = ensureOwnListing(listing);
const panelTitle = currentListing.id ? (
const isPublished =
currentListing.id && currentListing.attributes.state !== LISTING_STATE_DRAFT;
const panelTitle = isPublished ? (
<FormattedMessage
id="EditListingLocationPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
@ -95,8 +98,8 @@ class EditListingLocationPanel extends Component {
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
fetchErrors={errors}
/>
</div>
);

View file

@ -1,7 +1,8 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { array, bool, func, object, string } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { EditListingPhotosForm } from '../../forms';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';
@ -15,7 +16,7 @@ class EditListingPhotosPanel extends Component {
rootClassName,
errors,
fetchInProgress,
newListingCreated,
newListingPublished,
images,
listing,
onImageUpload,
@ -32,7 +33,9 @@ class EditListingPhotosPanel extends Component {
const classes = classNames(rootClass, className);
const currentListing = ensureOwnListing(listing);
const panelTitle = currentListing.id ? (
const isPublished =
currentListing.id && currentListing.attributes.state !== LISTING_STATE_DRAFT;
const panelTitle = isPublished ? (
<FormattedMessage
id="EditListingPhotosPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
@ -47,8 +50,8 @@ class EditListingPhotosPanel extends Component {
<EditListingPhotosForm
className={css.form}
disabled={fetchInProgress}
ready={newListingCreated}
errors={errors}
ready={newListingPublished}
fetchErrors={errors}
initialValues={{ images }}
images={images}
onImageUpload={onImageUpload}
@ -61,7 +64,6 @@ class EditListingPhotosPanel extends Component {
onRemoveImage={onRemoveImage}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
/>
</div>
@ -69,8 +71,6 @@ class EditListingPhotosPanel extends Component {
}
}
const { array, bool, func, object, shape, string } = PropTypes;
EditListingPhotosPanel.defaultProps = {
className: null,
rootClassName: null,
@ -82,15 +82,9 @@ EditListingPhotosPanel.defaultProps = {
EditListingPhotosPanel.propTypes = {
className: string,
rootClassName: string,
errors: shape({
createListingsError: object,
updateListingError: object,
showListingsError: object,
uploadImageError: object,
createStripeAccountError: object,
}),
errors: object,
fetchInProgress: bool.isRequired,
newListingCreated: bool.isRequired,
newListingPublished: bool.isRequired,
images: array,
// We cannot use propTypes.listing since the listing might be a draft.

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ensureOwnListing } from '../../util/data';
import { ListingLink } from '../../components';
import { EditListingPoliciesForm } from '../../forms';
@ -25,7 +26,8 @@ const EditListingPoliciesPanel = props => {
const currentListing = ensureOwnListing(listing);
const { publicData } = currentListing.attributes;
const panelTitle = currentListing.id ? (
const isPublished = currentListing.id && currentListing.attributes.state !== LISTING_STATE_DRAFT;
const panelTitle = isPublished ? (
<FormattedMessage
id="EditListingPoliciesPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
@ -53,8 +55,8 @@ const EditListingPoliciesPanel = props => {
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
fetchErrors={errors}
/>
</div>
);

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage } from 'react-intl';
import { LISTING_STATE_DRAFT } from '../../util/types';
import { ListingLink } from '../../components';
import { EditListingPricingForm } from '../../forms';
import { ensureOwnListing } from '../../util/data';
@ -29,7 +30,8 @@ const EditListingPricingPanel = props => {
const currentListing = ensureOwnListing(listing);
const { price } = currentListing.attributes;
const panelTitle = currentListing.id ? (
const isPublished = currentListing.id && currentListing.attributes.state !== LISTING_STATE_DRAFT;
const panelTitle = isPublished ? (
<FormattedMessage
id="EditListingPricingPanel.title"
values={{ listingTitle: <ListingLink listing={listing} /> }}
@ -47,8 +49,8 @@ const EditListingPricingPanel = props => {
onChange={onChange}
saveActionMsg={submitButtonText}
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
fetchErrors={errors}
/>
) : (
<div className={css.priceCurrencyInvalid}>

View file

@ -4,6 +4,11 @@ import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import classNames from 'classnames';
import { withViewport } from '../../util/contextHelpers';
import {
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_NEW,
LISTING_PAGE_PARAM_TYPES,
} from '../../util/urlHelpers';
import { ensureListing } from '../../util/data';
import { PayoutDetailsForm } from '../../forms';
import { Modal, NamedRedirect, Tabs } from '../../components';
@ -111,11 +116,11 @@ class EditListingWizard extends Component {
this.hasScrolledToTab = false;
this.state = {
submittedValues: null,
draftId: null,
showPayoutDetails: false,
};
this.handleCreateFlowTabScrolling = this.handleCreateFlowTabScrolling.bind(this);
this.handleCreateListing = this.handleCreateListing.bind(this);
this.handlePublishListing = this.handlePublishListing.bind(this);
this.handlePayoutModalClose = this.handlePayoutModalClose.bind(this);
this.handlePayoutSubmit = this.handlePayoutSubmit.bind(this);
}
@ -124,15 +129,15 @@ class EditListingWizard extends Component {
this.hasScrolledToTab = shouldScroll;
}
handleCreateListing(values) {
const { onCreateListing, currentUser } = this.props;
handlePublishListing(id) {
const { onPublishListingDraft, currentUser } = this.props;
const stripeConnected =
currentUser && currentUser.attributes && currentUser.attributes.stripeConnected;
if (stripeConnected) {
onCreateListing(values);
onPublishListingDraft(id);
} else {
this.setState({
submittedValues: values,
draftId: id,
showPayoutDetails: true,
});
}
@ -149,7 +154,7 @@ class EditListingWizard extends Component {
.then(() => {
this.setState({ showPayoutDetails: false });
this.props.onManageDisableScrolling('EditListingWizard.payoutModal', false);
this.props.onCreateListing(this.state.submittedValues);
this.props.onPublishListingDraft(this.state.draftId);
})
.catch(() => {
// do nothing
@ -173,15 +178,22 @@ class EditListingWizard extends Component {
} = this.props;
const selectedTab = params.tab;
const isNew = params.type === 'new';
const isNewListingFlow = [LISTING_PAGE_PARAM_TYPE_NEW, LISTING_PAGE_PARAM_TYPE_DRAFT].includes(
params.type
);
const rootClasses = rootClassName || css.root;
const classes = classNames(rootClasses, className);
const currentListing = ensureListing(listing);
const tabsStatus = tabsActive(isNew, currentListing);
const tabsStatus = tabsActive(isNewListingFlow, currentListing);
// If selectedTab is not active, redirect to the beginning of wizard
if (!tabsStatus[selectedTab]) {
return <NamedRedirect name="EditListingPage" params={{ ...params, tab: TABS[0] }} />;
const currentTabIndex = TABS.indexOf(selectedTab);
const nearestActiveTab = TABS.slice(0, currentTabIndex)
.reverse()
.find(t => tabsStatus[t]);
return <NamedRedirect name="EditListingPage" params={{ ...params, tab: nearestActiveTab }} />;
}
const { width } = viewport;
@ -220,7 +232,7 @@ class EditListingWizard extends Component {
tabLabel={tabLabel(intl, tab)}
tabLinkProps={tabLink(tab)}
selected={selectedTab === tab}
disabled={isNew && !tabsStatus[tab]}
disabled={isNewListingFlow && !tabsStatus[tab]}
tab={tab}
intl={intl}
params={params}
@ -228,7 +240,7 @@ class EditListingWizard extends Component {
marketplaceTabs={TABS}
errors={errors}
handleCreateFlowTabScrolling={this.handleCreateFlowTabScrolling}
handleCreateListing={this.handleCreateListing}
handlePublishListing={this.handlePublishListing}
fetchInProgress={fetchInProgress}
/>
);
@ -276,7 +288,7 @@ EditListingWizard.propTypes = {
params: shape({
id: string.isRequired,
slug: string.isRequired,
type: oneOf(['new', 'edit']).isRequired,
type: oneOf(LISTING_PAGE_PARAM_TYPES).isRequired,
tab: oneOf(TABS).isRequired,
}).isRequired,
@ -293,14 +305,14 @@ EditListingWizard.propTypes = {
}),
errors: shape({
createListingsError: object,
createListingDraftError: object,
updateListingError: object,
publishListingError: object,
showListingsError: object,
uploadImageError: object,
createStripeAccountError: object,
}),
}).isRequired,
fetchInProgress: bool.isRequired,
onCreateListing: func.isRequired,
onPayoutDetailsFormChange: func.isRequired,
onPayoutDetailsSubmit: func.isRequired,
onManageDisableScrolling: func.isRequired,

View file

@ -2,6 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'react-intl';
import routeConfiguration from '../../routeConfiguration';
import {
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_NEW,
LISTING_PAGE_PARAM_TYPES,
} from '../../util/urlHelpers';
import { ensureListing } from '../../util/data';
import { createResourceLocatorString } from '../../util/routes';
import {
@ -34,6 +39,28 @@ const pathParamsToNextTab = (params, tab, marketplaceTabs) => {
return { ...params, tab: nextTab };
};
// When user has update draft listing, he should be redirected to next EditListingWizardTab
const redirectAfterDraftUpdate = (listingId, params, tab, marketplaceTabs, history) => {
const currentPathParams = {
...params,
type: LISTING_PAGE_PARAM_TYPE_DRAFT,
id: listingId,
};
const routes = routeConfiguration();
// Replace current "new" path to "draft" path.
// Browser's back button should lead to editing current draft instead of creating a new one.
if (params.type === LISTING_PAGE_PARAM_TYPE_NEW) {
const draftURI = createResourceLocatorString('EditListingPage', routes, currentPathParams, {});
history.replace(draftURI);
}
// Redirect to next tab
const nextPathParams = pathParamsToNextTab(currentPathParams, tab, marketplaceTabs);
const to = createResourceLocatorString('EditListingPage', routes, nextPathParams, {});
history.push(to);
};
const EditListingWizardTab = props => {
const {
tab,
@ -41,55 +68,66 @@ const EditListingWizardTab = props => {
params,
errors,
fetchInProgress,
newListingCreated,
newListingPublished,
history,
images,
listing,
handleCreateFlowTabScrolling,
handleCreateListing,
handlePublishListing,
onUpdateListing,
onCreateListingDraft,
onImageUpload,
onUpdateImageOrder,
onRemoveImage,
onUpdateListingDraft,
onChange,
updatedTab,
updateInProgress,
intl,
} = props;
const isNew = params.type === 'new';
const { type } = params;
const isNewURI = type === LISTING_PAGE_PARAM_TYPE_NEW;
const isDraftURI = type === LISTING_PAGE_PARAM_TYPE_DRAFT;
const isNewListingFlow = isNewURI || isDraftURI;
const currentListing = ensureListing(listing);
const imageIds = images => {
return images ? images.map(img => img.imageId || img.id) : null;
};
const onCompleteEditListingWizardTab = (tab, updateValues) => {
if (isNew) {
const onUpsertListingDraft =
tab !== marketplaceTabs[0] ? onUpdateListingDraft : onCreateListingDraft;
onUpsertListingDraft(updateValues);
// Normalize images for API call
const { images: updatedImages, ...otherValues } = updateValues;
const imageProperty =
typeof updatedImages !== 'undefined' ? { images: imageIds(updatedImages) } : {};
const updateValuesWithImages = { ...otherValues, ...imageProperty };
if (tab !== marketplaceTabs[marketplaceTabs.length - 1]) {
// Create listing flow: smooth scrolling polyfill to scroll to correct tab
handleCreateFlowTabScrolling(false);
// Redirect to next tab
const pathParams = pathParamsToNextTab(params, tab, marketplaceTabs);
history.push(
createResourceLocatorString('EditListingPage', routeConfiguration(), pathParams, {})
);
} else {
// Normalize images for API call
const imageIdArray = imageIds(updateValues.images);
handleCreateListing({ ...listing.attributes, images: imageIdArray });
}
if (isNewListingFlow) {
const onUpsertListingDraft = isNewURI
? (tab, updateValues) => onCreateListingDraft(updateValues)
: onUpdateListing;
const upsertValues = isNewURI
? updateValuesWithImages
: { ...updateValuesWithImages, id: currentListing.id };
onUpsertListingDraft(tab, upsertValues)
.then(r => {
if (tab !== marketplaceTabs[marketplaceTabs.length - 1]) {
// Create listing flow: smooth scrolling polyfill to scroll to correct tab
handleCreateFlowTabScrolling(false);
// After successful saving of draft data, user should be redirected to next tab
redirectAfterDraftUpdate(r.data.data.id.uuid, params, tab, marketplaceTabs, history);
} else {
handlePublishListing(currentListing.id);
}
})
.catch(e => {
// No need for extra actions
});
} else {
const { images: updatedImages, ...rest } = updateValues;
// Normalize images for API call
const imageProperty =
typeof updatedImages !== 'undefined' ? { images: imageIds(updatedImages) } : {};
onUpdateListing(tab, { ...rest, id: currentListing.id, ...imageProperty });
onUpdateListing(tab, { ...updateValuesWithImages, id: currentListing.id });
}
};
@ -106,7 +144,7 @@ const EditListingWizardTab = props => {
switch (tab) {
case DESCRIPTION: {
const submitButtonTranslationKey = isNew
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewDescription'
: 'EditListingWizard.saveEditDescription';
return (
@ -120,7 +158,7 @@ const EditListingWizardTab = props => {
);
}
case FEATURES: {
const submitButtonTranslationKey = isNew
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewFeatures'
: 'EditListingWizard.saveEditFeatures';
return (
@ -134,7 +172,7 @@ const EditListingWizardTab = props => {
);
}
case POLICY: {
const submitButtonTranslationKey = isNew
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewPolicies'
: 'EditListingWizard.saveEditPolicies';
return (
@ -148,7 +186,7 @@ const EditListingWizardTab = props => {
);
}
case LOCATION: {
const submitButtonTranslationKey = isNew
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewLocation'
: 'EditListingWizard.saveEditLocation';
return (
@ -162,7 +200,7 @@ const EditListingWizardTab = props => {
);
}
case PRICING: {
const submitButtonTranslationKey = isNew
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewPricing'
: 'EditListingWizard.saveEditPricing';
return (
@ -176,16 +214,16 @@ const EditListingWizardTab = props => {
);
}
case PHOTOS: {
const submitButtonTranslationKey = isNew
const submitButtonTranslationKey = isNewListingFlow
? 'EditListingWizard.saveNewPhotos'
: 'EditListingWizard.saveEditPhotos';
// newListingCreated and fetchInProgress are flags for the last wizard tab
// newListingPublished and fetchInProgress are flags for the last wizard tab
return (
<EditListingPhotosPanel
{...panelProps(PHOTOS)}
submitButtonText={intl.formatMessage({ id: submitButtonTranslationKey })}
newListingCreated={newListingCreated}
newListingPublished={newListingPublished}
fetchInProgress={fetchInProgress}
images={images}
onImageUpload={onImageUpload}
@ -203,7 +241,6 @@ const EditListingWizardTab = props => {
};
EditListingWizardTab.defaultProps = {
errors: null,
listing: null,
updatedTab: null,
};
@ -214,19 +251,21 @@ EditListingWizardTab.propTypes = {
params: shape({
id: string.isRequired,
slug: string.isRequired,
type: oneOf(['new', 'edit']).isRequired,
type: oneOf(LISTING_PAGE_PARAM_TYPES).isRequired,
tab: oneOf(SUPPORTED_TABS).isRequired,
}).isRequired,
errors: shape({
createListingsError: object,
createListingDraftError: object,
publishListingError: object,
updateListingError: object,
showListingsError: object,
uploadImageError: object,
}).isRequired,
fetchInProgress: bool.isRequired,
newListingCreated: bool.isRequired,
newListingPublished: bool.isRequired,
history: shape({
push: func.isRequired,
replace: func.isRequired,
}).isRequired,
images: array.isRequired,
@ -243,13 +282,12 @@ EditListingWizardTab.propTypes = {
}),
handleCreateFlowTabScrolling: func.isRequired,
handleCreateListing: func.isRequired,
handlePublishListing: func.isRequired,
onUpdateListing: func.isRequired,
onCreateListingDraft: func.isRequired,
onImageUpload: func.isRequired,
onUpdateImageOrder: func.isRequired,
onRemoveImage: func.isRequired,
onUpdateListingDraft: func.isRequired,
onChange: func.isRequired,
updatedTab: string,
updateInProgress: bool.isRequired,

View file

@ -9,8 +9,12 @@
import React from 'react';
import { string, oneOfType, node } from 'prop-types';
import { richText } from '../../util/richText';
import { LISTING_STATE_PENDING_APPROVAL, propTypes } from '../../util/types';
import { LISTING_PAGE_PENDING_APPROVAL_VARIANT, createSlug } from '../../util/urlHelpers';
import { LISTING_STATE_DRAFT, LISTING_STATE_PENDING_APPROVAL, propTypes } from '../../util/types';
import {
LISTING_PAGE_DRAFT_VARIANT,
LISTING_PAGE_PENDING_APPROVAL_VARIANT,
createSlug,
} from '../../util/urlHelpers';
import { NamedLink } from '../../components';
import css from './ListingLink.css';
@ -36,13 +40,19 @@ const ListingLink = props => {
);
const isPendingApproval = state === LISTING_STATE_PENDING_APPROVAL;
const linkProps = isPendingApproval
const isDraft = state === LISTING_STATE_DRAFT;
const variant = isPendingApproval
? LISTING_PAGE_PENDING_APPROVAL_VARIANT
: isDraft
? LISTING_PAGE_DRAFT_VARIANT
: null;
const linkProps = !!variant
? {
name: 'ListingPageVariant',
params: {
id,
slug,
variant: LISTING_PAGE_PENDING_APPROVAL_VARIANT,
variant,
},
}
: {

View file

@ -24,7 +24,6 @@
display: flex;
flex-direction: column;
position: relative;
cursor: pointer;
/* Remove link's hover effect */
&:hover {
@ -77,6 +76,7 @@
width: 100%;
position: relative;
transition: var(--transitionStyleButton);
cursor: pointer;
&:hover {
transform: scale(1.02);
@ -203,22 +203,33 @@
.info {
/* Layout */
display: flex;
flex-direction: row;
padding: 16px 0 2px 0;
flex-direction: column;
padding: 8px 0 2px 0;
}
.price {
/* Layout */
display: flex;
flex-direction: column;
justify-content: flex-start;
flex-direction: row;
align-items: baseline;
flex-shrink: 0;
margin-right: 18px;
padding-top: 4px;
padding-bottom: 2px;
@media (--viewportMedium) {
padding-top: 5px;
padding-bottom: 3px;
}
}
.priceValue {
/* Font */
@apply --marketplaceH3FontStyles;
@apply --marketplaceH4FontStyles;
color: var(--marketplaceColor);
font-weight: var(--fontWeightSemiBold);
margin-right: 4px;
/* Remove default margins from font */
margin-top: 0;
@ -238,29 +249,46 @@
/* Remove default margins from font */
margin-top: 0;
margin-bottom: 0;
}
@media (--viewportMedium) {
@media (--viewportMedium) {
.perUnit {
margin-top: 0;
margin-bottom: 0;
}
}
.noPrice {
composes: perUnit;
padding: 5px 0 3px 0;
}
.mainInfo {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.titleWrapper {
}
.title {
/* Font */
@apply --marketplaceH3FontStyles;
color: var(--matterColor);
text-align: left;
padding-top: 3px;
padding-bottom: 3px;
/* Remove default margins from font */
margin-top: 0;
margin-bottom: 0;
@media (--viewportMedium) {
padding-top: 4px;
padding-bottom: 4px;
margin-top: 0;
margin-bottom: 0;
}
@ -270,6 +298,20 @@
color: var(--attentionColor);
}
.titleDraft {
margin-right: 8px;
}
/* Solid gray background for draft listings without image */
.draftNoImage {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: var(--matterColor);
}
.edit {
@apply --marketplaceButtonStylesSecondary;
@ -291,79 +333,8 @@
background-position: 15px center;
}
.loadingOverlayWrapper,
.errorOverlayWrapper,
.closedOverlayWrapper,
.pendingApprovalOverlayWrapper {
/* Positioning */
@apply --ManageListingCard_coverEverything;
}
.loadingOverlay,
.errorOverlay,
.closedOverlay,
.pendingApprovalOverlay {
/* Positioning */
@apply --ManageListingCard_coverEverything;
/* Overlay background style */
background-color: var(--matterColorLight);
opacity: 0.9;
}
.pendingApprovalOverlay {
cursor: default;
}
.loadingOverlayContent,
.errorOverlayContent,
.closedOverlayContent,
.pendingApprovalOverlayContent {
@apply --marketplaceH4FontStyles;
color: var(--matterColor);
/* Positioning */
@apply --ManageListingCard_coverEverything;
/* Layout */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: default;
/* Remove default margins from font */
margin-top: 0;
margin-bottom: 0;
@media (--viewportMedium) {
margin-top: 0;
margin-bottom: 0;
}
}
.errorOverlayContent {
color: var(--failColor);
justify-content: flex-start;
margin-top: 90px;
}
.pendingApprovalOverlayContent {
justify-content: flex-start;
margin-top: 90px;
}
.closedMessage {
max-width: 220px;
text-align: center;
}
.pendingMessage {
max-width: 300px;
text-align: center;
}
.openListingButton {
.openListingButton,
.finishListingDraftLink {
@apply --marketplaceButtonStylesPrimary;
@apply --marketplaceH5FontStyles;
@ -371,8 +342,7 @@
min-height: 0;
width: 114px;
height: 41px;
padding: 6px 0 8px;
padding: 8px;
border-radius: 4px;
}
@ -383,3 +353,38 @@
.cardIsOpen {
display: block;
}
.manageLinks {
@apply --marketplaceH5FontStyles;
line-height: 18px;
padding-top: 2px;
padding-bottom: 4px;
/* Remove default margins from font */
margin-top: 0;
margin-bottom: 0;
@media (--viewportMedium) {
line-height: 24px;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
}
.manageLink {
text-decoration: none;
color: var(--matterColor);
white-space: pre;
&:hover {
text-decoration: underline;
color: var(--matterColorDark);
}
}
.manageLinksSeparator {
margin: 0 3px 0 3px;
}

View file

@ -1,7 +1,11 @@
/* eslint-disable no-console */
import React from 'react';
import ManageListingCard from './ManageListingCard';
import { LISTING_STATE_CLOSED, LISTING_STATE_PENDING_APPROVAL } from '../../util/types';
import {
LISTING_STATE_CLOSED,
LISTING_STATE_PENDING_APPROVAL,
LISTING_STATE_DRAFT,
} from '../../util/types';
import { createOwnListing, fakeIntl } from '../../util/test-data';
const noop = () => null;
@ -60,3 +64,20 @@ export const PendingApproval = {
history: { push: noop },
},
};
export const Draft = {
component: ManageListingCardWrapper,
props: {
hasClosingError: false,
hasOpeningError: false,
intl: fakeIntl,
listing: createOwnListing('listing-draft', {
state: LISTING_STATE_DRAFT,
}),
isMenuOpen: false,
onCloseListing: noop,
onOpenListing: noop,
onToggleMenu: noop,
history: { push: noop },
},
};

View file

@ -5,23 +5,36 @@ import { withRouter } from 'react-router-dom';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import routeConfiguration from '../../routeConfiguration';
import { LISTING_STATE_PENDING_APPROVAL, LISTING_STATE_CLOSED, propTypes } from '../../util/types';
import {
LISTING_STATE_PENDING_APPROVAL,
LISTING_STATE_CLOSED,
LISTING_STATE_DRAFT,
propTypes,
} from '../../util/types';
import { formatMoney } from '../../util/currency';
import { ensureOwnListing } from '../../util/data';
import { LISTING_PAGE_PENDING_APPROVAL_VARIANT, createSlug } from '../../util/urlHelpers';
import {
LISTING_PAGE_PENDING_APPROVAL_VARIANT,
LISTING_PAGE_DRAFT_VARIANT,
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_EDIT,
createSlug,
} from '../../util/urlHelpers';
import { createResourceLocatorString } from '../../util/routes';
import config from '../../config';
import {
InlineTextButton,
Menu,
MenuLabel,
MenuContent,
MenuItem,
NamedLink,
IconSpinner,
ResponsiveImage,
} from '../../components';
import config from '../../config';
import MenuIcon from './MenuIcon';
import Overlay from './Overlay';
import css from './ManageListingCard.css';
// Menu content needs the same padding
@ -47,31 +60,31 @@ const priceData = (price, intl) => {
return {};
};
const createEditListingURL = (routes, listing) => {
const id = listing.id.uuid;
const slug = createSlug(listing.attributes.title);
const pathParams = { id, slug, type: 'edit', tab: 'description' };
return createResourceLocatorString('EditListingPage', routes, pathParams, {});
};
const createListingURL = (routes, listing) => {
const id = listing.id.uuid;
const slug = createSlug(listing.attributes.title);
const isPendingApproval = listing.attributes.state === LISTING_STATE_PENDING_APPROVAL;
const linkProps = isPendingApproval
? {
name: 'ListingPageVariant',
params: {
id,
slug,
variant: LISTING_PAGE_PENDING_APPROVAL_VARIANT,
},
}
: {
name: 'ListingPage',
params: { id, slug },
};
const isDraft = listing.attributes.state === LISTING_STATE_DRAFT;
const variant = isDraft
? LISTING_PAGE_DRAFT_VARIANT
: isPendingApproval
? LISTING_PAGE_PENDING_APPROVAL_VARIANT
: null;
const linkProps =
isPendingApproval || isDraft
? {
name: 'ListingPageVariant',
params: {
id,
slug,
variant,
},
}
: {
name: 'ListingPage',
params: { id, slug },
};
return createResourceLocatorString(linkProps.name, routes, linkProps.params, {});
};
@ -113,8 +126,10 @@ export const ManageListingCardComponent = props => {
const currentListing = ensureOwnListing(listing);
const id = currentListing.id.uuid;
const { title = '', price, state } = currentListing.attributes;
const slug = createSlug(title);
const isPendingApproval = state === LISTING_STATE_PENDING_APPROVAL;
const isClosed = state === LISTING_STATE_CLOSED;
const isDraft = state === LISTING_STATE_DRAFT;
const firstImage =
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
@ -124,115 +139,36 @@ export const ManageListingCardComponent = props => {
const { formattedPrice, priceTitle } = priceData(price, intl);
/* eslint-disable jsx-a11y/no-static-element-interactions */
const closedOverlay = !isClosed ? null : (
<div
className={css.closedOverlayWrapper}
onClick={event => {
event.preventDefault();
event.stopPropagation();
}}
>
<div className={css.closedOverlay} />
<div className={css.closedOverlayContent}>
<div className={css.closedMessage}>
<FormattedMessage id="ManageListingCard.closedListing" />
</div>
<button
className={css.openListingButton}
disabled={!!actionsInProgressListingId}
onClick={event => {
event.preventDefault();
event.stopPropagation();
if (!actionsInProgressListingId) {
onOpenListing(currentListing.id);
}
}}
>
<FormattedMessage id="ManageListingCard.openListing" />
</button>
</div>
</div>
);
const errorOverlay =
hasOpeningError || hasClosingError ? (
<div
className={css.errorOverlayWrapper}
onClick={event => {
event.preventDefault();
event.stopPropagation();
}}
>
<div className={css.errorOverlay} />
<div className={css.errorOverlayContent}>
<div className={css.closedMessage}>
<FormattedMessage id="ManageListingCard.actionFailed" />
</div>
</div>
</div>
) : null;
const pendingApprovalOverlay = isPendingApproval ? (
<div
className={css.pendingApprovalOverlayWrapper}
onClick={event => {
event.preventDefault();
event.stopPropagation();
}}
>
<div className={css.pendingApprovalOverlay} />
<div className={css.pendingApprovalOverlayContent}>
<div className={css.pendingMessage}>
<FormattedMessage
id="ManageListingCard.pendingApproval"
values={{ listingTitle: title }}
/>
</div>
</div>
</div>
) : null;
const thisInProgress = actionsInProgressListingId && actionsInProgressListingId.uuid === id;
const loadingOrErrorOverlay = thisInProgress ? (
<div
className={css.loadingOverlayWrapper}
onClick={event => {
event.preventDefault();
event.stopPropagation();
}}
>
<div className={css.loadingOverlay} />
<div className={css.loadingOverlayContent}>
<IconSpinner />
</div>
</div>
) : (
errorOverlay
);
/* eslint-enable jsx-a11y/no-static-element-interactions */
const hasError = hasOpeningError || hasClosingError;
const thisListingInProgress =
actionsInProgressListingId && actionsInProgressListingId.uuid === id;
const titleClasses = classNames(css.title, {
[css.titlePending]: isPendingApproval,
[css.titleDraft]: isDraft,
});
return (
<div
className={classes}
tabIndex={0}
onClick={event => {
event.preventDefault();
event.stopPropagation();
const editListingLinkType = isDraft
? LISTING_PAGE_PARAM_TYPE_DRAFT
: LISTING_PAGE_PARAM_TYPE_EDIT;
// ManageListingCard contains links, buttons and elements that are working with routing.
// This card doesn't work if <a> or <button> is used to wrap events that are card 'clicks'.
//
// NOTE: It might be better to absolute-position those buttons over a card-links.
// (So, that they have no parent-child relationship - like '<a>bla<a>blaa</a></a>')
history.push(createListingURL(routeConfiguration(), listing));
}}
>
<div className={css.threeToTwoWrapper}>
return (
<div className={classes}>
<div
className={css.threeToTwoWrapper}
tabIndex={0}
onClick={event => {
event.preventDefault();
event.stopPropagation();
// ManageListingCard contains links, buttons and elements that are working with routing.
// This card doesn't work if <a> or <button> is used to wrap events that are card 'clicks'.
//
// NOTE: It might be better to absolute-position those buttons over a card-links.
// (So, that they have no parent-child relationship - like '<a>bla<a>blaa</a></a>')
history.push(createListingURL(routeConfiguration(), listing));
}}
>
<div className={css.aspectWrapper}>
<ResponsiveImage
rootClassName={css.rootForImage}
@ -287,32 +223,106 @@ export const ManageListingCardComponent = props => {
</Menu>
</div>
</div>
{closedOverlay}
{pendingApprovalOverlay}
{loadingOrErrorOverlay}
{isDraft ? (
<React.Fragment>
<div className={classNames({ [css.draftNoImage]: !firstImage })} />
<Overlay
message={intl.formatMessage(
{ id: 'ManageListingCard.draftOverlayText' },
{ listingTitle: title }
)}
>
<NamedLink
className={css.finishListingDraftLink}
name="EditListingPage"
params={{ id, slug, type: LISTING_PAGE_PARAM_TYPE_DRAFT, tab: 'photos' }}
>
<FormattedMessage id="ManageListingCard.finishListingDraft" />
</NamedLink>
</Overlay>
</React.Fragment>
) : null}
{isClosed ? (
<Overlay
message={intl.formatMessage(
{ id: 'ManageListingCard.closedListing' },
{ listingTitle: title }
)}
>
<button
className={css.openListingButton}
disabled={!!actionsInProgressListingId}
onClick={event => {
event.preventDefault();
event.stopPropagation();
if (!actionsInProgressListingId) {
onOpenListing(currentListing.id);
}
}}
>
<FormattedMessage id="ManageListingCard.openListing" />
</button>
</Overlay>
) : null}
{isPendingApproval ? (
<Overlay
message={intl.formatMessage(
{ id: 'ManageListingCard.pendingApproval' },
{ listingTitle: title }
)}
/>
) : null}
{thisListingInProgress ? (
<Overlay>
<IconSpinner />
</Overlay>
) : hasError ? (
<Overlay errorMessage={intl.formatMessage({ id: 'ManageListingCard.actionFailed' })} />
) : null}
</div>
<div className={css.info}>
<div className={css.price}>
<div className={css.priceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.perUnit}>
<FormattedMessage id="ManageListingCard.perUnit" />
</div>
{formattedPrice ? (
<React.Fragment>
<div className={css.priceValue} title={priceTitle}>
{formattedPrice}
</div>
<div className={css.perUnit}>
<FormattedMessage id="ManageListingCard.perUnit" />
</div>
</React.Fragment>
) : (
<div className={css.noPrice}>
<FormattedMessage id="ManageListingCard.priceNotSet" />
</div>
)}
</div>
<div className={css.mainInfo}>
<div className={titleClasses}>{formatTitle(title, MAX_LENGTH_FOR_WORDS_IN_TITLE)}</div>
<div className={css.titleWrapper}>
<InlineTextButton
className={titleClasses}
onClick={event => {
event.preventDefault();
event.stopPropagation();
history.push(createListingURL(routeConfiguration(), listing));
}}
>
{formatTitle(title, MAX_LENGTH_FOR_WORDS_IN_TITLE)}
</InlineTextButton>
</div>
</div>
<div className={css.manageLinks}>
<NamedLink
className={css.manageLink}
name="EditListingPage"
params={{ id, slug, type: editListingLinkType, tab: 'description' }}
>
<FormattedMessage id="ManageListingCard.editListing" />
</NamedLink>
</div>
<button
className={css.edit}
onClick={event => {
event.preventDefault();
event.stopPropagation();
history.push(createEditListingURL(routeConfiguration(), listing));
}}
>
<FormattedMessage id="ManageListingCard.edit" />
</button>
</div>
</div>
);

View file

@ -0,0 +1,70 @@
@import '../../marketplace.css';
:root {
/*
These variables are available in global scope through ":root"
element (<html> tag). Variables with the same names are going to
overwrite each other if CSS Properties' (PostCSS plugin)
configuration "preserve: true" is used - meaning that variables
are left to CSS bundle. We are planning to enable it in the future
since browsers support CSS Properties already.
*/
--Overlay_coverEverything: {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
.root {
/* Positioning */
@apply --Overlay_coverEverything;
}
.overlay {
/* Positioning */
@apply --Overlay_coverEverything;
/* Overlay background style */
background-color: var(--matterColorLight);
opacity: 0.9;
}
.overlayContent {
@apply --marketplaceH4FontStyles;
color: var(--matterColor);
/* Positioning */
@apply --Overlay_coverEverything;
/* Layout */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: default;
/* Remove default margins from font */
margin-top: 0;
margin-bottom: 0;
}
@media (--viewportMedium) {
.overlayContent {
margin-top: 0;
margin-bottom: 0;
}
}
.message {
max-width: 220px;
text-align: center;
}
.errorMessage {
composes: message;
color: var(--failColor);
}

View file

@ -0,0 +1,46 @@
import React from 'react';
import { node, string } from 'prop-types';
import classNames from 'classnames';
import css from './Overlay.css';
const Overlay = props => {
const { className, rootClassName, message, errorMessage, children } = props;
const classes = classNames(rootClassName || css.root, className);
return (
<div
className={classes}
onClick={event => {
event.preventDefault();
event.stopPropagation();
}}
>
<div className={css.overlay} />
<div className={css.overlayContent}>
{errorMessage ? <div className={css.errorMessage}>{errorMessage}</div> : null}
{message ? <div className={css.message}>{message}</div> : null}
{children}
</div>
</div>
);
};
Overlay.defaultProps = {
className: null,
rootClassName: null,
message: null,
errorMessage: null,
children: null,
};
Overlay.propTypes = {
className: string,
rootClassName: string,
message: string,
errorMessage: string,
children: node,
};
export default Overlay;

View file

@ -3,10 +3,11 @@
exports[`ManageListingCard matches snapshot 1`] = `
<div
className=""
onClick={[Function]}
tabIndex={0}
>
<div>
<div
onClick={[Function]}
tabIndex={0}
>
<div>
<ResponsiveImage
alt="listing1 title"
@ -93,44 +94,59 @@ exports[`ManageListingCard matches snapshot 1`] = `
</div>
<div>
<div>
<div
title={55}
>
55
</div>
<React.Fragment>
<div
title={55}
>
55
</div>
<div>
<FormattedMessage
id="ManageListingCard.perUnit"
values={Object {}}
/>
</div>
</React.Fragment>
</div>
<div>
<div>
<FormattedMessage
id="ManageListingCard.perUnit"
values={Object {}}
/>
<InlineTextButton
className=""
onClick={[Function]}
>
<span
key="1"
style={
Object {
"wordBreak": "break-all",
}
}
>
listing1
</span>
title
</InlineTextButton>
</div>
</div>
<div>
<div
className=""
>
<span
key="1"
style={
Object {
"wordBreak": "break-all",
}
<NamedLink
name="EditListingPage"
params={
Object {
"id": "listing1",
"slug": "listing1-title",
"tab": "description",
"type": "edit",
}
>
listing1
</span>
title
</div>
}
>
<FormattedMessage
id="ManageListingCard.editListing"
values={Object {}}
/>
</NamedLink>
</div>
<button
onClick={[Function]}
>
<FormattedMessage
id="ManageListingCard.edit"
values={Object {}}
/>
</button>
</div>
</div>
`;

View file

@ -1,13 +1,9 @@
import omit from 'lodash/omit';
import omitBy from 'lodash/omitBy';
import isUndefined from 'lodash/isUndefined';
import mergeWith from 'lodash/mergeWith';
import { types as sdkTypes } from '../../util/sdkLoader';
import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import * as log from '../../util/log';
import { fetchCurrentUserHasListingsSuccess } from '../../ducks/user.duck';
import { overrideArrays } from '../../util/data';
const { UUID } = sdkTypes;
@ -22,9 +18,13 @@ const errorAction = actionType => error => ({ type: actionType, payload: error,
export const MARK_TAB_UPDATED = 'app/EditListingPage/MARK_TAB_UPDATED';
export const CLEAR_UPDATED_TAB = 'app/EditListingPage/CLEAR_UPDATED_TAB';
export const CREATE_LISTING_REQUEST = 'app/EditListingPage/CREATE_LISTING_REQUEST';
export const CREATE_LISTING_SUCCESS = 'app/EditListingPage/CREATE_LISTING_SUCCESS';
export const CREATE_LISTING_ERROR = 'app/EditListingPage/CREATE_LISTING_ERROR';
export const CREATE_LISTING_DRAFT_REQUEST = 'app/EditListingPage/CREATE_LISTING_DRAFT_REQUEST';
export const CREATE_LISTING_DRAFT_SUCCESS = 'app/EditListingPage/CREATE_LISTING_DRAFT_SUCCESS';
export const CREATE_LISTING_DRAFT_ERROR = 'app/EditListingPage/CREATE_LISTING_DRAFT_ERROR';
export const PUBLISH_LISTING_REQUEST = 'app/EditListingPage/PUBLISH_LISTING_REQUEST';
export const PUBLISH_LISTING_SUCCESS = 'app/EditListingPage/PUBLISH_LISTING_SUCCESS';
export const PUBLISH_LISTING_ERROR = 'app/EditListingPage/PUBLISH_LISTING_ERROR';
export const UPDATE_LISTING_REQUEST = 'app/EditListingPage/UPDATE_LISTING_REQUEST';
export const UPDATE_LISTING_SUCCESS = 'app/EditListingPage/UPDATE_LISTING_SUCCESS';
@ -40,20 +40,19 @@ export const UPLOAD_IMAGE_ERROR = 'app/EditListingPage/UPLOAD_IMAGE_ERROR';
export const UPDATE_IMAGE_ORDER = 'app/EditListingPage/UPDATE_IMAGE_ORDER';
export const CREATE_LISTING_DRAFT = 'app/EditListingPage/CREATE_LISTING_DRAFT';
export const UPDATE_LISTING_DRAFT = 'app/EditListingPage/UPDATE_LISTING_DRAFT';
export const REMOVE_LISTING_IMAGE = 'app/EditListingPage/REMOVE_LISTING_IMAGE';
// ================ Reducer ================ //
const initialState = {
// Error instance placeholders for each endpoint
createListingsError: null,
createListingDraftError: null,
publishingListing: null,
publishListingError: null,
updateListingError: null,
showListingsError: null,
uploadImageError: null,
createListingInProgress: false,
createListingDraftInProgress: false,
submittedListingId: null,
redirectToListing: false,
images: {},
@ -72,29 +71,53 @@ export default function reducer(state = initialState, action = {}) {
case CLEAR_UPDATED_TAB:
return { ...state, updatedTab: null, updateListingError: null };
case CREATE_LISTING_REQUEST:
case CREATE_LISTING_DRAFT_REQUEST:
return {
...state,
createListingInProgress: true,
createListingsError: null,
createListingDraftInProgress: true,
createListingDraftError: null,
submittedListingId: null,
redirectToListing: false,
listingDraft: null,
};
case CREATE_LISTING_SUCCESS:
case CREATE_LISTING_DRAFT_SUCCESS:
return {
...state,
createListingInProgress: false,
createListingDraftInProgress: false,
submittedListingId: payload.data.id,
redirectToListing: true,
listingDraft: payload.data,
};
case CREATE_LISTING_ERROR:
case CREATE_LISTING_DRAFT_ERROR:
return {
...state,
createListingInProgress: false,
createListingsError: payload,
redirectToListing: false,
createListingDraftInProgress: false,
createListingDraftError: payload,
};
case PUBLISH_LISTING_REQUEST:
return {
...state,
publishingListing: payload.listingId,
publishListingError: null,
};
case PUBLISH_LISTING_SUCCESS:
return {
redirectToListing: true,
publishingListing: null,
};
case PUBLISH_LISTING_ERROR: {
// eslint-disable-next-line no-console
console.error(payload);
return {
...state,
publishingListing: null,
publishListingError: {
listingId: state.publishingListing,
error: payload,
},
};
}
case UPDATE_LISTING_REQUEST:
return { ...state, updateInProgress: true, updateListingError: null };
case UPDATE_LISTING_SUCCESS:
@ -141,23 +164,6 @@ export default function reducer(state = initialState, action = {}) {
case UPDATE_IMAGE_ORDER:
return { ...state, imageOrder: payload.imageOrder };
case CREATE_LISTING_DRAFT:
case UPDATE_LISTING_DRAFT: {
const { attributes, images } = state.listingDraft || {};
const updatedImages = payload.images || images;
return {
...state,
listingDraft: {
attributes: mergeWith(
attributes,
omitBy(payload.attributes, isUndefined),
overrideArrays
),
images: updatedImages,
},
};
}
case REMOVE_LISTING_IMAGE: {
const id = payload.imageId;
@ -198,28 +204,6 @@ export const updateImageOrder = imageOrder => ({
payload: { imageOrder },
});
export const createListingDraft = listingData => {
const { images, ...attributes } = listingData;
return {
type: CREATE_LISTING_DRAFT,
payload: {
attributes,
images,
},
};
};
export const updateListingDraft = listingData => {
const { images, ...attributes } = listingData;
return {
type: UPDATE_LISTING_DRAFT,
payload: {
attributes,
images,
},
};
};
export const removeListingImage = imageId => ({
type: REMOVE_LISTING_IMAGE,
payload: { imageId },
@ -230,9 +214,14 @@ export const removeListingImage = imageId => ({
// expects.
// SDK method: ownListings.create
export const createListing = requestAction(CREATE_LISTING_REQUEST);
export const createListingSuccess = successAction(CREATE_LISTING_SUCCESS);
export const createListingError = errorAction(CREATE_LISTING_ERROR);
export const createListingDraft = requestAction(CREATE_LISTING_DRAFT_REQUEST);
export const createListingDraftSuccess = successAction(CREATE_LISTING_DRAFT_SUCCESS);
export const createListingDraftError = errorAction(CREATE_LISTING_DRAFT_ERROR);
// SDK method: ownListings.publish
export const publishListing = requestAction(PUBLISH_LISTING_REQUEST);
export const publishListingSuccess = successAction(PUBLISH_LISTING_SUCCESS);
export const publishListingError = errorAction(PUBLISH_LISTING_ERROR);
// SDK method: ownListings.update
export const updateListing = requestAction(UPDATE_LISTING_REQUEST);
@ -267,40 +256,56 @@ export function requestShowListing(actionPayload) {
};
}
export function requestCreateListing(data) {
export function requestCreateListingDraft(data) {
return (dispatch, getState, sdk) => {
dispatch(createListing(data));
dispatch(createListingDraft(data));
const queryParams = {
expand: true,
include: ['author', 'images'],
'fields.image': ['variants.landscape-crop', 'variants.landscape-crop2x'],
};
return sdk.ownListings
.create(data)
.then(response => {
const id = response.data.data.id.uuid;
// Modify store to understand that we have created listing and can redirect away
dispatch(createListingSuccess(response));
// Fetch listing data so that redirection is smooth
dispatch(
requestShowListing({
id,
include: ['author', 'images'],
'fields.image': ['variants.landscape-crop', 'variants.landscape-crop2x'],
})
);
return response;
})
.createDraft(data, queryParams)
.then(response => {
//const id = response.data.data.id.uuid;
// Add the created listing to the marketplace data
dispatch(addMarketplaceEntities(response));
// We must update the user duck since this might be the first
// listing for the user, therefore changing the
// currentUserHasListings flag in the store.
dispatch(fetchCurrentUserHasListingsSuccess(true));
// Modify store to understand that we have created listing and can redirect away
dispatch(createListingDraftSuccess(response));
return response;
})
.catch(e => {
log.error(e, 'create-listing-failed', { listingData: data });
return dispatch(createListingError(storableError(e)));
log.error(e, 'create-listing-draft-failed', { listingData: data });
return dispatch(createListingDraftError(storableError(e)));
});
};
}
export const requestPublishListingDraft = listingId => (dispatch, getState, sdk) => {
dispatch(publishListing(listingId));
return sdk.ownListings
.publishDraft({ id: listingId }, { expand: true })
.then(response => {
// Add the created listing to the marketplace data
dispatch(addMarketplaceEntities(response));
dispatch(publishListingSuccess(response));
return response;
})
.catch(e => {
dispatch(publishListingError(storableError(e)));
});
};
// Images return imageId which we need to map with previously generated temporary id
export function requestImageUpload(actionPayload) {
return (dispatch, getState, sdk) => {
@ -335,6 +340,7 @@ export function requestUpdateListing(tab, data) {
.then(() => {
dispatch(markTabUpdated(tab));
dispatch(updateListingSuccess(updateResponse));
return updateResponse;
})
.catch(e => {
log.error(e, 'update-listing-failed', { listingData: data });

View file

@ -5,8 +5,15 @@ import { withRouter } from 'react-router-dom';
import { intlShape, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { types as sdkTypes } from '../../util/sdkLoader';
import { LISTING_PAGE_PENDING_APPROVAL_VARIANT, createSlug } from '../../util/urlHelpers';
import { LISTING_STATE_PENDING_APPROVAL, propTypes } from '../../util/types';
import {
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_NEW,
LISTING_PAGE_PARAM_TYPES,
LISTING_PAGE_PENDING_APPROVAL_VARIANT,
createSlug,
} from '../../util/urlHelpers';
import { LISTING_STATE_DRAFT, LISTING_STATE_PENDING_APPROVAL, propTypes } from '../../util/types';
import { ensureOwnListing } from '../../util/data';
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
import { stripeAccountClearError, createStripeAccount } from '../../ducks/user.duck';
@ -14,9 +21,8 @@ import { EditListingWizard, NamedRedirect, Page } from '../../components';
import { TopbarContainer } from '../../containers';
import {
createListingDraft,
updateListingDraft,
requestCreateListing,
requestCreateListingDraft,
requestPublishListingDraft,
requestUpdateListing,
requestImageUpload,
updateImageOrder,
@ -38,16 +44,15 @@ export const EditListingPageComponent = props => {
getOwnListing,
history,
intl,
onCreateListing,
onUpdateListing,
onCreateListingDraft,
onPublishListingDraft,
onUpdateListing,
onImageUpload,
onRemoveListingImage,
onManageDisableScrolling,
onPayoutDetailsSubmit,
onPayoutDetailsFormChange,
onUpdateImageOrder,
onUpdateListingDraft,
onChange,
page,
params,
@ -55,18 +60,21 @@ export const EditListingPageComponent = props => {
} = props;
const { id, type } = params;
const isNewURI = type === LISTING_PAGE_PARAM_TYPE_NEW;
const isDraftURI = type === LISTING_PAGE_PARAM_TYPE_DRAFT;
const isNew = type === 'new';
const newListingCreated = isNew && !!page.submittedListingId;
const listingId = page.submittedListingId || (id ? new UUID(id) : null);
const currentListing = getOwnListing(listingId);
const isPendingApproval =
currentListing && currentListing.attributes.state === LISTING_STATE_PENDING_APPROVAL;
const currentListing = ensureOwnListing(getOwnListing(listingId));
const { state: currentListingState } = currentListing.attributes;
const shouldRedirect = page.submittedListingId && currentListing;
const showForm = isNew || currentListing;
const isPastDraft = currentListingState && currentListingState !== LISTING_STATE_DRAFT;
const shouldRedirect = (isNewURI || isDraftURI) && listingId && isPastDraft;
const showForm = isNewURI || currentListing.id;
if (shouldRedirect) {
const isPendingApproval =
currentListing && currentListingState === LISTING_STATE_PENDING_APPROVAL;
// If page has already listingId (after submit) and current listings exist
// redirect to listing page
const listingSlug = currentListing ? createSlug(currentListing.attributes.title) : null;
@ -91,36 +99,44 @@ export const EditListingPageComponent = props => {
return <NamedRedirect {...redirectProps} />;
} else if (showForm) {
const {
createListingsError = null,
createListingDraftError = null,
publishListingError = null,
updateListingError = null,
showListingsError = null,
uploadImageError = null,
} = page;
const errors = {
createListingsError,
createListingDraftError,
publishListingError,
updateListingError,
showListingsError,
uploadImageError,
createStripeAccountError,
};
const newListingPublished =
isDraftURI && currentListing && currentListingState !== LISTING_STATE_DRAFT;
// Show form if user is posting a new listing or editing existing one
const disableForm = page.redirectToListing && !showListingsError;
// Images are passed to EditListingForm so that it can generate thumbnails out of them
const currentListingImages = currentListing ? currentListing.images : [];
const currentListingImages =
currentListing && currentListing.images ? currentListing.images : [];
// Images not yet connected to the listing
const unattachedImages = page.imageOrder.map(i => page.images[i]);
const imageOrder = page.imageOrder || [];
const unattachedImages = imageOrder.map(i => page.images[i]);
const allImages = currentListingImages.concat(unattachedImages);
const removedImageIds = page.removedImageIds || [];
const images = allImages.filter(img => {
return !page.removedImageIds.includes(img.id);
return !removedImageIds.includes(img.id);
});
const title = isNew
? intl.formatMessage({ id: 'EditListingPage.titleCreateListing' })
: intl.formatMessage({ id: 'EditListingPage.titleEditListing' });
const title =
isNewURI || isDraftURI
? intl.formatMessage({ id: 'EditListingPage.titleCreateListing' })
: intl.formatMessage({ id: 'EditListingPage.titleEditListing' });
return (
<Page title={title} scrollingDisabled={scrollingDisabled}>
@ -137,14 +153,13 @@ export const EditListingPageComponent = props => {
disabled={disableForm}
errors={errors}
fetchInProgress={fetchInProgress}
newListingCreated={newListingCreated}
newListingPublished={newListingPublished}
history={history}
images={images}
listing={isNew ? page.listingDraft : currentListing}
onCreateListing={onCreateListing}
listing={currentListing}
onUpdateListing={onUpdateListing}
onCreateListingDraft={onCreateListingDraft}
onUpdateListingDraft={onUpdateListingDraft}
onPublishListingDraft={onPublishListingDraft}
onPayoutDetailsFormChange={onPayoutDetailsFormChange}
onPayoutDetailsSubmit={onPayoutDetailsSubmit}
onImageUpload={onImageUpload}
@ -154,7 +169,7 @@ export const EditListingPageComponent = props => {
currentUser={currentUser}
onManageDisableScrolling={onManageDisableScrolling}
updatedTab={page.updatedTab}
updateInProgress={page.updateInProgress || page.createListingInProgress}
updateInProgress={page.updateInProgress || page.createListingDraftInProgress}
/>
</Page>
);
@ -187,22 +202,21 @@ EditListingPageComponent.propTypes = {
currentUser: propTypes.currentUser,
fetchInProgress: bool.isRequired,
getOwnListing: func.isRequired,
onCreateListing: func.isRequired,
onCreateListingDraft: func.isRequired,
onPublishListingDraft: func.isRequired,
onImageUpload: func.isRequired,
onManageDisableScrolling: func.isRequired,
onPayoutDetailsFormChange: func.isRequired,
onPayoutDetailsSubmit: func.isRequired,
onUpdateImageOrder: func.isRequired,
onRemoveListingImage: func.isRequired,
onUpdateListingDraft: func.isRequired,
onUpdateListing: func.isRequired,
onChange: func.isRequired,
page: object.isRequired,
params: shape({
id: string.isRequired,
slug: string.isRequired,
type: oneOf(['new', 'edit']).isRequired,
type: oneOf(LISTING_PAGE_PARAM_TYPES).isRequired,
tab: string.isRequired,
}).isRequired,
scrollingDisabled: bool.isRequired,
@ -238,9 +252,9 @@ const mapStateToProps = state => {
};
const mapDispatchToProps = dispatch => ({
onCreateListing: values => dispatch(requestCreateListing(values)),
onUpdateListing: (tab, values) => dispatch(requestUpdateListing(tab, values)),
onCreateListingDraft: values => dispatch(createListingDraft(values)),
onCreateListingDraft: values => dispatch(requestCreateListingDraft(values)),
onPublishListingDraft: listingId => dispatch(requestPublishListingDraft(listingId)),
onImageUpload: data => dispatch(requestImageUpload(data)),
onManageDisableScrolling: (componentId, disableScrolling) =>
dispatch(manageDisableScrolling(componentId, disableScrolling)),
@ -248,7 +262,6 @@ const mapDispatchToProps = dispatch => ({
onPayoutDetailsSubmit: values => dispatch(createStripeAccount(values)),
onUpdateImageOrder: imageOrder => dispatch(updateImageOrder(imageOrder)),
onRemoveListingImage: imageId => dispatch(removeListingImage(imageId)),
onUpdateListingDraft: values => dispatch(updateListingDraft(values)),
onChange: () => dispatch(clearUpdatedTab()),
});

View file

@ -24,7 +24,7 @@ describe('EditListingPageComponent', () => {
onManageDisableScrolling={noop}
onCreateListing={noop}
onCreateListingDraft={noop}
onUpdateListingDraft={noop}
onPublishListingDraft={noop}
onUpdateListing={noop}
onImageUpload={noop}
onRemoveListingImage={noop}

View file

@ -10,8 +10,9 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
currentUser={null}
errors={
Object {
"createListingsError": null,
"createListingDraftError": null,
"createStripeAccountError": null,
"publishListingError": null,
"showListingsError": null,
"updateListingError": null,
"uploadImageError": null,
@ -25,18 +26,27 @@ exports[`EditListingPageComponent matches snapshot 1`] = `
}
id="EditListingWizard"
images={Array []}
newListingCreated={false}
listing={
Object {
"attributes": Object {
"publicData": Object {},
},
"id": null,
"images": Array [],
"type": "ownListing",
}
}
newListingPublished={false}
onChange={[Function]}
onCreateListing={[Function]}
onCreateListingDraft={[Function]}
onImageUpload={[Function]}
onManageDisableScrolling={[Function]}
onPayoutDetailsFormChange={[Function]}
onPayoutDetailsSubmit={[Function]}
onPublishListingDraft={[Function]}
onRemoveImage={[Function]}
onUpdateImageOrder={[Function]}
onUpdateListing={[Function]}
onUpdateListingDraft={[Function]}
params={
Object {
"id": "id",

View file

@ -2,7 +2,12 @@ import React from 'react';
import { bool, oneOfType, object } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { LISTING_STATE_PENDING_APPROVAL, LISTING_STATE_CLOSED, propTypes } from '../../util/types';
import {
LISTING_STATE_PENDING_APPROVAL,
LISTING_STATE_CLOSED,
LISTING_STATE_DRAFT,
propTypes,
} from '../../util/types';
import { NamedLink } from '../../components';
import EditIcon from './EditIcon';
@ -13,6 +18,7 @@ export const ActionBarMaybe = props => {
const state = listing.attributes.state;
const isPendingApproval = state === LISTING_STATE_PENDING_APPROVAL;
const isClosed = state === LISTING_STATE_CLOSED;
const isDraft = state === LISTING_STATE_DRAFT;
if (isOwnListing) {
let ownListingTextTranslationId = 'ListingPage.ownListing';
@ -21,8 +27,12 @@ export const ActionBarMaybe = props => {
ownListingTextTranslationId = 'ListingPage.ownListingPendingApproval';
} else if (isClosed) {
ownListingTextTranslationId = 'ListingPage.ownClosedListing';
} else if (isDraft) {
ownListingTextTranslationId = 'ListingPage.ownListingDraft';
}
const message = isDraft ? 'ListingPage.finishListing' : 'ListingPage.editListing';
const ownListingTextClasses = classNames(css.ownListingText, {
[css.ownListingTextPendingApproval]: isPendingApproval,
});
@ -34,7 +44,7 @@ export const ActionBarMaybe = props => {
</p>
<NamedLink className={css.editListingLink} name="EditListingPage" params={editParams}>
<EditIcon className={css.editIcon} />
<FormattedMessage id="ListingPage.editListing" />
<FormattedMessage id={message} />
</NamedLink>
</div>
);

View file

@ -6,7 +6,10 @@ import { storableError } from '../../util/errors';
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { denormalisedResponseEntities } from '../../util/data';
import { TRANSITION_ENQUIRE } from '../../util/types';
import { LISTING_PAGE_PENDING_APPROVAL_VARIANT } from '../../util/urlHelpers';
import {
LISTING_PAGE_DRAFT_VARIANT,
LISTING_PAGE_PENDING_APPROVAL_VARIANT,
} from '../../util/urlHelpers';
import { fetchCurrentUser, fetchCurrentUserHasOrdersSuccess } from '../../ducks/user.duck';
const { UUID } = sdkTypes;
@ -269,7 +272,8 @@ export const sendEnquiry = (listingId, message) => (dispatch, getState, sdk) =>
export const loadData = (params, search) => dispatch => {
const listingId = new UUID(params.id);
if (params.variant === LISTING_PAGE_PENDING_APPROVAL_VARIANT) {
const ownListingVariants = [LISTING_PAGE_DRAFT_VARIANT, LISTING_PAGE_PENDING_APPROVAL_VARIANT];
if (ownListingVariants.includes(params.variant)) {
return dispatch(showListing(listingId, true));
}

View file

@ -9,7 +9,14 @@ import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
import { LISTING_STATE_PENDING_APPROVAL, LISTING_STATE_CLOSED, propTypes } from '../../util/types';
import { types as sdkTypes } from '../../util/sdkLoader';
import { LISTING_PAGE_PENDING_APPROVAL_VARIANT, createSlug, parse } from '../../util/urlHelpers';
import {
LISTING_PAGE_DRAFT_VARIANT,
LISTING_PAGE_PENDING_APPROVAL_VARIANT,
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_EDIT,
createSlug,
parse,
} from '../../util/urlHelpers';
import { formatMoney } from '../../util/currency';
import { createResourceLocatorString, findRouteByRouteName } from '../../util/routes';
import { ensureListing, ensureOwnListing, ensureUser, userDisplayName } from '../../util/data';
@ -32,7 +39,7 @@ import { sendEnquiry, loadData, setInitialValues } from './ListingPage.duck';
import SectionImages from './SectionImages';
import SectionAvatar from './SectionAvatar';
import SectionHeading from './SectionHeading';
import SectionDescription from './SectionDescription';
import SectionDescriptionMaybe from './SectionDescriptionMaybe';
import SectionFeatures from './SectionFeatures';
import SectionReviews from './SectionReviews';
import SectionHost from './SectionHost';
@ -206,13 +213,20 @@ export class ListingPageComponent extends Component {
const isBook = !!parse(location.search).book;
const listingId = new UUID(rawParams.id);
const isPendingApprovalVariant = rawParams.variant === LISTING_PAGE_PENDING_APPROVAL_VARIANT;
const currentListing = isPendingApprovalVariant
? ensureOwnListing(getOwnListing(listingId))
: ensureListing(getListing(listingId));
const isDraftVariant = rawParams.variant === LISTING_PAGE_DRAFT_VARIANT;
const currentListing =
isPendingApprovalVariant || isDraftVariant
? ensureOwnListing(getOwnListing(listingId))
: ensureListing(getListing(listingId));
const listingSlug = rawParams.slug || createSlug(currentListing.attributes.title || '');
const params = { slug: listingSlug, ...rawParams };
const listingType = isDraftVariant
? LISTING_PAGE_PARAM_TYPE_DRAFT
: LISTING_PAGE_PARAM_TYPE_EDIT;
const listingTab = isDraftVariant ? 'photos' : 'description';
const isApproved =
currentListing.id && currentListing.attributes.state !== LISTING_STATE_PENDING_APPROVAL;
@ -224,7 +238,9 @@ export class ListingPageComponent extends Component {
// another user. We use this information to try to fetch the
// public listing.
const pendingOtherUsersListing =
isPendingApprovalVariant && showListingError && showListingError.status === 403;
(isPendingApprovalVariant || isDraftVariant) &&
showListingError &&
showListingError.status === 403;
const shouldShowPublicListingPage = pendingIsApproved || pendingOtherUsersListing;
if (shouldShowPublicListingPage) {
@ -418,8 +434,8 @@ export class ListingPageComponent extends Component {
editParams={{
id: listingId.uuid,
slug: listingSlug,
type: 'edit',
tab: 'description',
type: listingType,
tab: listingTab,
}}
imageCarouselOpen={this.state.imageCarouselOpen}
onImageCarouselClose={() => this.setState({ imageCarouselOpen: false })}
@ -438,7 +454,7 @@ export class ListingPageComponent extends Component {
showContactUser={showContactUser}
onContactUser={this.onContactUser}
/>
<SectionDescription description={description} />
<SectionDescriptionMaybe description={description} />
<SectionFeatures
options={amenitiesConfig}
selectedOptions={publicData.amenities}
@ -524,7 +540,7 @@ ListingPageComponent.propTypes = {
params: shape({
id: string.isRequired,
slug: string,
variant: oneOf([LISTING_PAGE_PENDING_APPROVAL_VARIANT]),
variant: oneOf([LISTING_PAGE_DRAFT_VARIANT, LISTING_PAGE_PENDING_APPROVAL_VARIANT]),
}).isRequired,
isAuthenticated: bool.isRequired,

View file

@ -6,9 +6,9 @@ import css from './ListingPage.css';
const MIN_LENGTH_FOR_LONG_WORDS_IN_DESCRIPTION = 20;
const SectionDescription = props => {
const SectionDescriptionMaybe = props => {
const { description } = props;
return (
return description ? (
<div className={css.sectionDescription}>
<h2 className={css.descriptionTitle}>
<FormattedMessage id="ListingPage.descriptionTitle" />
@ -20,7 +20,7 @@ const SectionDescription = props => {
})}
</p>
</div>
);
) : null;
};
export default SectionDescription;
export default SectionDescriptionMaybe;

View file

@ -147,7 +147,7 @@ exports[`ListingPage matches snapshot 1`] = `
}
showContactUser={true}
/>
<SectionDescription
<SectionDescriptionMaybe
description="listing1 description"
/>
<SectionFeatures

View file

@ -1,5 +1,5 @@
import React from 'react';
import { bool, func, string, arrayOf, shape } from 'prop-types';
import { arrayOf, bool, func, shape, string } from 'prop-types';
import { compose } from 'redux';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
@ -27,8 +27,8 @@ const EditListingDescriptionFormComponent = props => (
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
fetchErrors,
} = fieldRenderProps;
const titleMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.title' });
@ -56,12 +56,26 @@ const EditListingDescriptionFormComponent = props => (
id: 'EditListingDescriptionForm.descriptionRequired',
});
const errorMessage = updateError ? (
const { updateListingError, createListingDraftError, showListingsError } = fetchErrors || {};
const errorMessageUpdateListing = updateListingError ? (
<p className={css.error}>
<FormattedMessage id="EditListingDescriptionForm.updateFailed" />
</p>
) : null;
// This error happens only on first tab (of EditListingWizard)
const errorMessageCreateListingDraft = createListingDraftError ? (
<p className={css.error}>
<FormattedMessage id="EditListingDescriptionForm.createListingDraftError" />
</p>
) : null;
const errorMessageShowListing = showListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingDescriptionForm.showListingFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
@ -69,7 +83,9 @@ const EditListingDescriptionFormComponent = props => (
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
{errorMessageCreateListingDraft}
{errorMessageUpdateListing}
{errorMessageShowListing}
<FieldTextInput
id="title"
name="title"
@ -114,7 +130,7 @@ const EditListingDescriptionFormComponent = props => (
/>
);
EditListingDescriptionFormComponent.defaultProps = { className: null, updateError: null };
EditListingDescriptionFormComponent.defaultProps = { className: null, fetchErrors: null };
EditListingDescriptionFormComponent.propTypes = {
className: string,
@ -122,8 +138,12 @@ EditListingDescriptionFormComponent.propTypes = {
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
fetchErrors: shape({
createListingDraftError: propTypes.error,
showListingsError: propTypes.error,
updateListingError: propTypes.error,
}),
categories: arrayOf(
shape({
key: string.isRequired,

View file

@ -1,5 +1,5 @@
import React from 'react';
import { bool, func, string } from 'prop-types';
import { bool, func, shape, string } from 'prop-types';
import classNames from 'classnames';
import { Form as FinalForm } from 'react-final-form';
import arrayMutators from 'final-form-arrays';
@ -25,8 +25,8 @@ const EditListingFeaturesFormComponent = props => (
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
fetchErrors,
} = fieldRenderProps;
const classes = classNames(rootClassName || css.root, className);
@ -34,15 +34,23 @@ const EditListingFeaturesFormComponent = props => (
const submitInProgress = updateInProgress;
const submitDisabled = disabled || submitInProgress;
const errorMessage = updateError ? (
const { updateListingError, showListingsError } = fetchErrors || {};
const errorMessage = updateListingError ? (
<p className={css.error}>
<FormattedMessage id="EditListingFeaturesForm.updateFailed" />
</p>
) : null;
const errorMessageShowListing = showListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingFeaturesForm.showListingFailed" />
</p>
) : null;
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
{errorMessageShowListing}
<FieldCheckboxGroup
className={css.features}
@ -69,7 +77,7 @@ const EditListingFeaturesFormComponent = props => (
EditListingFeaturesFormComponent.defaultProps = {
rootClassName: null,
className: null,
updateError: null,
fetchErrors: null,
};
EditListingFeaturesFormComponent.propTypes = {
@ -79,8 +87,11 @@ EditListingFeaturesFormComponent.propTypes = {
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
fetchErrors: shape({
showListingsError: propTypes.error,
updateListingError: propTypes.error,
}),
};
const EditListingFeaturesForm = EditListingFeaturesFormComponent;

View file

@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bool, func, shape, string } from 'prop-types';
import { compose } from 'redux';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
@ -27,8 +27,8 @@ export const EditListingLocationFormComponent = props => (
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
fetchErrors,
values,
} = fieldRenderProps;
@ -48,12 +48,19 @@ export const EditListingLocationFormComponent = props => (
id: 'EditListingLocationForm.buildingPlaceholder',
});
const errorMessage = updateError ? (
const { updateListingError, showListingsError } = fetchErrors || {};
const errorMessage = updateListingError ? (
<p className={css.error}>
<FormattedMessage id="EditListingLocationForm.updateFailed" />
</p>
) : null;
const errorMessageShowListing = showListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingLocationForm.showListingFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
@ -62,6 +69,7 @@ export const EditListingLocationFormComponent = props => (
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
{errorMessageShowListing}
<LocationAutocompleteInputField
className={css.locationAddress}
inputClassName={css.locationAutocompleteInput}
@ -107,19 +115,20 @@ export const EditListingLocationFormComponent = props => (
EditListingLocationFormComponent.defaultProps = {
selectedPlace: null,
updateError: null,
fetchErrors: null,
};
const { func, string, bool } = PropTypes;
EditListingLocationFormComponent.propTypes = {
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
selectedPlace: propTypes.place,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
fetchErrors: shape({
showListingsError: propTypes.error,
updateListingError: propTypes.error,
}),
};
export default compose(injectIntl)(EditListingLocationFormComponent);

View file

@ -3,6 +3,7 @@
exports[`EditListingLocationForm matches snapshot 1`] = `
<ReactFinalForm
dispatch={[Function]}
fetchErrors={null}
intl={
Object {
"formatDate": [Function],
@ -19,7 +20,6 @@ exports[`EditListingLocationForm matches snapshot 1`] = `
render={[Function]}
saveActionMsg="Save location"
selectedPlace={null}
updateError={null}
updateInProgress={false}
updated={false}
/>

View file

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { array, bool, func, object, shape, string } from 'prop-types';
import { array, bool, func, shape, string } from 'prop-types';
import { compose } from 'redux';
import { Form as FinalForm, Field } from 'react-final-form';
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
@ -48,7 +48,7 @@ export class EditListingPhotosFormComponent extends Component {
form,
className,
disabled,
errors,
fetchErrors,
handleSubmit,
images,
imageUploadRequested,
@ -59,7 +59,6 @@ export class EditListingPhotosFormComponent extends Component {
ready,
saveActionMsg,
updated,
updateError,
updateInProgress,
} = fieldRenderProps;
@ -78,7 +77,8 @@ export class EditListingPhotosFormComponent extends Component {
id: 'EditListingPhotosForm.imageRequired',
});
const { createListingsError, showListingsError, uploadImageError } = errors;
const { publishListingError, showListingsError, updateListingError, uploadImageError } =
fetchErrors || {};
const uploadOverLimit = isUploadImageOverLimitError(uploadImageError);
let uploadImageFailed = null;
@ -100,9 +100,9 @@ export class EditListingPhotosFormComponent extends Component {
// NOTE: These error messages are here since Photos panel is the last visible panel
// before creating a new listing. If that order is changed, these should be changed too.
// Create and show listing errors are shown above submit button
const createListingFailed = createListingsError ? (
const publishListingFailed = publishListingError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.createListingFailed" />
<FormattedMessage id="EditListingPhotosForm.publishListingFailed" />
</p>
) : null;
const showListingFailed = showListingsError ? (
@ -135,7 +135,7 @@ export class EditListingPhotosFormComponent extends Component {
handleSubmit(e);
}}
>
{updateError ? (
{updateListingError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPhotosForm.updateFailed" />
</p>
@ -203,7 +203,7 @@ export class EditListingPhotosFormComponent extends Component {
<p className={css.tip}>
<FormattedMessage id="EditListingPhotosForm.addImagesTip" />
</p>
{createListingFailed}
{publishListingFailed}
{showListingFailed}
<Button
@ -223,13 +223,14 @@ export class EditListingPhotosFormComponent extends Component {
}
}
EditListingPhotosFormComponent.defaultProps = { errors: {}, updateError: null, images: [] };
EditListingPhotosFormComponent.defaultProps = { fetchErrors: null, images: [] };
EditListingPhotosFormComponent.propTypes = {
errors: shape({
createListingsError: object,
showListingsError: object,
uploadImageError: object,
fetchErrors: shape({
publishListingError: propTypes.error,
showListingsError: propTypes.error,
uploadImageError: propTypes.error,
updateListingError: propTypes.error,
}),
images: array,
intl: intlShape.isRequired,
@ -239,7 +240,6 @@ EditListingPhotosFormComponent.propTypes = {
saveActionMsg: string.isRequired,
updated: bool.isRequired,
ready: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
onRemoveImage: func.isRequired,
};

View file

@ -3,7 +3,7 @@
exports[`EditListingPhotosForm matches snapshot 1`] = `
<ReactFinalForm
dispatch={[Function]}
errors={Object {}}
fetchErrors={null}
imageUploadRequested={false}
images={Array []}
initialValues={
@ -32,7 +32,6 @@ exports[`EditListingPhotosForm matches snapshot 1`] = `
render={[Function]}
saveActionMsg="Save photos"
stripeConnected={false}
updateError={null}
updateInProgress={false}
updated={false}
/>

View file

@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bool, func, shape, string } from 'prop-types';
import { compose } from 'redux';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
@ -22,8 +22,8 @@ export const EditListingPoliciesFormComponent = props => (
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
fetchErrors,
} = fieldRenderProps;
const rulesLabelMessage = intl.formatMessage({
@ -33,11 +33,17 @@ export const EditListingPoliciesFormComponent = props => (
id: 'EditListingPoliciesForm.rulesPlaceholder',
});
const errorMessage = updateError ? (
const { updateListingError, showListingsError } = fetchErrors || {};
const errorMessage = updateListingError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPoliciesForm.updateFailed" />
</p>
) : null;
const errorMessageShowListing = showListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPoliciesForm.showListingFailed" />
</p>
) : null;
const classes = classNames(css.root, className);
const submitReady = updated && pristine;
@ -47,6 +53,7 @@ export const EditListingPoliciesFormComponent = props => (
return (
<Form className={classes} onSubmit={handleSubmit}>
{errorMessage}
{errorMessageShowListing}
<FieldTextInput
id="rules"
@ -77,16 +84,17 @@ EditListingPoliciesFormComponent.defaultProps = {
updateError: null,
};
const { func, string, bool } = PropTypes;
EditListingPoliciesFormComponent.propTypes = {
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
selectedPlace: propTypes.place,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
fetchErrors: shape({
showListingsError: propTypes.error,
updateListingError: propTypes.error,
}),
};
export default compose(injectIntl)(EditListingPoliciesFormComponent);

View file

@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bool, func, shape, string } from 'prop-types';
import { compose } from 'redux';
import { Form as FinalForm } from 'react-final-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
@ -28,8 +28,8 @@ export const EditListingPricingFormComponent = props => (
pristine,
saveActionMsg,
updated,
updateError,
updateInProgress,
fetchErrors,
} = fieldRenderProps;
const pricePerUnitMessage = intl.formatMessage({
@ -64,14 +64,20 @@ export const EditListingPricingFormComponent = props => (
const submitReady = updated && pristine;
const submitInProgress = updateInProgress;
const submitDisabled = invalid || disabled || submitInProgress;
const { updateListingError, showListingsError } = fetchErrors || {};
return (
<Form onSubmit={handleSubmit} className={classes}>
{updateError ? (
{updateListingError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPricingForm.updateFailed" />
</p>
) : null}
{showListingsError ? (
<p className={css.error}>
<FormattedMessage id="EditListingPricingForm.showListingFailed" />
</p>
) : null}
<FieldCurrencyInput
id="price"
name="price"
@ -98,17 +104,18 @@ export const EditListingPricingFormComponent = props => (
/>
);
EditListingPricingFormComponent.defaultProps = { updateError: null };
const { bool, func, string } = PropTypes;
EditListingPricingFormComponent.defaultProps = { fetchErrors: null };
EditListingPricingFormComponent.propTypes = {
intl: intlShape.isRequired,
onSubmit: func.isRequired,
saveActionMsg: string.isRequired,
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
fetchErrors: shape({
showListingsError: propTypes.error,
updateListingError: propTypes.error,
}),
};
export default compose(injectIntl)(EditListingPricingFormComponent);

View file

@ -120,16 +120,19 @@
"EditListingDescriptionForm.categoryLabel": "Sauna type",
"EditListingDescriptionForm.categoryPlaceholder": "Choose the type of your sauna…",
"EditListingDescriptionForm.categoryRequired": "You need to select a category for your sauna.",
"EditListingDescriptionForm.createListingDraftError": "Failed to create a draft listing. Please try again.",
"EditListingDescriptionForm.description": "Describe your sauna",
"EditListingDescriptionForm.descriptionPlaceholder": "How many people can fit at once? Does the sauna come with towels?",
"EditListingDescriptionForm.descriptionRequired": "A description is required.",
"EditListingDescriptionForm.maxLength": "Must be {maxLength} characters or less",
"EditListingDescriptionForm.showListingFailed": "Fetching listing data failed",
"EditListingDescriptionForm.title": "The name of your sauna",
"EditListingDescriptionForm.titlePlaceholder": "What's the name of your sauna?",
"EditListingDescriptionForm.titleRequired": "You need to add a name.",
"EditListingDescriptionForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingDescriptionPanel.createListingTitle": "Add your sauna",
"EditListingDescriptionPanel.title": "Edit details of {listingTitle}",
"EditListingFeaturesForm.showListingFailed": "Fetching listing data failed",
"EditListingFeaturesForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingFeaturesPanel.createListingTitle": "What amenities does your sauna have?",
"EditListingFeaturesPanel.title": "Edit the amenities of {listingTitle}",
@ -139,6 +142,7 @@
"EditListingLocationForm.addressRequired": "You need to provide a location",
"EditListingLocationForm.building": "Apt, suite, building # (optional)",
"EditListingLocationForm.buildingPlaceholder": "A 42",
"EditListingLocationForm.showListingFailed": "Fetching listing data failed",
"EditListingLocationForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingLocationPanel.createListingTitle": "Where's your sauna?",
"EditListingLocationPanel.title": "Edit the location of {listingTitle}",
@ -148,11 +152,11 @@
"EditListingPhotosForm.addImagesTip": "Tip: Choose the top 2-3 photos of your sauna from different angles in a good light that really show the space.",
"EditListingPhotosForm.bankAccountNumberRequired": "You need to add a bank account number.",
"EditListingPhotosForm.chooseImage": "+ Choose an image…",
"EditListingPhotosForm.createListingFailed": "Adding a new sauna failed",
"EditListingPhotosForm.imageRequired": "You need to add at least one image.",
"EditListingPhotosForm.imageTypes": ".JPG, .GIF or .PNG. Max. 10 MB",
"EditListingPhotosForm.imageUploadFailed.uploadFailed": "Failed to upload image. Please check that the image wasn't too big and try again.",
"EditListingPhotosForm.imageUploadFailed.uploadOverLimit": "The maximum file size is 10 MB. Please try again.",
"EditListingPhotosForm.publishListingFailed": "Publishing a new sauna failed",
"EditListingPhotosForm.savedImageAltText": "Saved listing image",
"EditListingPhotosForm.showListingFailed": "Fetching listing data failed",
"EditListingPhotosForm.updateFailed": "Failed to update listing. Please try again.",
@ -163,6 +167,7 @@
"EditListingPhotosPanel.title": "Edit the photos of {listingTitle}",
"EditListingPoliciesForm.rulesLabel": "Sauna rules",
"EditListingPoliciesForm.rulesPlaceholder": "Describe the no-nos",
"EditListingPoliciesForm.showListingFailed": "Fetching listing data failed",
"EditListingPoliciesForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingPoliciesPanel.createListingTitle": "Time to set some ground rules for the bathers.",
"EditListingPoliciesPanel.title": "Edit the rules of {listingTitle}",
@ -170,6 +175,7 @@
"EditListingPricingForm.pricePerUnit": "Price per night in euros",
"EditListingPricingForm.priceRequired": "You need to add a valid price.",
"EditListingPricingForm.priceTooLow": "Price should be at least {minPrice}.",
"EditListingPricingForm.showListingFailed": "Fetching listing data failed",
"EditListingPricingForm.updateFailed": "Failed to update listing. Please try again.",
"EditListingPricingPanel.createListingTitle": "How much does it cost?",
"EditListingPricingPanel.listingPriceCurrencyInvalid": "Listing currency is different from the marketplace currency. You cannot edit the price.",
@ -289,12 +295,14 @@
"ListingPage.errorLoadingListingMessage": "Could not load listing. Please try again.",
"ListingPage.errorLoadingListingTitle": "Error in loading listing",
"ListingPage.featuresTitle": "Amenities",
"ListingPage.finishListing": "Finish listing",
"ListingPage.hostedBy": "Hosted by {name}",
"ListingPage.loadingListingMessage": "Loading listing…",
"ListingPage.loadingListingTitle": "Loading listing…",
"ListingPage.locationTitle": "Location",
"ListingPage.ownClosedListing": "Your listing has been closed and can't be booked.",
"ListingPage.ownListing": "This is your own listing.",
"ListingPage.ownListingDraft": "This listing is a draft.",
"ListingPage.ownListingPendingApproval": "This listing is pending approval.",
"ListingPage.perUnit": "per night",
"ListingPage.reviewsError": "Loading reviews failed.",
@ -318,10 +326,14 @@
"ManageListingCard.actionFailed": "Whoops, something went wrong. Please refresh the page and try again.",
"ManageListingCard.closeListing": "Close listing",
"ManageListingCard.closedListing": "This listing is closed and not visible on the marketplace.",
"ManageListingCard.edit": "Edit",
"ManageListingCard.draftOverlayText": "{listingTitle} is a draft and can't be booked.",
"ManageListingCard.editListing": "Edit listing",
"ManageListingCard.finishListingDraft": "Finish listing",
"ManageListingCard.manageAvailability": "Manage availability",
"ManageListingCard.openListing": "Open listing",
"ManageListingCard.pendingApproval": "{listingTitle} is pending admin approval and can't be booked.",
"ManageListingCard.perUnit": "per night",
"ManageListingCard.priceNotSet": "Price per night not set",
"ManageListingCard.unsupportedPrice": "({currency})",
"ManageListingCard.unsupportedPriceTitle": "Unsupported currency ({currency})",
"ManageListingCard.viewListing": "View listing",

View file

@ -124,11 +124,13 @@ propTypes.user = shape({
profileImage: propTypes.image,
});
export const LISTING_STATE_DRAFT = 'draft';
export const LISTING_STATE_PENDING_APPROVAL = 'pendingApproval';
export const LISTING_STATE_PUBLISHED = 'published';
export const LISTING_STATE_CLOSED = 'closed';
const LISTING_STATES = [
LISTING_STATE_DRAFT,
LISTING_STATE_PENDING_APPROVAL,
LISTING_STATE_PUBLISHED,
LISTING_STATE_CLOSED,
@ -146,7 +148,7 @@ const listingAttributes = shape({
const ownListingAttributes = shape({
title: string.isRequired,
description: string.isRequired,
description: string,
geolocation: propTypes.latlng,
deleted: propTypes.value(false).isRequired,
state: oneOf(LISTING_STATES).isRequired,

View file

@ -4,6 +4,16 @@ import { types as sdkTypes } from './sdkLoader';
const { LatLng, LatLngBounds } = sdkTypes;
export const LISTING_PAGE_PENDING_APPROVAL_VARIANT = 'pending-approval';
export const LISTING_PAGE_DRAFT_VARIANT = 'draft';
export const LISTING_PAGE_PARAM_TYPE_NEW = 'new';
export const LISTING_PAGE_PARAM_TYPE_DRAFT = 'draft';
export const LISTING_PAGE_PARAM_TYPE_EDIT = 'edit';
export const LISTING_PAGE_PARAM_TYPES = [
LISTING_PAGE_PARAM_TYPE_NEW,
LISTING_PAGE_PARAM_TYPE_DRAFT,
LISTING_PAGE_PARAM_TYPE_EDIT,
];
// Create slug from random texts
// From Gist thread: https://gist.github.com/mathewbyrne/1280286

View file

@ -7486,9 +7486,9 @@ shallowequal@^0.2.2:
dependencies:
lodash.keys "^3.1.2"
sharetribe-flex-sdk@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.0.0.tgz#ed3a6199e4755917fcd97355e5d7633a490f4ba6"
sharetribe-flex-sdk@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/sharetribe-flex-sdk/-/sharetribe-flex-sdk-1.1.0.tgz#13cc5120b6c0709b20a2ff275c1496184b2e7255"
dependencies:
axios "^0.18.0"
js-cookie "^2.1.3"