diff --git a/docs/assets/extend-listing/capacity_listing_page.png b/docs/assets/extend-listing/capacity_listing_page.png new file mode 100644 index 00000000..23a2b3b3 Binary files /dev/null and b/docs/assets/extend-listing/capacity_listing_page.png differ diff --git a/docs/assets/extend-listing/edit_capacity.png b/docs/assets/extend-listing/edit_capacity.png new file mode 100644 index 00000000..a7f7478c Binary files /dev/null and b/docs/assets/extend-listing/edit_capacity.png differ diff --git a/docs/extend-listing.md b/docs/extend-listing.md new file mode 100644 index 00000000..4ad8cf1d --- /dev/null +++ b/docs/extend-listing.md @@ -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 => ( + { + const { + className, + disabled, + handleSubmit, + intl, + invalid, + pristine, + saveActionMsg, + updated, + updateError, + updateInProgress, + capacityOptions, + } = fieldRenderProps; + + const capacityPlaceholder= intl.formatMessage({ + id: 'EditListingCapacityForm.capacityPlaceholder', + }); + + const errorMessage = updateError ? ( +

+ +

+ ) : 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 ( +
+ {errorMessage} + + + + {capacityOptions.map(c => ( + + ))} + + + +
+ ); + }} + /> +); + +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 ? ( + }} + /> + ) : ( + + ); + + return ( +
+

{panelTitle}

+ { + const { capacity } = values; + const updateValues = { + publicData: { + capacity, + }, + }; + onSubmit(updateValues); + }} + onChange={onChange} + saveActionMsg={submitButtonText} + updated={panelUpdated} + updateError={errors.updateListingError} + updateInProgress={updateInProgress} + capacityOptions={config.custom.capacityOptions} + /> +
+ ); +}; + +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 ( + { + onCompleteEditListingWizardTab(tab, values); + }} + /> + ); +} +``` + +There! Now we've extended the listing data model with capacity information. + +![Edit the capacity](./assets/extend-listing/edit_capacity.png) + +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 ? ( +
+

+ +

+

{capacityOption.label}

+
+ ) : 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 `
` element: + + +```js +import SectionCapacity from './SectionCapacity'; +``` +```js +
+ // other sections + + + + // other sections +
+ +``` +```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! + +![Capacity on listing page](./assets/extend-listing/capacity_listing_page.png) + +## 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). diff --git a/docs/search-filters.md b/docs/search-filters.md index ebf7e261..7084e608 100644 --- a/docs/search-filters.md +++ b/docs/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