@constructor-io/constructorio-ui-plp 1.8.4 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/constructorio-ui-plp-bundled.js +12 -12
  2. package/lib/cjs/components/Filters/FilterGroup.js +2 -2
  3. package/lib/cjs/components/Filters/Filters.js +2 -2
  4. package/lib/cjs/components/Filters/UseFilterOptionsList.js +9 -1
  5. package/lib/cjs/hooks/useFilter.js +8 -2
  6. package/lib/cjs/hooks/useGroups.js +1 -0
  7. package/lib/cjs/hooks/useOptionsList.js +21 -2
  8. package/lib/cjs/utils/dataAttributeHelpers.js +7 -3
  9. package/lib/cjs/utils/itemFieldGetters.js +11 -1
  10. package/lib/cjs/utils/urlHelpers.js +4 -0
  11. package/lib/cjs/version.js +1 -1
  12. package/lib/mjs/components/Filters/FilterGroup.js +2 -2
  13. package/lib/mjs/components/Filters/Filters.js +2 -2
  14. package/lib/mjs/components/Filters/UseFilterOptionsList.js +10 -2
  15. package/lib/mjs/hooks/useFilter.js +8 -2
  16. package/lib/mjs/hooks/useGroups.js +1 -0
  17. package/lib/mjs/hooks/useOptionsList.js +24 -2
  18. package/lib/mjs/utils/dataAttributeHelpers.js +4 -0
  19. package/lib/mjs/utils/itemFieldGetters.js +6 -0
  20. package/lib/mjs/utils/urlHelpers.js +4 -0
  21. package/lib/mjs/version.js +1 -1
  22. package/lib/types/components/Filters/FilterGroup.d.ts +6 -1
  23. package/lib/types/components/Filters/Filters.d.ts +6 -1
  24. package/lib/types/components/Filters/UseFilterOptionsList.d.ts +8 -3
  25. package/lib/types/hooks/useFilter.d.ts +5 -0
  26. package/lib/types/hooks/useOptionsList.d.ts +5 -0
  27. package/lib/types/types.d.ts +4 -2
  28. package/lib/types/utils/dataAttributeHelpers.d.ts +1 -0
  29. package/lib/types/utils/itemFieldGetters.d.ts +3 -1
  30. package/lib/types/version.d.ts +1 -1
  31. package/package.json +1 -1
@@ -6,7 +6,7 @@ const utils_1 = require("../../utils");
6
6
  const FilterOptionsList_1 = tslib_1.__importDefault(require("./FilterOptionsList"));
7
7
  const FilterRangeSlider_1 = tslib_1.__importDefault(require("./FilterRangeSlider"));
8
8
  function FilterGroup(props) {
9
- const { facet, setFilter, initialNumOptions = 10, sliderStep, facetSliderSteps } = props;
9
+ const { facet, setFilter, initialNumOptions = 10, sliderStep, facetSliderSteps, isHiddenFilterOptionFn, } = props;
10
10
  const [isCollapsed, setIsCollapsed] = (0, react_1.useState)(false);
11
11
  const toggleIsCollapsed = () => setIsCollapsed(!isCollapsed);
12
12
  const onFilterSelect = (facetName) => (value) => {
@@ -16,7 +16,7 @@ function FilterGroup(props) {
16
16
  react_1.default.createElement("button", { className: 'cio-filter-header', type: 'button', onClick: toggleIsCollapsed },
17
17
  facet.displayName,
18
18
  react_1.default.createElement("i", { className: `cio-arrow ${isCollapsed ? 'cio-arrow-up' : 'cio-arrow-down'}` })),
19
- ((0, utils_1.isMultipleOrBucketedFacet)(facet) || (0, utils_1.isSingleFacet)(facet)) && (react_1.default.createElement(FilterOptionsList_1.default, { isCollapsed: isCollapsed, facet: facet, modifyRequestMultipleFilter: onFilterSelect(facet.name), initialNumOptions: initialNumOptions })),
19
+ ((0, utils_1.isMultipleOrBucketedFacet)(facet) || (0, utils_1.isSingleFacet)(facet)) && (react_1.default.createElement(FilterOptionsList_1.default, { isCollapsed: isCollapsed, facet: facet, modifyRequestMultipleFilter: onFilterSelect(facet.name), initialNumOptions: initialNumOptions, isHiddenFilterOptionFn: isHiddenFilterOptionFn })),
20
20
  (0, utils_1.isRangeFacet)(facet) && (react_1.default.createElement(FilterRangeSlider_1.default, { isCollapsed: isCollapsed, rangedFacet: facet, modifyRequestRangeFilter: onFilterSelect(facet.name), sliderStep: (facetSliderSteps === null || facetSliderSteps === void 0 ? void 0 : facetSliderSteps[facet.name]) || sliderStep }))));
21
21
  }
22
22
  exports.default = FilterGroup;
@@ -6,7 +6,7 @@ const react_1 = tslib_1.__importDefault(require("react"));
6
6
  const FilterGroup_1 = tslib_1.__importDefault(require("./FilterGroup"));
7
7
  const useFilter_1 = tslib_1.__importDefault(require("../../hooks/useFilter"));
8
8
  function Filters(props) {
9
- const { children, initialNumOptions } = props, useFiltersProps = tslib_1.__rest(props, ["children", "initialNumOptions"]);
9
+ const { children, initialNumOptions, isHiddenFilterOptionFn } = props, useFiltersProps = tslib_1.__rest(props, ["children", "initialNumOptions", "isHiddenFilterOptionFn"]);
10
10
  const { facets, setFilter, sliderStep, facetSliderSteps, clearFilters } = (0, useFilter_1.default)(useFiltersProps);
11
11
  return (react_1.default.createElement(react_1.default.Fragment, null, typeof children === 'function' ? (children({
12
12
  facets,
@@ -14,6 +14,6 @@ function Filters(props) {
14
14
  sliderStep,
15
15
  facetSliderSteps,
16
16
  clearFilters,
17
- })) : (react_1.default.createElement("div", { className: 'cio-filters' }, facets.map((facet) => (react_1.default.createElement(FilterGroup_1.default, { facet: facet, initialNumOptions: initialNumOptions, setFilter: setFilter, sliderStep: sliderStep, facetSliderSteps: facetSliderSteps, key: facet.name })))))));
17
+ })) : (react_1.default.createElement("div", { className: 'cio-filters' }, facets.map((facet) => (react_1.default.createElement(FilterGroup_1.default, { facet: facet, initialNumOptions: initialNumOptions, setFilter: setFilter, sliderStep: sliderStep, facetSliderSteps: facetSliderSteps, isHiddenFilterOptionFn: isHiddenFilterOptionFn, key: facet.name })))))));
18
18
  }
19
19
  exports.default = Filters;
@@ -3,12 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = require("react");
5
5
  const useOptionsList_1 = tslib_1.__importDefault(require("../../hooks/useOptionsList"));
6
+ const useCioPlpContext_1 = require("../../hooks/useCioPlpContext");
6
7
  function useFilterOptionsList(props) {
7
- const { initialNumOptions, modifyRequestMultipleFilter, isCollapsed } = props;
8
+ const { initialNumOptions, modifyRequestMultipleFilter, isCollapsed, isHiddenFilterOptionFn } = props;
8
9
  const facet = 'facet' in props ? props.facet : props.multipleFacet;
10
+ const { getIsHiddenFilterOptionField } = (0, useCioPlpContext_1.useCioPlpContext)().itemFieldGetters;
11
+ const isHiddenOptionFn = (0, react_1.useCallback)((option) => (typeof isHiddenFilterOptionFn === 'function' && isHiddenFilterOptionFn(option)) ||
12
+ (typeof getIsHiddenFilterOptionField === 'function' &&
13
+ getIsHiddenFilterOptionField(option)) ||
14
+ false, [isHiddenFilterOptionFn, getIsHiddenFilterOptionField]);
9
15
  const { isShowAll, setIsShowAll, optionsToRender, setOptionsToRender } = (0, useOptionsList_1.default)({
10
16
  options: facet.options,
11
17
  initialNumOptions,
18
+ isHiddenOptionFn,
19
+ nestedOptionsKey: 'options', // Enable recursive filtering for hierarchical facet options
12
20
  });
13
21
  const [selectedOptionMap, setSelectedOptionMap] = (0, react_1.useState)({});
14
22
  const onOptionSelect = (optionValue) => {
@@ -1,15 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
+ const react_1 = require("react");
4
5
  const useCioPlpContext_1 = require("./useCioPlpContext");
5
6
  const useRequestConfigs_1 = tslib_1.__importDefault(require("./useRequestConfigs"));
6
7
  function useFilter(props) {
7
- const { facets, sliderStep, facetSliderSteps } = props;
8
+ const { facets, sliderStep, facetSliderSteps, isHiddenFilterFn } = props;
8
9
  const contextValue = (0, useCioPlpContext_1.useCioPlpContext)();
9
10
  if (!contextValue) {
10
11
  throw new Error('useFilter must be used within a component that is a child of <CioPlp />');
11
12
  }
13
+ const { getIsHiddenFilterField } = contextValue.itemFieldGetters;
12
14
  const { getRequestConfigs, setRequestConfigs } = (0, useRequestConfigs_1.default)();
15
+ const isHiddenFilter = (0, react_1.useCallback)((facet) => (typeof isHiddenFilterFn === 'function' && isHiddenFilterFn(facet)) ||
16
+ (typeof getIsHiddenFilterField === 'function' && getIsHiddenFilterField(facet)) ||
17
+ false, [isHiddenFilterFn, getIsHiddenFilterField]);
18
+ const filteredFacets = (0, react_1.useMemo)(() => facets.filter((facet) => !isHiddenFilter(facet)), [facets, isHiddenFilter]);
13
19
  const setFilter = (filterName, filterValue) => {
14
20
  const newFilters = getRequestConfigs().filters || {};
15
21
  newFilters[filterName] = filterValue;
@@ -23,7 +29,7 @@ function useFilter(props) {
23
29
  setRequestConfigs({ filters: {}, page: 1 });
24
30
  };
25
31
  return {
26
- facets,
32
+ facets: filteredFacets,
27
33
  setFilter,
28
34
  sliderStep,
29
35
  facetSliderSteps,
@@ -36,6 +36,7 @@ function useGroups(props) {
36
36
  options: groupOptions,
37
37
  initialNumOptions: numOptionsProps,
38
38
  isHiddenOptionFn,
39
+ nestedOptionsKey: 'children', // Enable recursive filtering for group children
39
40
  });
40
41
  const [selectedGroupId, setSelectedGroupId] = (0, react_1.useState)();
41
42
  const onOptionSelect = (groupId) => {
@@ -2,9 +2,28 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const react_1 = require("react");
4
4
  const defaultIsHiddenOptionFn = () => false;
5
+ /**
6
+ * Recursively filters options and their nested children
7
+ */
8
+ function filterOptionsRecursively(options, isHiddenFn, nestedKey) {
9
+ return options
10
+ .filter((option) => !isHiddenFn(option))
11
+ .map((option) => {
12
+ const nestedOptions = option[nestedKey];
13
+ if (Array.isArray(nestedOptions) && nestedOptions.length > 0) {
14
+ return Object.assign(Object.assign({}, option), { [nestedKey]: filterOptionsRecursively(nestedOptions, isHiddenFn, nestedKey) });
15
+ }
16
+ return option;
17
+ });
18
+ }
5
19
  function useOptionsList(props) {
6
- const { options, initialNumOptions = 5, isHiddenOptionFn = defaultIsHiddenOptionFn } = props;
7
- const filteredOptions = (0, react_1.useMemo)(() => options.filter((option) => !isHiddenOptionFn(option)), [isHiddenOptionFn, options]);
20
+ const { options, initialNumOptions = 5, isHiddenOptionFn = defaultIsHiddenOptionFn, nestedOptionsKey, } = props;
21
+ const filteredOptions = (0, react_1.useMemo)(() => {
22
+ if (nestedOptionsKey) {
23
+ return filterOptionsRecursively(options, isHiddenOptionFn, nestedOptionsKey);
24
+ }
25
+ return options.filter((option) => !isHiddenOptionFn(option));
26
+ }, [isHiddenOptionFn, options, nestedOptionsKey]);
8
27
  const [isShowAll, setIsShowAll] = (0, react_1.useState)(false);
9
28
  const [optionsToRender, setOptionsToRender] = (0, react_1.useState)(filteredOptions);
10
29
  (0, react_1.useEffect)(() => {
@@ -16,6 +16,7 @@ exports.cnstrcDataAttrs = {
16
16
  zeroResults: 'data-cnstrc-zero-result',
17
17
  slCampaignId: 'data-cnstrc-sl-campaign-id',
18
18
  slCampaignOwner: 'data-cnstrc-sl-campaign-owner',
19
+ resultPage: 'data-cnstrc-result-page',
19
20
  },
20
21
  search: {
21
22
  searchContainer: 'data-cnstrc-search',
@@ -53,18 +54,20 @@ function getProductCardCnstrcDataAttributes(productInfo, options) {
53
54
  }
54
55
  exports.getProductCardCnstrcDataAttributes = getProductCardCnstrcDataAttributes;
55
56
  function getPlpContainerCnstrcDataAttributes(data, requestConfigs, isLoading = false) {
56
- var _a, _b;
57
+ var _a, _b, _c;
57
58
  if (!data || (!(0, typeHelpers_1.isPlpSearchDataResults)(data) && !(0, typeHelpers_1.isPlpBrowseDataResults)(data)))
58
59
  return {};
59
60
  const { filterName, filterValue } = requestConfigs;
60
61
  const pageType = (0, requestConfigsHelpers_1.getPageType)(requestConfigs);
61
62
  const isZeroResults = data.response.totalNumResults === 0;
63
+ const resultPage = ((_a = data === null || data === void 0 ? void 0 : data.request) === null || _a === void 0 ? void 0 : _a.page) || requestConfigs.page || 1;
62
64
  let dataCnstrc = {};
63
65
  switch (pageType) {
64
66
  case 'browse':
65
67
  dataCnstrc = {
66
68
  [exports.cnstrcDataAttrs.browse.browseContainer]: true,
67
69
  [exports.cnstrcDataAttrs.common.numResults]: data.response.totalNumResults,
70
+ [exports.cnstrcDataAttrs.common.resultPage]: resultPage,
68
71
  [exports.cnstrcDataAttrs.common.resultId]: data.resultId,
69
72
  [exports.cnstrcDataAttrs.browse.filterName]: filterName,
70
73
  [exports.cnstrcDataAttrs.browse.filterValue]: filterValue,
@@ -75,9 +78,10 @@ function getPlpContainerCnstrcDataAttributes(data, requestConfigs, isLoading = f
75
78
  [exports.cnstrcDataAttrs.search.searchContainer]: true,
76
79
  [exports.cnstrcDataAttrs.common.resultId]: data.resultId,
77
80
  [exports.cnstrcDataAttrs.common.numResults]: data.response.totalNumResults,
81
+ [exports.cnstrcDataAttrs.common.resultPage]: resultPage,
78
82
  };
79
83
  // Add search term
80
- if ((_a = data.request) === null || _a === void 0 ? void 0 : _a.term) {
84
+ if ((_b = data.request) === null || _b === void 0 ? void 0 : _b.term) {
81
85
  dataCnstrc[exports.cnstrcDataAttrs.search.searchTerm] = data.request.term;
82
86
  }
83
87
  break;
@@ -94,7 +98,7 @@ function getPlpContainerCnstrcDataAttributes(data, requestConfigs, isLoading = f
94
98
  dataCnstrc[exports.cnstrcDataAttrs.common.zeroResults] = true;
95
99
  }
96
100
  // Add section if it's not "Products"
97
- if (((_b = data.request) === null || _b === void 0 ? void 0 : _b.section) && data.request.section !== 'Products') {
101
+ if (((_c = data.request) === null || _c === void 0 ? void 0 : _c.section) && data.request.section !== 'Products') {
98
102
  dataCnstrc[exports.cnstrcDataAttrs.common.section] = data.request.section;
99
103
  }
100
104
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getItemUrl = exports.getIsHiddenGroupField = exports.getSwatchPreview = exports.getSwatches = exports.getRolloverImage = exports.getSalePrice = exports.getPrice = void 0;
3
+ exports.getItemUrl = exports.getIsHiddenFilterOptionField = exports.getIsHiddenFilterField = exports.getIsHiddenGroupField = exports.getSwatchPreview = exports.getSwatches = exports.getRolloverImage = exports.getSalePrice = exports.getPrice = void 0;
4
4
  // eslint-disable-next-line import/prefer-default-export
5
5
  function getPrice(item) {
6
6
  return item.data.price;
@@ -45,6 +45,16 @@ function getIsHiddenGroupField(group) {
45
45
  return (_a = group === null || group === void 0 ? void 0 : group.data) === null || _a === void 0 ? void 0 : _a.cio_plp_hidden;
46
46
  }
47
47
  exports.getIsHiddenGroupField = getIsHiddenGroupField;
48
+ function getIsHiddenFilterField(facet) {
49
+ var _a;
50
+ return (_a = facet === null || facet === void 0 ? void 0 : facet.data) === null || _a === void 0 ? void 0 : _a.cio_plp_hidden;
51
+ }
52
+ exports.getIsHiddenFilterField = getIsHiddenFilterField;
53
+ function getIsHiddenFilterOptionField(option) {
54
+ var _a;
55
+ return (_a = option === null || option === void 0 ? void 0 : option.data) === null || _a === void 0 ? void 0 : _a.cio_plp_hidden;
56
+ }
57
+ exports.getIsHiddenFilterOptionField = getIsHiddenFilterOptionField;
48
58
  function getItemUrl(item) {
49
59
  return item === null || item === void 0 ? void 0 : item.url;
50
60
  }
@@ -111,6 +111,10 @@ function getUrlFromState(state, url) {
111
111
  if (exports.defaultQueryStringMap[key] === undefined) {
112
112
  return;
113
113
  }
114
+ // Don't append page=1 to URL since it's the default and affects SEO
115
+ if (key === 'page' && val === 1) {
116
+ return;
117
+ }
114
118
  let encodedVal = '';
115
119
  if (key === 'filters' && state.filters) {
116
120
  getFilterParamsFromState(params, state.filters);
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '1.8.4';
3
+ exports.default = '1.10.0';
@@ -3,7 +3,7 @@ import { isMultipleOrBucketedFacet, isRangeFacet, isSingleFacet } from '../../ut
3
3
  import FilterOptionsList from './FilterOptionsList';
4
4
  import FilterRangeSlider from './FilterRangeSlider';
5
5
  export default function FilterGroup(props) {
6
- const { facet, setFilter, initialNumOptions = 10, sliderStep, facetSliderSteps } = props;
6
+ const { facet, setFilter, initialNumOptions = 10, sliderStep, facetSliderSteps, isHiddenFilterOptionFn, } = props;
7
7
  const [isCollapsed, setIsCollapsed] = useState(false);
8
8
  const toggleIsCollapsed = () => setIsCollapsed(!isCollapsed);
9
9
  const onFilterSelect = (facetName) => (value) => {
@@ -13,6 +13,6 @@ export default function FilterGroup(props) {
13
13
  React.createElement("button", { className: 'cio-filter-header', type: 'button', onClick: toggleIsCollapsed },
14
14
  facet.displayName,
15
15
  React.createElement("i", { className: `cio-arrow ${isCollapsed ? 'cio-arrow-up' : 'cio-arrow-down'}` })),
16
- (isMultipleOrBucketedFacet(facet) || isSingleFacet(facet)) && (React.createElement(FilterOptionsList, { isCollapsed: isCollapsed, facet: facet, modifyRequestMultipleFilter: onFilterSelect(facet.name), initialNumOptions: initialNumOptions })),
16
+ (isMultipleOrBucketedFacet(facet) || isSingleFacet(facet)) && (React.createElement(FilterOptionsList, { isCollapsed: isCollapsed, facet: facet, modifyRequestMultipleFilter: onFilterSelect(facet.name), initialNumOptions: initialNumOptions, isHiddenFilterOptionFn: isHiddenFilterOptionFn })),
17
17
  isRangeFacet(facet) && (React.createElement(FilterRangeSlider, { isCollapsed: isCollapsed, rangedFacet: facet, modifyRequestRangeFilter: onFilterSelect(facet.name), sliderStep: facetSliderSteps?.[facet.name] || sliderStep }))));
18
18
  }
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import FilterGroup from './FilterGroup';
4
4
  import useFilter from '../../hooks/useFilter';
5
5
  export default function Filters(props) {
6
- const { children, initialNumOptions, ...useFiltersProps } = props;
6
+ const { children, initialNumOptions, isHiddenFilterOptionFn, ...useFiltersProps } = props;
7
7
  const { facets, setFilter, sliderStep, facetSliderSteps, clearFilters } = useFilter(useFiltersProps);
8
8
  return (React.createElement(React.Fragment, null, typeof children === 'function' ? (children({
9
9
  facets,
@@ -11,5 +11,5 @@ export default function Filters(props) {
11
11
  sliderStep,
12
12
  facetSliderSteps,
13
13
  clearFilters,
14
- })) : (React.createElement("div", { className: 'cio-filters' }, facets.map((facet) => (React.createElement(FilterGroup, { facet: facet, initialNumOptions: initialNumOptions, setFilter: setFilter, sliderStep: sliderStep, facetSliderSteps: facetSliderSteps, key: facet.name })))))));
14
+ })) : (React.createElement("div", { className: 'cio-filters' }, facets.map((facet) => (React.createElement(FilterGroup, { facet: facet, initialNumOptions: initialNumOptions, setFilter: setFilter, sliderStep: sliderStep, facetSliderSteps: facetSliderSteps, isHiddenFilterOptionFn: isHiddenFilterOptionFn, key: facet.name })))))));
15
15
  }
@@ -1,11 +1,19 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useState } from 'react';
2
2
  import useOptionsList from '../../hooks/useOptionsList';
3
+ import { useCioPlpContext } from '../../hooks/useCioPlpContext';
3
4
  export default function useFilterOptionsList(props) {
4
- const { initialNumOptions, modifyRequestMultipleFilter, isCollapsed } = props;
5
+ const { initialNumOptions, modifyRequestMultipleFilter, isCollapsed, isHiddenFilterOptionFn } = props;
5
6
  const facet = 'facet' in props ? props.facet : props.multipleFacet;
7
+ const { getIsHiddenFilterOptionField } = useCioPlpContext().itemFieldGetters;
8
+ const isHiddenOptionFn = useCallback((option) => (typeof isHiddenFilterOptionFn === 'function' && isHiddenFilterOptionFn(option)) ||
9
+ (typeof getIsHiddenFilterOptionField === 'function' &&
10
+ getIsHiddenFilterOptionField(option)) ||
11
+ false, [isHiddenFilterOptionFn, getIsHiddenFilterOptionField]);
6
12
  const { isShowAll, setIsShowAll, optionsToRender, setOptionsToRender } = useOptionsList({
7
13
  options: facet.options,
8
14
  initialNumOptions,
15
+ isHiddenOptionFn,
16
+ nestedOptionsKey: 'options', // Enable recursive filtering for hierarchical facet options
9
17
  });
10
18
  const [selectedOptionMap, setSelectedOptionMap] = useState({});
11
19
  const onOptionSelect = (optionValue) => {
@@ -1,12 +1,18 @@
1
+ import { useCallback, useMemo } from 'react';
1
2
  import { useCioPlpContext } from './useCioPlpContext';
2
3
  import useRequestConfigs from './useRequestConfigs';
3
4
  export default function useFilter(props) {
4
- const { facets, sliderStep, facetSliderSteps } = props;
5
+ const { facets, sliderStep, facetSliderSteps, isHiddenFilterFn } = props;
5
6
  const contextValue = useCioPlpContext();
6
7
  if (!contextValue) {
7
8
  throw new Error('useFilter must be used within a component that is a child of <CioPlp />');
8
9
  }
10
+ const { getIsHiddenFilterField } = contextValue.itemFieldGetters;
9
11
  const { getRequestConfigs, setRequestConfigs } = useRequestConfigs();
12
+ const isHiddenFilter = useCallback((facet) => (typeof isHiddenFilterFn === 'function' && isHiddenFilterFn(facet)) ||
13
+ (typeof getIsHiddenFilterField === 'function' && getIsHiddenFilterField(facet)) ||
14
+ false, [isHiddenFilterFn, getIsHiddenFilterField]);
15
+ const filteredFacets = useMemo(() => facets.filter((facet) => !isHiddenFilter(facet)), [facets, isHiddenFilter]);
10
16
  const setFilter = (filterName, filterValue) => {
11
17
  const newFilters = getRequestConfigs().filters || {};
12
18
  newFilters[filterName] = filterValue;
@@ -20,7 +26,7 @@ export default function useFilter(props) {
20
26
  setRequestConfigs({ filters: {}, page: 1 });
21
27
  };
22
28
  return {
23
- facets,
29
+ facets: filteredFacets,
24
30
  setFilter,
25
31
  sliderStep,
26
32
  facetSliderSteps,
@@ -32,6 +32,7 @@ export default function useGroups(props) {
32
32
  options: groupOptions,
33
33
  initialNumOptions: numOptionsProps,
34
34
  isHiddenOptionFn,
35
+ nestedOptionsKey: 'children', // Enable recursive filtering for group children
35
36
  });
36
37
  const [selectedGroupId, setSelectedGroupId] = useState();
37
38
  const onOptionSelect = (groupId) => {
@@ -1,8 +1,30 @@
1
1
  import { useEffect, useMemo, useState } from 'react';
2
2
  const defaultIsHiddenOptionFn = () => false;
3
+ /**
4
+ * Recursively filters options and their nested children
5
+ */
6
+ function filterOptionsRecursively(options, isHiddenFn, nestedKey) {
7
+ return options
8
+ .filter((option) => !isHiddenFn(option))
9
+ .map((option) => {
10
+ const nestedOptions = option[nestedKey];
11
+ if (Array.isArray(nestedOptions) && nestedOptions.length > 0) {
12
+ return {
13
+ ...option,
14
+ [nestedKey]: filterOptionsRecursively(nestedOptions, isHiddenFn, nestedKey),
15
+ };
16
+ }
17
+ return option;
18
+ });
19
+ }
3
20
  export default function useOptionsList(props) {
4
- const { options, initialNumOptions = 5, isHiddenOptionFn = defaultIsHiddenOptionFn } = props;
5
- const filteredOptions = useMemo(() => options.filter((option) => !isHiddenOptionFn(option)), [isHiddenOptionFn, options]);
21
+ const { options, initialNumOptions = 5, isHiddenOptionFn = defaultIsHiddenOptionFn, nestedOptionsKey, } = props;
22
+ const filteredOptions = useMemo(() => {
23
+ if (nestedOptionsKey) {
24
+ return filterOptionsRecursively(options, isHiddenOptionFn, nestedOptionsKey);
25
+ }
26
+ return options.filter((option) => !isHiddenOptionFn(option));
27
+ }, [isHiddenOptionFn, options, nestedOptionsKey]);
6
28
  const [isShowAll, setIsShowAll] = useState(false);
7
29
  const [optionsToRender, setOptionsToRender] = useState(filteredOptions);
8
30
  useEffect(() => {
@@ -13,6 +13,7 @@ export const cnstrcDataAttrs = {
13
13
  zeroResults: 'data-cnstrc-zero-result',
14
14
  slCampaignId: 'data-cnstrc-sl-campaign-id',
15
15
  slCampaignOwner: 'data-cnstrc-sl-campaign-owner',
16
+ resultPage: 'data-cnstrc-result-page',
16
17
  },
17
18
  search: {
18
19
  searchContainer: 'data-cnstrc-search',
@@ -53,12 +54,14 @@ export function getPlpContainerCnstrcDataAttributes(data, requestConfigs, isLoad
53
54
  const { filterName, filterValue } = requestConfigs;
54
55
  const pageType = getPageType(requestConfigs);
55
56
  const isZeroResults = data.response.totalNumResults === 0;
57
+ const resultPage = data?.request?.page || requestConfigs.page || 1;
56
58
  let dataCnstrc = {};
57
59
  switch (pageType) {
58
60
  case 'browse':
59
61
  dataCnstrc = {
60
62
  [cnstrcDataAttrs.browse.browseContainer]: true,
61
63
  [cnstrcDataAttrs.common.numResults]: data.response.totalNumResults,
64
+ [cnstrcDataAttrs.common.resultPage]: resultPage,
62
65
  [cnstrcDataAttrs.common.resultId]: data.resultId,
63
66
  [cnstrcDataAttrs.browse.filterName]: filterName,
64
67
  [cnstrcDataAttrs.browse.filterValue]: filterValue,
@@ -69,6 +72,7 @@ export function getPlpContainerCnstrcDataAttributes(data, requestConfigs, isLoad
69
72
  [cnstrcDataAttrs.search.searchContainer]: true,
70
73
  [cnstrcDataAttrs.common.resultId]: data.resultId,
71
74
  [cnstrcDataAttrs.common.numResults]: data.response.totalNumResults,
75
+ [cnstrcDataAttrs.common.resultPage]: resultPage,
72
76
  };
73
77
  // Add search term
74
78
  if (data.request?.term) {
@@ -33,6 +33,12 @@ export function getSwatchPreview(variation) {
33
33
  export function getIsHiddenGroupField(group) {
34
34
  return group?.data?.cio_plp_hidden;
35
35
  }
36
+ export function getIsHiddenFilterField(facet) {
37
+ return facet?.data?.cio_plp_hidden;
38
+ }
39
+ export function getIsHiddenFilterOptionField(option) {
40
+ return option?.data?.cio_plp_hidden;
41
+ }
36
42
  export function getItemUrl(item) {
37
43
  return item?.url;
38
44
  }
@@ -99,6 +99,10 @@ export function getUrlFromState(state, url) {
99
99
  if (defaultQueryStringMap[key] === undefined) {
100
100
  return;
101
101
  }
102
+ // Don't append page=1 to URL since it's the default and affects SEO
103
+ if (key === 'page' && val === 1) {
104
+ return;
105
+ }
102
106
  let encodedVal = '';
103
107
  if (key === 'filters' && state.filters) {
104
108
  getFilterParamsFromState(params, state.filters);
@@ -1 +1 @@
1
- export default '1.8.4';
1
+ export default '1.10.0';
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { PlpFacet } from '../../types';
2
+ import type { PlpFacet, PlpFacetOption } from '../../types';
3
3
  import { UseFilterReturn } from '../../hooks/useFilter';
4
4
  export interface FilterGroupProps {
5
5
  facet: PlpFacet;
@@ -7,5 +7,10 @@ export interface FilterGroupProps {
7
7
  initialNumOptions?: number;
8
8
  sliderStep?: number;
9
9
  facetSliderSteps?: Record<string, number>;
10
+ /**
11
+ * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render
12
+ * @returns boolean
13
+ */
14
+ isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean;
10
15
  }
11
16
  export default function FilterGroup(props: FilterGroupProps): React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { IncludeRenderProps } from '../../types';
2
+ import { IncludeRenderProps, PlpFacetOption } from '../../types';
3
3
  import { UseFilterProps, UseFilterReturn } from '../../hooks/useFilter';
4
4
  export type FiltersProps = UseFilterProps & {
5
5
  /**
@@ -7,6 +7,11 @@ export type FiltersProps = UseFilterProps & {
7
7
  * The remaining options will be hidden under a "Show All" button
8
8
  */
9
9
  initialNumOptions?: number;
10
+ /**
11
+ * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render
12
+ * @returns boolean
13
+ */
14
+ isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean;
10
15
  };
11
16
  export type FiltersWithRenderProps = IncludeRenderProps<FiltersProps, UseFilterReturn>;
12
17
  export default function Filters(props: FiltersWithRenderProps): React.JSX.Element;
@@ -1,8 +1,13 @@
1
- import { PlpMultipleFacet, PlpSingleFacet } from '../../types';
1
+ import { PlpFacetOption, PlpMultipleFacet, PlpSingleFacet } from '../../types';
2
2
  interface UseFilterOptionsListPropsBase {
3
3
  modifyRequestMultipleFilter: (selectedOptions: Array<string> | null) => void;
4
4
  initialNumOptions: number;
5
5
  isCollapsed: boolean;
6
+ /**
7
+ * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render
8
+ * @returns boolean
9
+ */
10
+ isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean;
6
11
  }
7
12
  interface UseFilterOptionsListPropsLegacy extends UseFilterOptionsListPropsBase {
8
13
  /** @deprecated Use `facet` instead */
@@ -19,8 +24,8 @@ export default function useFilterOptionsList(props: UseFilterOptionsListProps):
19
24
  isCollapsed: boolean;
20
25
  isShowAll: boolean;
21
26
  setIsShowAll: import("react").Dispatch<import("react").SetStateAction<boolean>>;
22
- optionsToRender: import("../../types").PlpFacetOption[];
23
- setOptionsToRender: import("react").Dispatch<import("react").SetStateAction<import("../../types").PlpFacetOption[]>>;
27
+ optionsToRender: PlpFacetOption[];
28
+ setOptionsToRender: import("react").Dispatch<import("react").SetStateAction<PlpFacetOption[]>>;
24
29
  selectedOptionMap: {};
25
30
  setSelectedOptionMap: import("react").Dispatch<import("react").SetStateAction<{}>>;
26
31
  onOptionSelect: (optionValue: string) => void;
@@ -19,5 +19,10 @@ export interface UseFilterProps {
19
19
  * Per-facet slider step configuration
20
20
  */
21
21
  facetSliderSteps?: Record<string, number>;
22
+ /**
23
+ * Function that takes in a PlpFacet and returns `true` if the facet should be hidden from the final render
24
+ * @returns boolean
25
+ */
26
+ isHiddenFilterFn?: (facet: PlpFacet) => boolean;
22
27
  }
23
28
  export default function useFilter(props: UseFilterProps): UseFilterReturn;
@@ -13,6 +13,11 @@ export interface UseOptionsListProps<T> {
13
13
  * @returns boolean
14
14
  */
15
15
  isHiddenOptionFn?: (option: T) => boolean;
16
+ /**
17
+ * Key name for nested options array (e.g., 'options' for hierarchical facets, 'children' for groups).
18
+ * When provided, filtering will be applied recursively to nested options.
19
+ */
20
+ nestedOptionsKey?: string;
16
21
  }
17
22
  export default function useOptionsList<T>(props: UseOptionsListProps<T>): {
18
23
  initialNumOptions: number;
@@ -17,6 +17,8 @@ export interface ItemFieldGetters {
17
17
  getSwatchPreview: (variation: Variation) => string;
18
18
  getSwatches: (item: Item, retrievePrice: ItemFieldGetters['getPrice'], retrieveSwatchPreview: ItemFieldGetters['getSwatchPreview'], retrieveSalePrice: ItemFieldGetters['getSalePrice'], retrieveRolloverImage: ItemFieldGetters['getRolloverImage']) => SwatchItem[] | undefined;
19
19
  getIsHiddenGroupField: (group: PlpItemGroup) => boolean | undefined;
20
+ getIsHiddenFilterField: (facet: PlpFacet) => boolean | undefined;
21
+ getIsHiddenFilterOptionField: (option: PlpFacetOption) => boolean | undefined;
20
22
  getItemUrl: (item: Item) => string | undefined;
21
23
  }
22
24
  export interface Formatters {
@@ -279,13 +281,13 @@ export interface PlpFacetOption {
279
281
  count: number;
280
282
  displayName: string;
281
283
  value: string;
282
- data: object;
284
+ data: Record<string, any>;
283
285
  range?: ['-inf' | number, 'inf' | number];
284
286
  options?: Array<PlpHierarchicalFacetOption>;
285
287
  }
286
288
  export interface PlpHierarchicalFacetOption extends PlpFacetOption {
287
289
  options: Array<PlpHierarchicalFacetOption>;
288
- data: object & {
290
+ data: Record<string, any> & {
289
291
  parentValue: string | null;
290
292
  };
291
293
  }
@@ -18,6 +18,7 @@ export declare const cnstrcDataAttrs: {
18
18
  zeroResults: string;
19
19
  slCampaignId: string;
20
20
  slCampaignOwner: string;
21
+ resultPage: string;
21
22
  };
22
23
  search: {
23
24
  searchContainer: string;
@@ -1,8 +1,10 @@
1
- import { ItemFieldGetters, Item, SwatchItem, Variation, PlpItemGroup } from '../types';
1
+ import { ItemFieldGetters, Item, SwatchItem, Variation, PlpItemGroup, PlpFacet, PlpFacetOption } from '../types';
2
2
  export declare function getPrice(item: Item | Variation): number;
3
3
  export declare function getSalePrice(item: Item | Variation): number | undefined;
4
4
  export declare function getRolloverImage(item: Item | Variation): string | undefined;
5
5
  export declare function getSwatches(item: Item, retrievePrice: ItemFieldGetters['getPrice'], retrieveSwatchPreview: ItemFieldGetters['getSwatchPreview'], retrieveSalePrice: ItemFieldGetters['getSalePrice'], retrieveRolloverImage: ItemFieldGetters['getRolloverImage']): SwatchItem[] | undefined;
6
6
  export declare function getSwatchPreview(variation: Variation): string;
7
7
  export declare function getIsHiddenGroupField(group: PlpItemGroup): any;
8
+ export declare function getIsHiddenFilterField(facet: PlpFacet): any;
9
+ export declare function getIsHiddenFilterOptionField(option: PlpFacetOption): any;
8
10
  export declare function getItemUrl(item: Item): string | undefined;
@@ -1,2 +1,2 @@
1
- declare const _default: "1.8.4";
1
+ declare const _default: "1.10.0";
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructor-io/constructorio-ui-plp",
3
- "version": "1.8.4",
3
+ "version": "1.10.0",
4
4
  "description": "Constructor PLP UI library for web applications",
5
5
  "author": "Constructor.io Corporation",
6
6
  "license": "MIT",