@@ -100,9 +107,9 @@ class MainPanel extends Component {
this.setState({ isSearchFiltersPanelOpen: false })}
+ filterParamNames={secondaryFilterParamNames}
{...secondaryFilters}
/>
@@ -159,9 +166,18 @@ MainPanel.propTypes = {
pagination: propTypes.pagination,
searchParamsForPagination: object,
showAsModalMaxWidth: number.isRequired,
- customURLParamToConfig: object.isRequired,
- primaryFilters: object,
- secondaryFilters: object,
+ primaryFilters: objectOf(
+ shape({
+ paramName: string.isRequired,
+ options: array.isRequired,
+ })
+ ),
+ secondaryFilters: objectOf(
+ shape({
+ paramName: string.isRequired,
+ options: array.isRequired,
+ })
+ ),
};
export default MainPanel;
diff --git a/src/containers/SearchPage/SearchPage.helpers.js b/src/containers/SearchPage/SearchPage.helpers.js
index 48e0fe44..c61f3c46 100644
--- a/src/containers/SearchPage/SearchPage.helpers.js
+++ b/src/containers/SearchPage/SearchPage.helpers.js
@@ -4,53 +4,93 @@ import { createResourceLocatorString } from '../../util/routes';
import { createSlug } from '../../util/urlHelpers';
import routeConfiguration from '../../routeConfiguration';
-// customURLParams
-export const validURLParamForExtendedData = (paramKey, urlParams, customConfigKeys) => {
- const configKey = customConfigKeys[paramKey];
- const value = urlParams[paramKey];
- const valueArray = value ? value.split(',') : [];
+/**
+ * Validates a filter search param agains a filters configuration.
+ *
+ * All invalid param names and values are dropped
+ *
+ * @param {String} paramName Search parameter name
+ * @param {Object} paramValue Search parameter value
+ * @param {Object} filters Filters configuration
+ */
+export const validURLParamForExtendedData = (paramName, paramValue, filters) => {
+ const filtersArray = Object.values(filters);
+ // resolve configuration for this filter
+ const filterConfig = filtersArray.find(f => f.paramName === paramName);
+
+ const valueArray = paramValue ? paramValue.split(',') : [];
+
+ if (filterConfig && valueArray.length > 0) {
+ const allowedValues = filterConfig.options.map(o => o.key);
- if (configKey && valueArray.length > 0) {
- const allowedValues = config.custom[configKey].map(a => a.key);
const validValues = intersection(valueArray, allowedValues).join(',');
- return validValues.length > 0 ? { [paramKey]: validValues } : {};
+ return validValues.length > 0 ? { [paramName]: validValues } : {};
}
return {};
};
-// validate filter params
-export const validURLParamsForExtendedData = (params, customURLParamToConfig) => {
- const paramKeys = Object.keys(params);
- const customURLParams = Object.keys(customURLParamToConfig);
- return paramKeys.reduce((validParams, paramKey) => {
- return customURLParams.includes(paramKey)
+/**
+ * Checks filter param value validity.
+ *
+ * Non-filter params are dropped.
+ *
+ * @param {Object} params Search params
+ * @param {Object} filters Filters configuration
+ */
+export const validFilterParams = (params, filters) => {
+ const filterParamNames = Object.values(filters).map(f => f.paramName);
+ const paramEntries = Object.entries(params);
+
+ return paramEntries.reduce((validParams, entry) => {
+ const paramName = entry[0];
+ const paramValue = entry[1];
+
+ return filterParamNames.includes(paramName)
? {
...validParams,
- ...validURLParamForExtendedData(paramKey, params, customURLParamToConfig),
+ ...validURLParamForExtendedData(paramName, paramValue, filters),
}
- : { ...validParams, [paramKey]: params[paramKey] };
+ : { ...validParams };
+ }, {});
+};
+
+/**
+ * Checks filter param value validity.
+ *
+ * Non-filter params are returned as they are.
+ *
+ * @param {Object} params Search params
+ * @param {Object} filters Filters configuration
+ */
+export const validURLParamsForExtendedData = (params, filters) => {
+ const filterParamNames = Object.values(filters).map(f => f.paramName);
+ const paramEntries = Object.entries(params);
+
+ return paramEntries.reduce((validParams, entry) => {
+ const paramName = entry[0];
+ const paramValue = entry[1];
+
+ return filterParamNames.includes(paramName)
+ ? {
+ ...validParams,
+ ...validURLParamForExtendedData(paramName, paramValue, filters),
+ }
+ : { ...validParams, [paramName]: paramValue };
}, {});
};
// extract search parameters, including a custom URL params
// which are validated by mapping the values to marketplace custom config.
-export const pickSearchParamsOnly = (params, customURLParamToConfig) => {
+export const pickSearchParamsOnly = (params, filters) => {
const { address, origin, bounds, country, ...rest } = params || {};
- const customURLParams = Object.keys(customURLParamToConfig);
const boundsMaybe = bounds ? { bounds } : {};
const originMaybe = config.sortSearchByDistance && origin ? { origin } : {};
-
- const customSearchParamKeys = Object.keys(rest);
- const customSearchParams = customSearchParamKeys.reduce((validParams, paramKey) => {
- return customURLParams.includes(paramKey)
- ? { ...validParams, ...validURLParamForExtendedData(paramKey, rest, customURLParamToConfig) }
- : { ...validParams };
- }, {});
+ const filterParams = validFilterParams(rest, filters);
return {
...boundsMaybe,
...originMaybe,
- ...customSearchParams,
+ ...filterParams,
};
};
diff --git a/src/containers/SearchPage/SearchPage.helpers.test.js b/src/containers/SearchPage/SearchPage.helpers.test.js
new file mode 100644
index 00000000..52edf7bb
--- /dev/null
+++ b/src/containers/SearchPage/SearchPage.helpers.test.js
@@ -0,0 +1,124 @@
+import {
+ validURLParamForExtendedData,
+ validFilterParams,
+ validURLParamsForExtendedData,
+ pickSearchParamsOnly,
+} from './SearchPage.helpers.js';
+
+const urlParams = {
+ pub_category: 'smoke',
+ pub_amenities: 'towels,bathroom',
+};
+
+const filters = {
+ categoryFilter: {
+ paramName: 'pub_category',
+ options: [{ key: 'smoke' }, { key: 'wooden' }],
+ },
+ amenitiesFilter: {
+ paramName: 'pub_amenities',
+ options: [{ key: 'towels' }, { key: 'bathroom' }],
+ },
+};
+
+describe('SearchPage.helpers', () => {
+ describe('validURLParamForExtendedData', () => {
+ it('returns a valid parameter', () => {
+ const validParam = validURLParamForExtendedData('pub_category', 'smoke', filters);
+ expect(validParam).toEqual({ pub_category: 'smoke' });
+ });
+
+ it('takes empty params', () => {
+ const validParam = validURLParamForExtendedData('pub_category', '', filters);
+ expect(validParam).toEqual({});
+ });
+
+ it('drops an invalid param value', () => {
+ const validParam = validURLParamForExtendedData('pub_category', 'invalid', filters);
+ expect(validParam).toEqual({});
+ });
+
+ it('drops a param with invalid name', () => {
+ const validParam = validURLParamForExtendedData('pub_invalid', 'towels', filters);
+ expect(validParam).toEqual({});
+ });
+ });
+
+ describe('validFilterParams', () => {
+ it('returns valid parameters', () => {
+ const validParams = validFilterParams(urlParams, filters);
+ expect(validParams).toEqual(urlParams);
+ });
+
+ it('takes empty params', () => {
+ const validParams = validFilterParams({}, filters);
+ expect(validParams).toEqual({});
+ });
+
+ it('drops an invalid filter param value', () => {
+ const params = { pub_category: 'smoke', pub_amenities: 'invalid1,invalid2' };
+ const validParams = validFilterParams(params, filters);
+ expect(validParams).toEqual({ pub_category: 'smoke' });
+ });
+
+ it('drops non-filter params', () => {
+ const params = { pub_category: 'smoke', other_param: 'somevalue' };
+ const validParams = validFilterParams(params, filters);
+ expect(validParams).toEqual({ pub_category: 'smoke' });
+ });
+ });
+
+ describe('validURLParamsForExtendedData', () => {
+ it('returns valid parameters', () => {
+ const validParams = validURLParamsForExtendedData(urlParams, filters);
+ expect(validParams).toEqual(urlParams);
+ });
+
+ it('takes empty params', () => {
+ const validParams = validURLParamsForExtendedData({}, filters);
+ expect(validParams).toEqual({});
+ });
+
+ it('drops an invalid filter param value', () => {
+ const params = { pub_category: 'smoke', pub_amenities: 'invalid1,invalid2' };
+ const validParams = validURLParamsForExtendedData(params, filters);
+ expect(validParams).toEqual({ pub_category: 'smoke' });
+ });
+
+ it('returns non-filter params', () => {
+ const params = { pub_category: 'smoke', other_param: 'somevalue' };
+ const validParams = validURLParamsForExtendedData(params, filters);
+ expect(validParams).toEqual(params);
+ });
+ });
+
+ describe('pickSearchParamsOnly', () => {
+ it('returns search parameters', () => {
+ const params = {
+ address: 'address value',
+ origin: 'origin value',
+ bounds: 'bounds value',
+ country: 'country value',
+ };
+ const validParams = pickSearchParamsOnly(params, filters);
+ expect(validParams).toEqual({ bounds: 'bounds value' });
+ });
+
+ it('returns filter parameters', () => {
+ const validParams = pickSearchParamsOnly(urlParams, filters);
+ expect(validParams).toEqual(urlParams);
+ });
+
+ it('drops an invalid filter param value', () => {
+ const params = { pub_category: 'smoke', pub_amenities: 'invalid1,invalid2' };
+ const validParams = pickSearchParamsOnly(params, filters);
+ expect(validParams).toEqual({ pub_category: 'smoke' });
+ });
+
+ it('drops non-search params', () => {
+ const params = { pub_category: 'smoke', other_param: 'somevalue' };
+ const validParams = pickSearchParamsOnly(params, filters);
+ expect(validParams).toEqual({ pub_category: 'smoke' });
+ });
+ });
+});
diff --git a/src/containers/SearchPage/SearchPage.js b/src/containers/SearchPage/SearchPage.js
index 47a2f449..a27f0cd3 100644
--- a/src/containers/SearchPage/SearchPage.js
+++ b/src/containers/SearchPage/SearchPage.js
@@ -26,6 +26,7 @@ import { searchListings, searchMapListings, setActiveListing } from './SearchPag
import {
pickSearchParamsOnly,
validURLParamsForExtendedData,
+ validFilterParams,
createSearchResultSchema,
} from './SearchPage.helpers';
import MainPanel from './MainPanel';
@@ -39,14 +40,6 @@ 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 = 'pub_category';
-const AMENITIES_URL_PARAM = 'pub_amenities';
-
-const customURLParamToConfig = {
- [CATEGORY_URL_PARAM]: 'categories',
- [AMENITIES_URL_PARAM]: 'amenities',
-};
-
export class SearchPageComponent extends Component {
constructor(props) {
super(props);
@@ -65,11 +58,27 @@ export class SearchPageComponent extends Component {
this.modalOpenedBoundsChange = false;
this.searchMapListingsInProgress = false;
+ this.filters = this.filters.bind(this);
this.onIdle = debounce(this.onIdle.bind(this), SEARCH_WITH_MAP_DEBOUNCE);
this.onOpenMobileModal = this.onOpenMobileModal.bind(this);
this.onCloseMobileModal = this.onCloseMobileModal.bind(this);
}
+ filters() {
+ const { categories, amenities } = this.props;
+
+ return {
+ categoryFilter: {
+ paramName: 'pub_category',
+ options: categories,
+ },
+ amenitiesFilter: {
+ paramName: 'pub_amenities',
+ options: amenities,
+ },
+ };
+ }
+
componentWillReceiveProps(nextProps) {
if (!isEqual(this.props.location, nextProps.location)) {
// If no mapSearch url parameter is given, this is original location search
@@ -118,7 +127,7 @@ export class SearchPageComponent extends Component {
bounds: viewportBounds,
country,
mapSearch: true,
- ...validURLParamsForExtendedData(rest, customURLParamToConfig),
+ ...validFilterParams(rest, this.filters()),
};
this.viewportBounds = viewportBounds;
@@ -155,8 +164,6 @@ export class SearchPageComponent extends Component {
searchInProgress,
searchListingsError,
searchParams,
- categories,
- amenities,
activeListingId,
onActivateListing,
} = this.props;
@@ -166,16 +173,18 @@ export class SearchPageComponent extends Component {
latlngBounds: ['bounds'],
});
+ const filters = this.filters();
+
// urlQueryParams doesn't contain page specific url params
// like mapSearch, page or origin (origin depends on config.sortSearchByDistance)
- const urlQueryParams = pickSearchParamsOnly(searchInURL, customURLParamToConfig);
+ const urlQueryParams = pickSearchParamsOnly(searchInURL, filters);
// Page transition might initially use values from previous search
const urlQueryString = stringify(urlQueryParams);
- const paramsQueryString = stringify(pickSearchParamsOnly(searchParams, customURLParamToConfig));
+ const paramsQueryString = stringify(pickSearchParamsOnly(searchParams, filters));
const searchParamsAreInSync = urlQueryString === paramsQueryString;
- const validQueryParams = validURLParamsForExtendedData(searchInURL, customURLParamToConfig);
+ const validQueryParams = validURLParamsForExtendedData(searchInURL, filters);
const isWindowDefined = typeof window !== 'undefined';
const isMobileLayout = isWindowDefined && window.innerWidth < MODAL_BREAKPOINT;
@@ -227,8 +236,10 @@ export class SearchPageComponent extends Component {
pagination={pagination}
searchParamsForPagination={parse(location.search)}
showAsModalMaxWidth={MODAL_BREAKPOINT}
- customURLParamToConfig={customURLParamToConfig}
- primaryFilters={{ amenities, categories }}
+ primaryFilters={{
+ categoryFilter: filters.categoryFilter,
+ amenitiesFilter: filters.amenitiesFilter,
+ }}
/>