mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
commit
ac287cfffb
38 changed files with 927 additions and 553 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
70
src/components/ManageListingCard/Overlay.css
Normal file
70
src/components/ManageListingCard/Overlay.css
Normal 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);
|
||||
}
|
||||
46
src/components/ManageListingCard/Overlay.js
Normal file
46
src/components/ManageListingCard/Overlay.js
Normal 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;
|
||||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ describe('EditListingPageComponent', () => {
|
|||
onManageDisableScrolling={noop}
|
||||
onCreateListing={noop}
|
||||
onCreateListingDraft={noop}
|
||||
onUpdateListingDraft={noop}
|
||||
onPublishListingDraft={noop}
|
||||
onUpdateListing={noop}
|
||||
onImageUpload={noop}
|
||||
onRemoveListingImage={noop}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -147,7 +147,7 @@ exports[`ListingPage matches snapshot 1`] = `
|
|||
}
|
||||
showContactUser={true}
|
||||
/>
|
||||
<SectionDescription
|
||||
<SectionDescriptionMaybe
|
||||
description="listing1 description"
|
||||
/>
|
||||
<SectionFeatures
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue