Merge pull request #671 from sharetribe/category-config

Categories in publicData
This commit is contained in:
Kimmo Puputti 2018-01-30 10:00:44 +02:00 committed by GitHub
commit 071b46c540
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 251 additions and 348 deletions

View file

@ -6,6 +6,7 @@ import { ensureListing } from '../../util/data';
import { createSlug } from '../../util/urlHelpers';
import { NamedLink } from '../../components';
import { EditListingDescriptionForm } from '../../containers';
import config from '../../config';
import css from './EditListingDescriptionPanel.css';
@ -24,7 +25,7 @@ const EditListingDescriptionPanel = props => {
const classes = classNames(rootClassName || css.root, className);
const currentListing = ensureListing(listing);
const { description, title, customAttributes } = currentListing.attributes;
const { description, title, publicData } = currentListing.attributes;
const listingTitle = title || '';
const listingLink = currentListing.id ? (
<NamedLink name="ListingPage" params={{ id: currentListing.id.uuid, slug: createSlug(title) }}>
@ -48,14 +49,19 @@ const EditListingDescriptionPanel = props => {
<h1 className={css.title}>{panelTitle}</h1>
<EditListingDescriptionForm
className={css.form}
initialValues={{ title, description, ...customAttributes }}
initialValues={{ title, description, category: publicData.category }}
saveActionMsg={submitButtonText}
onSubmit={values => {
const { title, description, category } = values;
const updateValues = {
title,
description,
// Save category also to the deprecated customAttributes
// so it can be used in search.
// TODO: remove when publicData is used in search
customAttributes: { category },
publicData: { category },
};
@ -65,6 +71,7 @@ const EditListingDescriptionPanel = props => {
updated={panelUpdated}
updateError={errors.updateListingError}
updateInProgress={updateInProgress}
categories={config.custom.categories}
/>
</div>
);

View file

@ -191,7 +191,6 @@ EditListingWizard.propTypes = {
// We cannot use propTypes.listing since the listing might be a draft.
listing: shape({
attributes: shape({
customAttributes: object, // structure (key: value) can be defined in management console
publicData: object,
description: string,
geolocation: object,

View file

@ -219,7 +219,6 @@ EditListingWizardTab.propTypes = {
// We cannot use propTypes.listing since the listing might be a draft.
listing: shape({
attributes: shape({
customAttributes: object, // structure (key: value) can be defined in management console
publicData: object,
description: string,
geolocation: object,

View file

@ -1,4 +0,0 @@
@import '../../marketplace.css';
.root {
}

View file

@ -1,68 +0,0 @@
import React from 'react';
import { string } from 'prop-types';
import { intlShape, injectIntl } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import { required } from '../../util/validators';
import { SelectField } from '../../components';
import css from './FieldCustomAttributeSelect.css';
const FieldCustomAttributeSelect = props => {
const { className, rootClassName, id, customAttribute, intl } = props;
// Does custom attribute 'customAttribute' exists in current marketplace configuration
const ca = config.customAttributes && config.customAttributes[customAttribute];
const isSingleChoice = ca && ca.select === 'single' && ca.type === 'string';
// If custom attribute config for given 'customAttribute' doesn't exist, don't print SelectField
if (!isSingleChoice) {
return null;
}
const caLabel = intl.formatMessage({ id: `FieldCustomAttributeSelect.${customAttribute}.label` });
const caPlaceholder = intl.formatMessage({
id: `FieldCustomAttributeSelect.${customAttribute}.placeholder`,
});
const caRequired = required(
intl.formatMessage({
id: `FieldCustomAttributeSelect.${customAttribute}.required`,
})
);
const classes = classNames(rootClassName || css.root, className);
return (
<SelectField
className={classes}
name={customAttribute}
id={id}
label={caLabel}
validate={caRequired}
>
<option value="">{caPlaceholder}</option>
{ca.values.map(c => (
<option key={c} value={c}>
{intl.formatMessage({ id: `FieldCustomAttributeSelect.${customAttribute}.option.${c}` })}
</option>
))}
</SelectField>
);
};
FieldCustomAttributeSelect.defaultProps = {
className: null,
rootClassName: null,
};
FieldCustomAttributeSelect.propTypes = {
className: string,
rootClassName: string,
id: string.isRequired,
customAttribute: string.isRequired,
// From react-intl
intl: intlShape.isRequired,
};
export default injectIntl(FieldCustomAttributeSelect);

View file

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { compose } from 'redux';
import { object, string, bool, number, func, shape, array } from 'prop-types';
import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import { omit } from 'lodash';
@ -8,9 +9,10 @@ import { omit } from 'lodash';
import { SelectSingleFilter } from '../../components';
import routeConfiguration from '../../routeConfiguration';
import { createResourceLocatorString } from '../../util/routes';
import config from '../../config';
import css from './SearchFilters.css';
const CATEGORY_URL_PARAM = 'ca_category';
const SearchFiltersComponent = props => {
const {
rootClassName,
@ -19,7 +21,9 @@ const SearchFiltersComponent = props => {
listingsAreLoaded,
resultsCount,
searchInProgress,
categories,
history,
intl,
} = props;
const loadingResults = <FormattedMessage id="SearchFilters.loadingResults" />;
@ -32,27 +36,27 @@ const SearchFiltersComponent = props => {
const classes = classNames(rootClassName || css.root, className);
const onSelectSingle = (customAttribute, option) => {
// Name of the corresponding query parameter.
// The custom attribute query parameters are named
// ca_<custom_attribute_name> in the API.
const caParam = `ca_${customAttribute}`;
const categoryLabel = intl.formatMessage({
id: 'SearchFilters.categoryLabel',
});
const onSelectOption = (urlParam, option) => {
// query parameters after selecting the option
// if no option is passed, clear the selection for the filter
const queryParams = option
? { ...urlQueryParams, [caParam]: option }
: omit(urlQueryParams, caParam);
? { ...urlQueryParams, [urlParam]: option }
: omit(urlQueryParams, urlParam);
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, queryParams));
};
const hasCategoryConfig = config.customAttributes && config.customAttributes.category;
const categoryFilter = hasCategoryConfig ? (
const categoryFilter = categories ? (
<SelectSingleFilter
customAttribute="category"
urlQueryParams={urlQueryParams}
onSelect={onSelectSingle}
urlParam={CATEGORY_URL_PARAM}
paramLabel={categoryLabel}
onSelect={onSelectOption}
options={categories}
/>
) : null;
return (
@ -68,13 +72,12 @@ const SearchFiltersComponent = props => {
);
};
const { object, string, bool, number, func, shape } = PropTypes;
SearchFiltersComponent.defaultProps = {
rootClassName: null,
className: null,
resultsCount: null,
searchingInProgress: false,
categories: null,
};
SearchFiltersComponent.propTypes = {
@ -86,13 +89,17 @@ SearchFiltersComponent.propTypes = {
searchingInProgress: bool,
onMapIconClick: func.isRequired,
onManageDisableScrolling: func.isRequired,
categories: array,
// from withRouter
history: shape({
push: func.isRequired,
}).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const SearchFilters = withRouter(SearchFiltersComponent);
const SearchFilters = compose(withRouter, injectIntl)(SearchFiltersComponent);
export default SearchFilters;

View file

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { object, string, bool, number, func, shape, array } from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { withRouter } from 'react-router-dom';
@ -8,18 +8,16 @@ import { omit, toPairs } from 'lodash';
import routeConfiguration from '../../routeConfiguration';
import { createResourceLocatorString } from '../../util/routes';
import { SecondaryButton, ModalInMobile, Button, SelectSingleFilterMobile } from '../../components';
import config from '../../config';
import css from './SearchFiltersMobile.css';
// prefix for all custom attribute query params
const CA_PREFIX = 'ca_';
const CATEGORY_URL_PARAM = 'ca_category';
const validateParamValue = value => value !== null && value !== undefined && value.length > 0;
// Check if a filter parameter is included query parameters
const hasFilterQueryParams = queryParams => {
const firstFilterParam = toPairs(queryParams).find(entry => {
return !!(entry[0].startsWith(CA_PREFIX) && validateParamValue(entry[1]));
return validateParamValue(entry[1]);
});
return !!firstFilterParam;
};
@ -29,9 +27,6 @@ class SearchFiltersMobileComponent extends Component {
super(props);
this.state = { isFiltersOpenOnMobile: false };
//TODO: take as props
this.customAttribute = 'category';
this.openFilters = this.openFilters.bind(this);
this.cancelFilters = this.cancelFilters.bind(this);
this.closeFilters = this.closeFilters.bind(this);
@ -68,19 +63,14 @@ class SearchFiltersMobileComponent extends Component {
this.setState({ isFiltersOpenOnMobile: false });
}
onSelectSingle(customAttribute, option) {
onSelectSingle(urlParam, option) {
const { urlQueryParams, history } = this.props;
// Name of the corresponding query parameter.
// The custom attribute query parameters are named
// ca_<custom_attribute_name> in the API.
const caParam = `${CA_PREFIX}${customAttribute}`;
// query parameters after selecting the option
// if no option is passed, clear the selection for the filter
const queryParams = option
? { ...urlQueryParams, [caParam]: option }
: omit(urlQueryParams, caParam);
? { ...urlQueryParams, [urlParam]: option }
: omit(urlQueryParams, urlParam);
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, queryParams));
}
@ -89,8 +79,7 @@ class SearchFiltersMobileComponent extends Component {
resetAll(e) {
const { urlQueryParams, history } = this.props;
const caParam = `${CA_PREFIX}${this.customAttribute}`;
const queryParams = omit(urlQueryParams, caParam);
const queryParams = omit(urlQueryParams, CATEGORY_URL_PARAM);
history.push(createResourceLocatorString('SearchPage', routeConfiguration(), {}, queryParams));
// blur event target if event is passed
@ -110,6 +99,7 @@ class SearchFiltersMobileComponent extends Component {
showAsModalMaxWidth,
onMapIconClick,
onManageDisableScrolling,
categories,
intl,
} = this.props;
@ -138,12 +128,16 @@ class SearchFiltersMobileComponent extends Component {
</SecondaryButton>
);
const hasCategoryConfig = config.customAttributes && config.customAttributes.category;
const categoryFilter = hasCategoryConfig ? (
const categoryLabel = intl.formatMessage({
id: 'SearchFiltersMobile.categoryLabel',
});
const categoryFilter = categories ? (
<SelectSingleFilterMobile
customAttribute={this.customAttribute}
urlQueryParams={urlQueryParams}
urlParam={CATEGORY_URL_PARAM}
paramLabel={categoryLabel}
onSelect={this.onSelectSingle}
options={categories}
intl={intl}
/>
) : null;
@ -188,8 +182,6 @@ class SearchFiltersMobileComponent extends Component {
}
}
const { object, string, bool, number, func, shape } = PropTypes;
SearchFiltersMobileComponent.defaultProps = {
rootClassName: null,
className: null,
@ -211,6 +203,7 @@ SearchFiltersMobileComponent.propTypes = {
onManageDisableScrolling: func.isRequired,
onOpenModal: func,
onCloseModal: func,
categories: array,
// from injectIntl
intl: intlShape.isRequired,

View file

@ -1,13 +1,17 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { object, string, func, arrayOf, shape } from 'prop-types';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import config from '../../config';
import { Menu, MenuContent, MenuItem, MenuLabel } from '../../components';
import css from './SelectSingleFilter.css';
class SelectSingleFilterComponent extends Component {
const optionLabel = (options, key) => {
const option = options.find(o => o.key === key);
return option ? option.label : key;
};
class SelectSingleFilter extends Component {
constructor(props) {
super(props);
@ -20,25 +24,19 @@ class SelectSingleFilterComponent extends Component {
this.setState({ isOpen: isOpen });
}
selectOption(customAttribute, option) {
selectOption(urlParam, option) {
this.setState({ isOpen: false });
this.props.onSelect(customAttribute, option);
this.props.onSelect(urlParam, option);
}
render() {
const { rootClassName, className, customAttribute, urlQueryParams, intl } = this.props;
const { rootClassName, className, urlQueryParams, urlParam, paramLabel, options } = this.props;
// custom attribute content
const ca = customAttribute && config.customAttributes[customAttribute];
// name of the corresponding query parameter
const caParam = `ca_${customAttribute}`;
// current value of this custom attribute filter
const currentValue = urlQueryParams[caParam];
const currentValue = urlQueryParams[urlParam];
// resolve menu label text and class
const menuLabel = currentValue
? intl.formatMessage({ id: `SelectSingleFilter.category.option.${currentValue}` })
: intl.formatMessage({ id: `SelectSingleFilter.${customAttribute}.label` });
const menuLabel = currentValue ? optionLabel(options, currentValue) : paramLabel;
const menuLabelClass = currentValue ? css.menuLabelSelected : css.menuLabel;
const classes = classNames(rootClassName || css.root, className);
@ -53,29 +51,26 @@ class SelectSingleFilterComponent extends Component {
>
<MenuLabel className={menuLabelClass}>{menuLabel}</MenuLabel>
<MenuContent className={css.menuContent}>
{ca.values.map(v => {
{options.map(option => {
// check if this option is selected
const selected = currentValue === v;
const selected = currentValue === option.key;
// menu item border class
const menuItemBorderClass = selected ? css.menuItemBorderSelected : css.menuItemBorder;
return (
<MenuItem key={v}>
<MenuItem key={option.key}>
<button
className={css.menuItem}
onClick={() => this.selectOption(customAttribute, v)}
onClick={() => this.selectOption(urlParam, option.key)}
>
<span className={menuItemBorderClass} />
<FormattedMessage id={`SelectSingleFilter.category.option.${v}`} />
{option.label}
</button>
</MenuItem>
);
})}
<MenuItem key={'clearLink'}>
<button
className={css.clearMenuItem}
onClick={() => this.selectOption(customAttribute, null)}
>
<button className={css.clearMenuItem} onClick={() => this.selectOption(urlParam, null)}>
<FormattedMessage id={'SelectSingleFilter.clear'} />
</button>
</MenuItem>
@ -85,24 +80,25 @@ class SelectSingleFilterComponent extends Component {
}
}
const { object, string, func } = PropTypes;
SelectSingleFilterComponent.defaultProps = {
SelectSingleFilter.defaultProps = {
rootClassName: null,
className: null,
};
SelectSingleFilterComponent.propTypes = {
SelectSingleFilter.propTypes = {
rootClassName: string,
className: string,
customAttribute: string.isRequired,
urlQueryParams: object.isRequired,
urlParam: string.isRequired,
paramLabel: string.isRequired,
onSelect: func.isRequired,
// from injectIntl
intl: intlShape.isRequired,
options: arrayOf(
shape({
key: string.isRequired,
label: string.isRequired,
})
).isRequired,
};
const SelectSingleFilter = injectIntl(SelectSingleFilterComponent);
export default SelectSingleFilter;

View file

@ -1,12 +1,11 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { object, string, func, arrayOf, shape } from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import config from '../../config';
import css from './SelectSingleFilterMobile.css';
class SelectSingleFilterMobileComponent extends Component {
class SelectSingleFilterMobile extends Component {
constructor(props) {
super(props);
this.state = { isOpen: true };
@ -15,8 +14,8 @@ class SelectSingleFilterMobileComponent extends Component {
}
selectOption(option, e) {
const { customAttribute, onSelect } = this.props;
onSelect(customAttribute, option);
const { urlParam, onSelect } = this.props;
onSelect(urlParam, option);
// blur event target if event is passed
if (e && e.currentTarget) {
@ -29,17 +28,10 @@ class SelectSingleFilterMobileComponent extends Component {
}
render() {
const { rootClassName, className, customAttribute, urlQueryParams, intl } = this.props;
const { rootClassName, className, urlQueryParams, urlParam, paramLabel, options } = this.props;
const filterLabel = intl.formatMessage({
id: `SelectSingleFilterMobile.${customAttribute}.label`,
});
// custom attribute content
const ca = customAttribute && config.customAttributes[customAttribute];
// name of the corresponding query parameter
const caParam = `ca_${customAttribute}`;
// current value of this custom attribute filter
const currentValue = urlQueryParams[caParam];
const currentValue = urlQueryParams[urlParam];
const labelClass = currentValue ? css.filterLabelSelected : css.filterLabel;
@ -53,22 +45,26 @@ class SelectSingleFilterMobileComponent extends Component {
<div className={classes}>
<div className={labelClass}>
<button className={css.labelButton} onClick={this.toggleIsOpen}>
<span className={labelClass}>{filterLabel}</span>
<span className={labelClass}>{paramLabel}</span>
</button>
<button className={css.clearButton} onClick={e => this.selectOption(null, e)}>
<FormattedMessage id={'SelectSingleFilterMobile.clear'} />
</button>
</div>
<div className={optionsContainerClass}>
{ca.values.map(v => {
{options.map(option => {
// check if this option is selected
const selected = currentValue === v;
const selected = currentValue === option.key;
// menu item border class
const optionBorderClass = selected ? css.optionBorderSelected : css.optionBorder;
return (
<button key={v} className={css.option} onClick={() => this.selectOption(v)}>
<button
key={option.key}
className={css.option}
onClick={() => this.selectOption(option.key)}
>
<span className={optionBorderClass} />
<FormattedMessage id={`SelectSingleFilterMobile.category.option.${v}`} />
{option.label}
</button>
);
})}
@ -78,24 +74,25 @@ class SelectSingleFilterMobileComponent extends Component {
}
}
const { object, string, func } = PropTypes;
SelectSingleFilterMobileComponent.defaultProps = {
SelectSingleFilterMobile.defaultProps = {
rootClassName: null,
className: null,
};
SelectSingleFilterMobileComponent.propTypes = {
SelectSingleFilterMobile.propTypes = {
rootClassName: string,
className: string,
customAttribute: string.isRequired,
urlQueryParams: object.isRequired,
urlParam: string.isRequired,
paramLabel: string.isRequired,
onSelect: func.isRequired,
// from injectIntl
intl: intlShape.isRequired,
options: arrayOf(
shape({
key: string.isRequired,
label: string.isRequired,
})
).isRequired,
};
const SelectSingleFilterMobile = injectIntl(SelectSingleFilterMobileComponent);
export default SelectSingleFilterMobile;

View file

@ -55,7 +55,6 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -168,7 +167,6 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -300,7 +298,6 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -429,7 +426,6 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -558,7 +554,6 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -805,7 +800,6 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -915,7 +909,6 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -1028,7 +1021,6 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -1160,7 +1152,6 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -1289,7 +1280,6 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -1418,7 +1408,6 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -1665,7 +1654,6 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -1775,7 +1763,6 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -1888,7 +1875,6 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -2020,7 +2006,6 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -2149,7 +2134,6 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -2278,7 +2262,6 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -2525,7 +2508,6 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -2635,7 +2617,6 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -2748,7 +2729,6 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -2880,7 +2860,6 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3009,7 +2988,6 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3138,7 +3116,6 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3385,7 +3362,6 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3495,7 +3471,6 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3608,7 +3583,6 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3740,7 +3714,6 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3869,7 +3842,6 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -3998,7 +3970,6 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -4245,7 +4216,6 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -4355,7 +4325,6 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -4468,7 +4437,6 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -4600,7 +4568,6 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -4729,7 +4696,6 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -4858,7 +4824,6 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -5105,7 +5070,6 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -5215,7 +5179,6 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -5328,7 +5291,6 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -5460,7 +5422,6 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -5589,7 +5550,6 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -5718,7 +5678,6 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -5965,7 +5924,6 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6069,7 +6027,6 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6182,7 +6139,6 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6301,7 +6257,6 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6418,7 +6373,6 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6535,7 +6489,6 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6770,7 +6723,6 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6861,7 +6813,6 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -6974,7 +6925,6 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -7093,7 +7043,6 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -7210,7 +7159,6 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -7327,7 +7275,6 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -7562,7 +7509,6 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -7653,7 +7599,6 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -7766,7 +7711,6 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -7885,7 +7829,6 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8002,7 +7945,6 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8119,7 +8061,6 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8354,7 +8295,6 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8445,7 +8385,6 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8558,7 +8497,6 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8677,7 +8615,6 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8794,7 +8731,6 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -8911,7 +8847,6 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -9146,7 +9081,6 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -9237,7 +9171,6 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -9350,7 +9283,6 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -9469,7 +9401,6 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -9586,7 +9517,6 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -9703,7 +9633,6 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -9938,7 +9867,6 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10029,7 +9957,6 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10142,7 +10069,6 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10261,7 +10187,6 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10378,7 +10303,6 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10495,7 +10419,6 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10730,7 +10653,6 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10821,7 +10743,6 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
currentListing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -10934,7 +10855,6 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -11053,7 +10973,6 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -11170,7 +11089,6 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -11287,7 +11205,6 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -11522,7 +11439,6 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {

View file

@ -31,9 +31,6 @@ export { default as ExpandingTextarea } from './ExpandingTextarea/ExpandingTexta
export { default as ExternalLink } from './ExternalLink/ExternalLink';
export { default as FilterPanel } from './FilterPanel/FilterPanel';
export { default as FieldCheckbox } from './FieldCheckbox/FieldCheckbox';
export {
default as FieldCustomAttributeSelect,
} from './FieldCustomAttributeSelect/FieldCustomAttributeSelect';
export { default as FieldGroupCheckbox } from './FieldGroupCheckbox/FieldGroupCheckbox';
export { default as FieldReviewRating } from './FieldReviewRating/FieldReviewRating';
export { default as Footer } from './Footer/Footer';

View file

@ -211,41 +211,6 @@ const stripeSupportedCountries = [
},
];
// Custom attributes are marketplace specific listing data (e.g. listing could have a category).
// Custom attributes can be defined through management console and code related to custom
// attributes should be changed accordingly.
//
// Here's an example what custom attributes might look like for bicycle listings. This code
// assumes that a custom attribute, called 'category', is created through management console
// with 4 possible values: 'road', 'mountain', 'track', and 'other'.
//
// When listing information is queried customAttributes is returned among other attributes:
// {
// id: 1,
// type: 'listing',
// attributes: {
// title: 'sauna',
// // and description, price, etc.
// customAttributes: {
// category: "mountain",
// // and other added custom attributes as "key: value" pairs
// },
// },
// }
const exampleCustomAttributes = {
category: {
select: 'single', // possible values: 'single' (only type supported atm.)
type: 'string',
values: ['road', 'mountain', 'track', 'other'],
},
};
// To use the example custom attributes, set the
// REACT_APP_USE_EXAMPLE_CUSTOM_ATTRIBUTES variable to `true` in the
// gitignored `.env.development.local` file
const useExampleCustomAttributes = process.env.REACT_APP_USE_EXAMPLE_CUSTOM_ATTRIBUTES === 'true';
const customAttributes = useExampleCustomAttributes ? exampleCustomAttributes : {};
// Address information is used in SEO schema for Organization (http://schema.org/PostalAddress)
const addressCountry = 'FI';
const addressRegion = 'Helsinki';
@ -284,7 +249,6 @@ const config = {
sdk: { clientId: sdkClientId, baseUrl: sdkBaseUrl },
currency,
currencyConfig,
customAttributes,
stripe: { publishableKey: stripePublishableKey, supportedCountries: stripeSupportedCountries },
canonicalRootURL,
address: {

View file

@ -0,0 +1,38 @@
import React from 'react';
import { required } from '../../util/validators';
import { SelectField } from '../../components';
import css from './EditListingDescriptionForm.css';
const CustomCategorySelectFieldMaybe = props => {
const { name, id, categories, intl } = props;
const categoryLabel = intl.formatMessage({
id: 'EditListingDescriptionForm.categoryLabel',
});
const categoryPlaceholder = intl.formatMessage({
id: 'EditListingDescriptionForm.categoryPlaceholder',
});
const categoryRequired = required(
intl.formatMessage({
id: 'EditListingDescriptionForm.categoryRequired',
})
);
return categories ? (
<SelectField
className={css.category}
name={name}
id={id}
label={categoryLabel}
validate={categoryRequired}
>
<option value="">{categoryPlaceholder}</option>
{categories.map(c => (
<option key={c.key} value={c.key}>
{c.label}
</option>
))}
</SelectField>
) : null;
};
export default CustomCategorySelectFieldMaybe;

View file

@ -1,12 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { bool, func, string, arrayOf, shape } from 'prop-types';
import { compose } from 'redux';
import { reduxForm, propTypes as formPropTypes } from 'redux-form';
import { intlShape, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { propTypes } from '../../util/types';
import { maxLength, required } from '../../util/validators';
import { FieldCustomAttributeSelect, Form, Button, TextInputField } from '../../components';
import { Form, Button, TextInputField } from '../../components';
import CustomCategorySelectFieldMaybe from './CustomCategorySelectFieldMaybe';
import css from './EditListingDescriptionForm.css';
@ -25,6 +26,7 @@ const EditListingDescriptionFormComponent = props => {
updated,
updateError,
updateInProgress,
categories,
} = props;
const titleMessage = intl.formatMessage({ id: 'EditListingDescriptionForm.title' });
@ -86,10 +88,11 @@ const EditListingDescriptionFormComponent = props => {
validate={[required(descriptionRequiredMessage)]}
/>
<FieldCustomAttributeSelect
className={css.category}
<CustomCategorySelectFieldMaybe
name="category"
id={`${form}.category`}
customAttribute="category"
categories={categories}
intl={intl}
/>
<Button
@ -107,8 +110,6 @@ const EditListingDescriptionFormComponent = props => {
EditListingDescriptionFormComponent.defaultProps = { className: null, updateError: null };
const { bool, func, string } = PropTypes;
EditListingDescriptionFormComponent.propTypes = {
...formPropTypes,
className: string,
@ -118,6 +119,12 @@ EditListingDescriptionFormComponent.propTypes = {
updated: bool.isRequired,
updateError: propTypes.error,
updateInProgress: bool.isRequired,
categories: arrayOf(
shape({
key: string.isRequired,
label: string.isRequired,
})
),
};
const formName = 'EditListingDescriptionForm';

View file

@ -16,6 +16,7 @@ describe('EditListingDescriptionForm', () => {
saveActionMsg="Save description"
updated={false}
updateInProgress={false}
categories={[{ key: 'cat1', label: 'Cat 1' }, { key: 'cat2', label: 'Cat 2' }]}
/>
);
expect(tree).toMatchSnapshot();

View file

@ -54,6 +54,42 @@ exports[`EditListingDescriptionForm matches snapshot 1`] = `
value=""
/>
</div>
<div
className=""
>
<label
htmlFor="fakeTestForm.category"
>
EditListingDescriptionForm.categoryLabel
</label>
<select
className=""
id="fakeTestForm.category"
name="category"
onBlur={[Function]}
onChange={[Function]}
onDragStart={[Function]}
onDrop={[Function]}
onFocus={[Function]}
value=""
>
<option
value=""
>
EditListingDescriptionForm.categoryPlaceholder
</option>
<option
value="cat1"
>
Cat 1
</option>
<option
value="cat2"
>
Cat 2
</option>
</select>
</div>
<button
className=""
disabled={true}

View file

@ -146,6 +146,11 @@ const gotoListingTab = (history, listing) => {
);
};
const categoryLabel = (categories, key) => {
const cat = categories.find(c => c.key === key);
return cat ? cat.label : key;
};
// TODO: price unit (per x), custom fields, contact, reviews
// N.B. All the presentational content needs to be extracted to their own components
export class ListingPageComponent extends Component {
@ -262,13 +267,10 @@ export class ListingPageComponent extends Component {
publicData,
} = currentListing.attributes;
const { customAttributes } = currentListing.attributes;
const hasCategoryConfig = config.customAttributes && config.customAttributes.category;
const hasCategoryAttribute = customAttributes && customAttributes.category;
const category =
hasCategoryConfig && hasCategoryAttribute ? (
publicData && publicData.category ? (
<span>
<FormattedMessage id={`ListingPage.category.${customAttributes.category}`} />
{categoryLabel(config.custom.categories, publicData.category)}
<span className={css.separator}></span>
</span>
) : null;

View file

@ -54,7 +54,6 @@ exports[`ListingPage matches snapshot 1`] = `
listing={
Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {

View file

@ -42,10 +42,12 @@ const MODAL_BREAKPOINT = 768; // Search is in modal on mobile layout
const SEARCH_WITH_MAP_DEBOUNCE = 300; // Little bit of debounce before search is initiated.
const BOUNDS_FIXED_PRECISION = 8;
const CATEGORY_URL_PARAM = 'ca_category';
// extract search parameters, including a custom attribute named category
const pickSearchParamsOnly = params => {
const { address, origin, bounds, ca_category } = params || {};
return { address, origin, bounds, ca_category };
const { address, origin, bounds, ...rest } = params || {};
return { address, origin, bounds, [CATEGORY_URL_PARAM]: rest[CATEGORY_URL_PARAM] };
};
export class SearchPageComponent extends Component {
@ -185,6 +187,7 @@ export class SearchPageComponent extends Component {
searchInProgress,
searchListingsError,
searchParams,
categories,
} = this.props;
// eslint-disable-next-line no-unused-vars
const { mapSearch, page, ...searchInURL } = parse(location.search, {
@ -301,6 +304,7 @@ export class SearchPageComponent extends Component {
searchListingsError={searchListingsError}
onMapIconClick={onMapIconClick}
onManageDisableScrolling={onManageDisableScrolling}
categories={categories}
/>
<SearchFiltersMobile
className={css.searchFiltersMobile}
@ -314,6 +318,7 @@ export class SearchPageComponent extends Component {
onManageDisableScrolling={onManageDisableScrolling}
onOpenModal={this.onOpenMobileModal}
onCloseModal={this.onCloseMobileModal}
categories={categories}
/>
<div
className={classNames(css.listings, {
@ -354,6 +359,7 @@ SearchPageComponent.defaultProps = {
searchListingsError: null,
searchParams: {},
tab: 'listings',
categories: config.custom.categories,
};
const { array, bool, func, oneOf, object, shape, string } = PropTypes;
@ -369,6 +375,7 @@ SearchPageComponent.propTypes = {
searchListingsError: propTypes.error,
searchParams: object,
tab: oneOf(['filters', 'listings', 'map']).isRequired,
categories: array,
// from withRouter
history: shape({
@ -429,9 +436,9 @@ SearchPage.loadData = (params, search) => {
latlng: ['origin'],
latlngBounds: ['bounds'],
});
const page = queryParams.page || 1;
const { page = 1, ...rest } = queryParams;
return searchListings({
...queryParams,
...rest,
page,
perPage: RESULT_PAGE_SIZE,
include: ['author', 'images'],

View file

@ -41,6 +41,7 @@ describe('SearchPageComponent', () => {
onSearchMapListings: noop,
sendVerificationEmailInProgress: false,
onResendVerificationEmail: noop,
categories: [{ key: 'cat1', label: 'Cat 1' }, { key: 'cat2', label: 'Cat 2' }],
};
const tree = renderShallow(<SearchPageComponent {...props} />);
expect(tree).toMatchSnapshot();

View file

@ -20,7 +20,19 @@ exports[`SearchPageComponent matches snapshot 1`] = `
<withRouter(Connect(TopbarContainerComponent)) />
<div>
<div>
<withRouter(SearchFiltersComponent)
<withRouter(InjectIntl(SearchFiltersComponent))
categories={
Array [
Object {
"key": "cat1",
"label": "Cat 1",
},
Object {
"key": "cat2",
"label": "Cat 2",
},
]
}
listingsAreLoaded={true}
onManageDisableScrolling={[Function]}
onMapIconClick={[Function]}
@ -37,6 +49,18 @@ exports[`SearchPageComponent matches snapshot 1`] = `
}
/>
<InjectIntl(withRouter(SearchFiltersMobileComponent))
categories={
Array [
Object {
"key": "cat1",
"label": "Cat 1",
},
Object {
"key": "cat2",
"label": "Cat 2",
},
]
}
listingsAreLoaded={true}
onCloseModal={[Function]}
onManageDisableScrolling={[Function]}

View file

@ -148,7 +148,6 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {
@ -346,7 +345,6 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
},
"listing": Object {
"attributes": Object {
"customAttributes": Object {},
"deleted": false,
"description": "listing1 description",
"geolocation": LatLng {

View file

@ -36,3 +36,10 @@ export const amenities = [
label: 'Own food allowed',
},
];
export const categories = [
{ key: 'road', label: 'Road' },
{ key: 'mountain', label: 'Mountain' },
{ key: 'track', label: 'Track' },
{ key: 'other', label: 'Other' },
];

View file

@ -109,6 +109,9 @@
"DateRangeInputField.invalidStartDate": "The start date is not valid",
"DateRangeInputField.screenReaderInputMessage": "Date input",
"DateRangeInputField.startDatePlaceholderText": "Add start date",
"EditListingDescriptionForm.categoryLabel": "Sauna type",
"EditListingDescriptionForm.categoryPlaceholder": "Choose the type of your sauna…",
"EditListingDescriptionForm.categoryRequired": "You need to select a category for your sauna.",
"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.",
@ -191,13 +194,6 @@
"EnquiryForm.messageRequired": "A message is required",
"EnquiryForm.sendEnquiryError": "Whoops, something went wrong. Please try again.",
"EnquiryForm.submitButtonText": "Send enquiry",
"FieldCustomAttributeSelect.category.label": "Sauna type",
"FieldCustomAttributeSelect.category.option.mountain": "Mountain",
"FieldCustomAttributeSelect.category.option.other": "Other",
"FieldCustomAttributeSelect.category.option.road": "Road",
"FieldCustomAttributeSelect.category.option.track": "Track",
"FieldCustomAttributeSelect.category.placeholder": "Choose the type of your sauna…",
"FieldCustomAttributeSelect.category.required": "You need to select a category for your sauna.",
"FieldReviewRating.star1": "Bad experience - 1 star",
"FieldReviewRating.star2": "Not so nice - 2 stars",
"FieldReviewRating.star3": "OK - 3 stars",
@ -255,10 +251,6 @@
"ListingPage.bookingHelp": "Start by choosing your dates.",
"ListingPage.bookingHelpClosedListing": "Sorry, this listing has been closed.",
"ListingPage.bookingTitle": "Book {title}",
"ListingPage.category.mountain": "Mountain",
"ListingPage.category.other": "Other",
"ListingPage.category.road": "Road",
"ListingPage.category.track": "Track",
"ListingPage.closedListing": "This listing has been closed and can't be booked.",
"ListingPage.closedListingButtonText": "Sorry, this listing has been closed.",
"ListingPage.contactUser": "Contact",
@ -477,6 +469,7 @@
"ReviewModal.description": "Reviews are an important part of the Saunatime community. Please share what went well and what could have been improved.",
"ReviewModal.later": "Later",
"ReviewModal.title": "Leave a review for {revieweeName}",
"SearchFilters.categoryLabel": "Category",
"SearchFilters.filtersButtonLabel": "Filters",
"SearchFilters.foundResults": "{count, number} {count, plural, one {result} other {results}}",
"SearchFilters.loadingResults": "Loading search results…",
@ -485,6 +478,7 @@
"SearchFilters.noResultsMobile": "No results.",
"SearchFilters.openMapView": "Map",
"SearchFiltersMobile.cancel": "CANCEL",
"SearchFiltersMobile.categoryLabel": "Category",
"SearchFiltersMobile.heading": "Filter saunas",
"SearchFiltersMobile.resetAll": "Reset all",
"SearchFiltersMobile.showListings": "Show {count, number} {count, plural, one {sauna} other {saunas}}",
@ -510,17 +504,7 @@
"SectionLocations.listingsInLocation": "Saunas in {location}",
"SectionLocations.subtitle": "We have more than 1000 saunas around Finland. Here are some of our most popular locations.",
"SectionLocations.title": "We have wooden saunas, electric saunas, and even tent saunas.",
"SelectSingleFilter.category.label": "Category",
"SelectSingleFilter.category.option.mountain": "Mountain",
"SelectSingleFilter.category.option.other": "Other",
"SelectSingleFilter.category.option.road": "Road",
"SelectSingleFilter.category.option.track": "Track",
"SelectSingleFilter.clear": "Clear",
"SelectSingleFilterMobile.category.label": "Category",
"SelectSingleFilterMobile.category.option.mountain": "Mountain",
"SelectSingleFilterMobile.category.option.other": "Other",
"SelectSingleFilterMobile.category.option.road": "Road",
"SelectSingleFilterMobile.category.option.track": "Track",
"SelectSingleFilterMobile.clear": "Clear",
"SendMessageForm.sendFailed": "Failed to send. Please try again.",
"SendMessageForm.sendMessage": "Send message",

View file

@ -165,7 +165,7 @@ export const ensureListing = listing => {
const empty = {
id: null,
type: 'listing',
attributes: { customAttributes: {}, publicData: {} },
attributes: { publicData: {} },
images: [],
};
return { ...empty, ...listing };
@ -180,7 +180,7 @@ export const ensureOwnListing = listing => {
const empty = {
id: null,
type: 'ownListing',
attributes: { customAttributes: {}, publicData: {} },
attributes: { publicData: {} },
images: [],
};
return { ...empty, ...listing };

View file

@ -86,7 +86,6 @@ export const createListing = (id, attributes = {}, includes = {}) => ({
deleted: false,
state: LISTING_STATE_PUBLISHED,
price: new Money(5500, 'USD'),
customAttributes: {},
publicData: {},
...attributes,
},
@ -104,7 +103,6 @@ export const createOwnListing = (id, attributes = {}, includes = {}) => ({
deleted: false,
state: LISTING_STATE_PUBLISHED,
price: new Money(5500, 'USD'),
customAttributes: {},
publicData: {},
...attributes,
},

View file

@ -142,7 +142,6 @@ const listingAttributes = shape({
deleted: propTypes.value(false).isRequired,
state: oneOf(LISTING_STATES).isRequired,
price: propTypes.money,
customAttributes: object,
publicData: object.isRequired,
});
@ -153,7 +152,6 @@ const ownListingAttributes = shape({
deleted: propTypes.value(false).isRequired,
state: oneOf(LISTING_STATES).isRequired,
price: propTypes.money,
customAttributes: object,
publicData: object.isRequired,
});