mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Add documentation for extending listing data model
This commit is contained in:
parent
e2fd352dcf
commit
aa76bc7983
4 changed files with 481 additions and 6 deletions
BIN
docs/assets/extend-listing/capacity_listing_page.png
Normal file
BIN
docs/assets/extend-listing/capacity_listing_page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/assets/extend-listing/edit_capacity.png
Normal file
BIN
docs/assets/extend-listing/edit_capacity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
472
docs/extend-listing.md
Normal file
472
docs/extend-listing.md
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
# Extend listing data
|
||||
|
||||
This guide walks you the different steps required to expand the listing datamodel in your
|
||||
marketplace. We'll have a look on how the data is added, how it can be presented and finally how it
|
||||
can be used to filter searches.
|
||||
|
||||
Adding new attributes to the data model on a Flex concept called [extended data](./extended-data.md).
|
||||
|
||||
Three main areas in extending the listing data model are:
|
||||
|
||||
- [Declare the attribute and it's possible values](#declare-the-attribute-and-it's-possible-values)
|
||||
- [Edit the listing wizard](#edit-the-listing-wizard)
|
||||
- [Show the attribute on listing page](#show-the-attribute-on-listing-page)
|
||||
- [Use the attribute as a search filter](#use-the-attribute-as-a-search-filter)
|
||||
|
||||
In this guide we will extend the listing data model by adding a _capacity_ attribute and using it as
|
||||
a search filter.
|
||||
|
||||
## Add a new attribute
|
||||
|
||||
### Declare the attribute and it's possible values
|
||||
|
||||
So, you have come up with a great new extension to the listing data model, fantastic! Depending of
|
||||
the type of data the new attribute is going to be used for it might make sense to define and store the
|
||||
possible values for the attribute. In case the data will be free form text there might not be use
|
||||
for this but in our case we wan't to fix the possible values that can be used for the capacity. Also
|
||||
if the attribute will be used as a search filter it helps to keep track of the possible values
|
||||
somewhere.
|
||||
|
||||
A commonly used way of storing the possible values for a listing attribute is to declare them in the
|
||||
[marketplace-custom-config.js](../src/marketplace-custom-config.js) file. Place the values in an
|
||||
array and export the variable containing the array. The exports from that file are included in the
|
||||
`config` and are available as properties in `config.custom`. Search filters and some components used
|
||||
to edit and present the data rely on a data model of an object that contains `key` and `label`
|
||||
properties.
|
||||
|
||||
For the capacity attribute we'll define a set of values that describe the capacity in a few ranges
|
||||
of how many people can fit into a given sauna:
|
||||
|
||||
```js
|
||||
export const capacityOptions = [
|
||||
{ key: '1to3', label: '1 to 3' },
|
||||
{ key: '4to6', label: '4 to 6' },
|
||||
{ key: '7to9', label: '7 to 9' },
|
||||
{ key: '10plus', label: '10 plus' },
|
||||
];
|
||||
```
|
||||
|
||||
### Edit the listing wizard
|
||||
|
||||
Next step is to add means for modifying the attribute data in listings. This is achieved by adding
|
||||
proper inputs to the `EditListingWizard`. It could probably make sense to add the input to the
|
||||
_description_ tab or modify the _amenities_ tab to also include capacity but for the sake of clarity
|
||||
let's add a new tab to the wizard. The new tab will be placed between the _amenities_ and _policy_
|
||||
tabs.
|
||||
|
||||
|
||||
First lets declare the tab in `EditListingWizardTab`:
|
||||
|
||||
```js
|
||||
export const DESCRIPTION = 'description';
|
||||
export const FEATURES = 'features';
|
||||
export const CAPACITY = 'capacity';
|
||||
export const POLICY = 'policy';
|
||||
export const LOCATION = 'location';
|
||||
export const PRICING = 'pricing';
|
||||
export const PHOTOS = 'photos';
|
||||
|
||||
// EditListingWizardTab component supports these tabs
|
||||
export const SUPPORTED_TABS = [DESCRIPTION, FEATURES, CAPACITY , POLICY, LOCATION, PRICING, PHOTOS];
|
||||
```
|
||||
|
||||
Now in` EditListingWizard` we can take that tab declaration into use. Add the tab name to the `TABS`
|
||||
array and add a correct label for the tab.
|
||||
|
||||
```js
|
||||
export const TABS = [DESCRIPTION, FEATURES, CAPACITY, POLICY, LOCATION, PRICING, PHOTOS];
|
||||
```
|
||||
|
||||
Also remember to add a label for the tab in the `tabLabel` function:
|
||||
|
||||
```js
|
||||
const tabLabel = (intl, tab) => {
|
||||
let key = null;
|
||||
if (tab === DESCRIPTION) {
|
||||
key = 'EditListingWizard.tabLabelDescription';
|
||||
} else if (tab === FEATURES) {
|
||||
key = 'EditListingWizard.tabLabelFeatures';
|
||||
} else if (tab === CAPACITY) {
|
||||
key = 'EditListingWizard.tabLabelCapacity';
|
||||
} else if (tab === POLICY) {
|
||||
key = 'EditListingWizard.tabLabelPolicy';
|
||||
} else if (tab === LOCATION) {
|
||||
key = 'EditListingWizard.tabLabelLocation';
|
||||
} else if (tab === PRICING) {
|
||||
key = 'EditListingWizard.tabLabelPricing';
|
||||
} else if (tab === PHOTOS) {
|
||||
key = 'EditListingWizard.tabLabelPhotos';
|
||||
}
|
||||
|
||||
return intl.formatMessage({ id: key });
|
||||
};
|
||||
```
|
||||
|
||||
The `tabCompleted` function keeps track of which data the user has already provided in order to
|
||||
tell which tabs are completed. We will be using listing's _public data_ property (see the [extended
|
||||
data](./extended-data.md) documentation for more info on different extended data types) for storing
|
||||
the capacity data so based on that information we can add a check for capacity in `tabCompleted`:
|
||||
|
||||
```js
|
||||
/**
|
||||
* Check if a wizard tab is completed.
|
||||
*
|
||||
* @param tab wizard's tab
|
||||
* @param listing is contains some specific data if tab is completed
|
||||
*
|
||||
* @return true if tab / step is completed.
|
||||
*/
|
||||
const tabCompleted = (tab, listing) => {
|
||||
const { description, geolocation, price, title, publicData } = listing.attributes;
|
||||
const images = listing.images;
|
||||
|
||||
switch (tab) {
|
||||
case DESCRIPTION:
|
||||
return !!(description && title);
|
||||
case FEATURES:
|
||||
return !!(publicData && publicData.amenities);
|
||||
case CAPACITY:
|
||||
return !!(publicData && publicData.capacity);
|
||||
case POLICY:
|
||||
return !!(publicData && typeof publicData.rules !== 'undefined');
|
||||
case LOCATION:
|
||||
return !!(geolocation && publicData && publicData.location && publicData.location.address);
|
||||
case PRICING:
|
||||
return !!price;
|
||||
case PHOTOS:
|
||||
return images && images.length > 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Next task is to add form and panel components that render the capacity tab. As for the form, let's
|
||||
create a `EditListingCapacityForm` component:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
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';
|
||||
import classNames from 'classnames';
|
||||
import { propTypes } from '../../util/types';
|
||||
import { Form, Button, FieldSelect } from '../../components';
|
||||
import { required } from '../../util/validators';
|
||||
|
||||
import css from './EditListingCapacityForm.css';
|
||||
|
||||
export const EditListingCapacityFormComponent = props => (
|
||||
<FinalForm
|
||||
{...props}
|
||||
render={fieldRenderProps => {
|
||||
const {
|
||||
className,
|
||||
disabled,
|
||||
handleSubmit,
|
||||
intl,
|
||||
invalid,
|
||||
pristine,
|
||||
saveActionMsg,
|
||||
updated,
|
||||
updateError,
|
||||
updateInProgress,
|
||||
capacityOptions,
|
||||
} = fieldRenderProps;
|
||||
|
||||
const capacityPlaceholder= intl.formatMessage({
|
||||
id: 'EditListingCapacityForm.capacityPlaceholder',
|
||||
});
|
||||
|
||||
const errorMessage = updateError ? (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="EditListingCapacityForm.updateFailed" />
|
||||
</p>
|
||||
) : null;
|
||||
|
||||
const capacityRequired = required(
|
||||
intl.formatMessage({
|
||||
id: 'EditListingCapacityForm.capacityRequired',
|
||||
})
|
||||
);
|
||||
|
||||
const classes = classNames(css.root, className);
|
||||
const submitReady = updated && pristine;
|
||||
const submitInProgress = updateInProgress;
|
||||
const submitDisabled = invalid || disabled || submitInProgress;
|
||||
|
||||
return (
|
||||
<Form className={classes} onSubmit={handleSubmit}>
|
||||
{errorMessage}
|
||||
|
||||
<FieldSelect
|
||||
className={css.capacity}
|
||||
name="capacity"
|
||||
id="capacity"
|
||||
validate={capacityRequired}
|
||||
>
|
||||
<option value="">{capacityPlaceholder}</option>
|
||||
{capacityOptions.map(c => (
|
||||
<option key={c.key} value={c.key}>
|
||||
{c.label}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
|
||||
<Button
|
||||
className={css.submitButton}
|
||||
type="submit"
|
||||
inProgress={submitInProgress}
|
||||
disabled={submitDisabled}
|
||||
ready={submitReady}
|
||||
>
|
||||
{saveActionMsg}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
EditListingCapacityFormComponent.defaultProps = {
|
||||
selectedPlace: null,
|
||||
updateError: null,
|
||||
};
|
||||
|
||||
EditListingCapacityFormComponent.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onSubmit: func.isRequired,
|
||||
saveActionMsg: string.isRequired,
|
||||
updated: bool.isRequired,
|
||||
updateError: propTypes.error,
|
||||
updateInProgress: bool.isRequired,
|
||||
capacityOptions: arrayOf(
|
||||
shape({
|
||||
key: string.isRequired,
|
||||
label: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
};
|
||||
|
||||
export default compose(injectIntl)(EditListingCapacityFormComponent);
|
||||
```
|
||||
|
||||
The form component receives `capacityOptions` as a prop which are used to populate a `FieldSelect`
|
||||
component for selecting the capacity. The `EditListingCapacityForm` is also added to the
|
||||
`src/containers/index.js` file so that it can easilly be referenced from other components. To use
|
||||
the capacity editing form we'll add a panel components which is then used in `EditListingWizardTab`
|
||||
to render the wizard phase. This component we'll call `EditListingCapacityPanel`
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { ensureOwnListing } from '../../util/data';
|
||||
import { ListingLink } from '../../components';
|
||||
import { EditListingCapacityForm } from '../../containers';
|
||||
import config from '../../config.js';
|
||||
|
||||
import css from './EditListingCapacityPanel.css';
|
||||
|
||||
const EditListingCapacityPanel = props => {
|
||||
const {
|
||||
className,
|
||||
rootClassName,
|
||||
listing,
|
||||
onSubmit,
|
||||
onChange,
|
||||
submitButtonText,
|
||||
panelUpdated,
|
||||
updateInProgress,
|
||||
errors,
|
||||
} = props;
|
||||
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
const currentListing = ensureOwnListing(listing);
|
||||
const { publicData } = currentListing.attributes;
|
||||
|
||||
const panelTitle = currentListing.id ? (
|
||||
<FormattedMessage
|
||||
id="EditListingCapacityPanel.title"
|
||||
values={{ listingTitle: <ListingLink listing={listing} /> }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage id="EditListingCapacityPanel.createListingTitle" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<h1 className={css.title}>{panelTitle}</h1>
|
||||
<EditListingCapacityForm
|
||||
className={css.form}
|
||||
initialValues={{ capacity: publicData.capacity }}
|
||||
onSubmit={values => {
|
||||
const { capacity } = values;
|
||||
const updateValues = {
|
||||
publicData: {
|
||||
capacity,
|
||||
},
|
||||
};
|
||||
onSubmit(updateValues);
|
||||
}}
|
||||
onChange={onChange}
|
||||
saveActionMsg={submitButtonText}
|
||||
updated={panelUpdated}
|
||||
updateError={errors.updateListingError}
|
||||
updateInProgress={updateInProgress}
|
||||
capacityOptions={config.custom.capacityOptions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const { func, object, string, bool } = PropTypes;
|
||||
|
||||
EditListingCapacityPanel.defaultProps = {
|
||||
className: null,
|
||||
rootClassName: null,
|
||||
listing: null,
|
||||
};
|
||||
|
||||
EditListingCapacityPanel.propTypes = {
|
||||
className: string,
|
||||
rootClassName: string,
|
||||
|
||||
// We cannot use propTypes.listing since the listing might be a draft.
|
||||
listing: object,
|
||||
|
||||
onSubmit: func.isRequired,
|
||||
onChange: func.isRequired,
|
||||
submitButtonText: string.isRequired,
|
||||
panelUpdated: bool.isRequired,
|
||||
updateInProgress: bool.isRequired,
|
||||
errors: object.isRequired,
|
||||
};
|
||||
|
||||
export default EditListingCapacityPanel;
|
||||
```
|
||||
|
||||
In the panel component we check for an initial capacity value from the `public_data` property of the
|
||||
listing. In the submit handler the new capacity value is stored in the same property. Respectively
|
||||
`EditListingCapacityPanel` is need to be exported from `src/components/index.js` for easier access
|
||||
from other files.
|
||||
|
||||
Now that we have the panel and the form all ready we can add the panel to the `EditListingWizardTab`
|
||||
component. This is done by adding a new block to the `switch` structure that handles rendering the
|
||||
correct panel:
|
||||
|
||||
```js
|
||||
case CAPACITY: {
|
||||
const submitButtonTranslationKey = isNew
|
||||
? 'EditListingWizard.saveNewCapacity'
|
||||
: 'EditListingWizard.saveEditCapacity';
|
||||
return (
|
||||
<EditListingCapacityPanel
|
||||
{...panelProps(CAPACITY)}
|
||||
submitButtonText={intl.formatMessage({ id: submitButtonTranslationKey })}
|
||||
onSubmit={values => {
|
||||
onCompleteEditListingWizardTab(tab, values);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
There! Now we've extended the listing data model with capacity information.
|
||||
|
||||

|
||||
|
||||
The capacity data can now be added to new and existing listings. Next chapter describes how that
|
||||
data can be presented in the listing page.
|
||||
|
||||
## Show the attribute on listing page
|
||||
|
||||
Next step in adding a new attribute to the listing is to present it in the listing page. On some
|
||||
cases an extension to the listing data model can be used purely to enhance the application logic but
|
||||
in our case with the capacity information we want to show the added attribute in the listing page
|
||||
for potential customers.
|
||||
|
||||
To show the capacity attribute in the listing page, let's create a specific component for it and
|
||||
place it in the `ListingPage` container. Desired outcome could be achieved just by editing the
|
||||
`ListingPage` but extracting the capacity UI parts into a separate component will simplify the
|
||||
`ListingPage` and make possible upstream updates from the Flex web template repo easier as there's
|
||||
less chances for merge conflicts. So, let's create a `SectionCapacity` component in the
|
||||
`src/containers/ListingPage/` directory:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { array, shape, string } from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import css from './ListingPage.css';
|
||||
|
||||
const SectionCapacity = props => {
|
||||
const { publicData, options, } = props;
|
||||
|
||||
const capacity = publicData.capacity;
|
||||
const capacityOption = options.find(option => option.key === capacity);
|
||||
|
||||
return capacityOption ? (
|
||||
<div className={css.sectionCapacity}>
|
||||
<h2 className={css.capacityTitle}>
|
||||
<FormattedMessage id="ListingPage.capacityTitle" />
|
||||
</h2>
|
||||
<p className={css.capacity}>{capacityOption.label}</p>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
SectionCapacity.propTypes = {
|
||||
options: array.isRequired,
|
||||
publicData: shape({
|
||||
capacity: string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default SectionCapacity;
|
||||
```
|
||||
|
||||
Remember to add corresponding css definitions to `ListingPage.css` to get the styling right. Import
|
||||
that into `ListingPage` and place it inside the `<div className={css.mainContent}>` element:
|
||||
|
||||
|
||||
```js
|
||||
import SectionCapacity from './SectionCapacity';
|
||||
```
|
||||
```js
|
||||
<div className={css.mainContent}>
|
||||
// other sections
|
||||
|
||||
<SectionCapacity publicData={publicData} options={capacityOptions} />
|
||||
|
||||
// other sections
|
||||
</div>
|
||||
|
||||
```
|
||||
```js
|
||||
ListingPageComponent.defaultProps = {
|
||||
// other default props
|
||||
capacityOptions: config.custom.capacityOptions,
|
||||
};
|
||||
|
||||
ListingPageComponent.propTypes = {
|
||||
// other props
|
||||
capacityOptions: array,
|
||||
}
|
||||
```
|
||||
|
||||
In the snippet above, the capacity options are passed to the `ListingPage` as a property, with the
|
||||
default proprty value pulling the options from the custom config (`marketplace-custom-config.js`).
|
||||
This way the listing page test can define it's own capacity options that are in line with test data
|
||||
used in the test and custom config changes will not affect test results.
|
||||
|
||||
And voilà, we have listing capacity presented in the listing page!
|
||||
|
||||

|
||||
|
||||
## Use the attribute as a search filter
|
||||
|
||||
To see how the capacity attribute can be used to filter search results, please refer to the [search
|
||||
filters documentation](./search-filters.md).
|
||||
|
|
@ -25,12 +25,15 @@ desktop search view.
|
|||
Next we'll guide you through the steps of adding a _capacity_ filter to the marketplace.
|
||||
|
||||
First step for adding a new filter is to make sure that the data being used for filtering is saved
|
||||
in the listing's `publicData` attribute. This can be done by modifying the `EditListingWizard`. Another
|
||||
aspect in search filters is that the public data needs to be indexed in the API. This is currently
|
||||
achieved with a manual operation done by the Sharetribe support. Once a public data attribute is
|
||||
added to the listings and the data attribute is indexed, the listing searches can be filtered by
|
||||
that attribute by adding a query parameter that consists of a preceding "pub\_" and the attribute
|
||||
name, so for the _capacity_ attribute the parameter would be "pub_capacity".
|
||||
in the listing's `publicData` attribute. On how to achieve this, please refer to the [documentation
|
||||
on extending the listing data model](./extend-listing.md). Another aspect in search filters is that
|
||||
the public data needs to be indexed in the API. This is currently achieved with a manual operation
|
||||
done by the Sharetribe support. Once a public data attribute is added to the listings and the data
|
||||
attribute is indexed, the listing searches can be filtered by that attribute by adding a query
|
||||
parameter that consists of a preceding "pub\_" and the attribute name, so for the _capacity_
|
||||
attribute the parameter would be "pub_capacity".
|
||||
|
||||
Further reading on public data can be found in the [extended data documentation](./extended-data.md).
|
||||
|
||||
### Common changes
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue