Skip to content

[CDX-326] Allow blacklisting facets from filters#207

Merged
esezen merged 15 commits intomainfrom
cdx-326-plp-ui-feature-allow-blacklisting-facets-from-filters
Mar 3, 2026
Merged

[CDX-326] Allow blacklisting facets from filters#207
esezen merged 15 commits intomainfrom
cdx-326-plp-ui-feature-allow-blacklisting-facets-from-filters

Conversation

@esezen
Copy link
Contributor

@esezen esezen commented Jan 28, 2026

Pull Request Checklist

Before you submit a pull request, please make sure you have to following:

  • I have added or updated TypeScript types for my changes, ensuring they are compatible with the existing codebase.
  • I have added JSDoc comments to my TypeScript definitions for improved documentation.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have added any necessary documentation (if appropriate).
  • [ x I have made sure my PR is up-to-date with the main branch.

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no API changes)
  • Documentation content changes
  • TypeScript type definitions update
  • Other... Please describe:

Clone of #192

Copilot AI review requested due to automatic review settings January 28, 2026 12:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds functionality to allow facets and facet options to be hidden from the filter UI through three methods: custom functions, metadata flags, or custom field getters. This enables conditional rendering of filters on a per-page basis (e.g., showing a facet on search results but hiding it on category pages).

Changes:

  • Added isHiddenFilterFn and isHiddenFilterOptionFn props to filter components for custom hiding logic
  • Introduced getIsHiddenFilterField and getIsHiddenFilterOptionField field getters to support metadata-based hiding via cio_plp_hidden flag
  • Enhanced useOptionsList hook with recursive filtering capability for nested options in hierarchical structures

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/utils/itemFieldGetters.ts Added field getter functions for detecting hidden filters and filter options
src/types.ts Updated type definitions to support new field getters and changed data type from object to Record<string, any>
src/stories/components/Filters/Filters.stories.tsx Added story examples demonstrating the three methods for hiding filters
src/stories/components/Filters/Code Examples.mdx Added comprehensive documentation with code examples for the hiding functionality
src/hooks/useOptionsList.ts Added recursive filtering support for nested options via nestedOptionsKey parameter
src/hooks/useGroups.ts Enabled recursive filtering for group children by passing nestedOptionsKey: 'children'
src/hooks/useFilter.ts Implemented facet filtering logic using both custom function and metadata-based approaches
src/components/Filters/UseFilterOptionsList.tsx Added support for hiding individual filter options using custom functions and metadata
src/components/Filters/Filters.tsx Passed through isHiddenFilterOptionFn prop to child components
src/components/Filters/FilterGroup.tsx Passed through isHiddenFilterOptionFn prop to FilterOptionsList
spec/hooks/useGroups/useGroups.test.js Added tests for recursive filtering of nested group children
spec/hooks/useFilter/useFilter.test.js Added comprehensive tests for facet hiding functionality
spec/components/Filters/Filters.test.jsx Added integration tests for filter and filter option hiding

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

displayName: string;
value: string;
data: object;
data: Record<string, any>;
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any bypasses TypeScript's type safety. Consider using unknown instead, which provides type safety while still allowing flexibility: Record<string, unknown>.

Copilot uses AI. Check for mistakes.
export interface PlpHierarchicalFacetOption extends PlpFacetOption {
options: Array<PlpHierarchicalFacetOption>;
data: object & { parentValue: string | null };
data: Record<string, any> & { parentValue: string | null };
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any bypasses TypeScript's type safety. Consider using unknown instead: Record<string, unknown> & { parentValue: string | null }.

Suggested change
data: Record<string, any> & { parentValue: string | null };
data: Record<string, unknown> & { parentValue: string | null };

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +42
const nestedOptions = (option as Record<string, any>)[nestedKey];
if (Array.isArray(nestedOptions) && nestedOptions.length > 0) {
return {
...option,
[nestedKey]: filterOptionsRecursively(nestedOptions, isHiddenFn, nestedKey),
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any bypasses TypeScript's type safety. Consider using unknown instead and adding proper type guards: (option as Record<string, unknown>)[nestedKey].

Suggested change
const nestedOptions = (option as Record<string, any>)[nestedKey];
if (Array.isArray(nestedOptions) && nestedOptions.length > 0) {
return {
...option,
[nestedKey]: filterOptionsRecursively(nestedOptions, isHiddenFn, nestedKey),
const nestedOptions = (option as Record<string, unknown>)[nestedKey];
if (Array.isArray(nestedOptions) && nestedOptions.length > 0) {
return {
...option,
[nestedKey]: filterOptionsRecursively(nestedOptions as Array<T>, isHiddenFn, nestedKey),

Copilot uses AI. Check for mistakes.
@constructor-claude-bedrock

This comment has been minimized.

Alexey-Pavlov
Alexey-Pavlov previously approved these changes Feb 5, 2026
Copy link
Contributor

@Alexey-Pavlov Alexey-Pavlov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks a lot for handling this!

Mudaafi
Mudaafi previously approved these changes Feb 19, 2026
Copy link
Contributor

@Mudaafi Mudaafi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, just a couple of doc suggestions

Comment on lines +180 to +182
### Method 3: Custom Field Getter

You can also customize the metadata field used for hiding by providing a custom `itemFieldGetters` to `CioPlpProvider`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably important to mention the field/function needs to return a boolean: true -> hide, false -> render

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea


> **Note:** This functionality is intended for **conditional rendering per page**. For example, showing a facet on search results but hiding it on category pages. If a facet or option should be hidden across your entire site, use the hidden facets feature in the Constructor.io dashboard or API instead. Hidden facets configured through the dashboard are excluded from API responses entirely, which is more efficient than client-side filtering.

### Method 1: Custom Function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to method 3. Generally, this method should be the least used by consumers compared to the other 2 just by virtue of how much customization this requires

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart. 47fd2f0

Co-authored-by: Ahmad Mudaafi' <ahmad.mudaafi@constructor.io>
@esezen esezen dismissed stale reviews from Mudaafi and Alexey-Pavlov via 0d68194 February 20, 2026 14:04
@constructor-claude-bedrock

This comment has been minimized.

@esezen esezen requested a review from Mudaafi March 2, 2026 16:59
@constructor-claude-bedrock
Copy link

Code Review Results

✅ Strengths

Well-structured feature that extends the existing isHiddenGroupFn / getIsHiddenGroupField pattern consistently across facets and facet options, with good test coverage, JSDoc comments, and helpful Storybook documentation.

🚨 Critical Issues

  • [File: src/hooks/useOptionsList.ts Line: 38] The filterOptionsRecursively function uses Record<string, any> casting to access nested options by key. This defeats TypeScript's type safety — a runtime access on a completely unrelated type would silently succeed. Consider constraining the generic T with a structural bound when nestedOptionsKey is provided, or overloading the function signature, e.g.:

    function filterOptionsRecursively<T extends Record<string, unknown>>(
      options: Array<T>,
      isHiddenFn: (option: T) => boolean,
      nestedKey: keyof T,
    ): Array<T>

    At minimum, remove the any from the cast (Record<string, unknown> is safer).

  • [File: src/components/Filters/UseFilterOptionsList.tsx Line: 34] useCioPlpContext() is called without a null-check, yet the same file's context consumers (useFilter, useGroups) guard against a missing context with a thrown error. If useFilterOptionsList is ever called outside the provider (e.g. in isolated unit tests), this will throw an uncaught TypeError. Add the same guard that exists in the sibling hooks, or rely on useCioPlpContext's own throw — but verify that contract is guaranteed.

⚠️ Important Issues

  • [File: src/hooks/useOptionsList.ts Lines: 57-63] When nestedOptionsKey is not provided, the flat filter path is used — but when it is provided, the recursive path also applies the top-level filter via filterOptionsRecursively. This means the two code paths are inconsistent: the non-recursive path calls isHiddenOptionFn once per option, while the recursive path calls it again on each nested level. There's no bug here today because the callers that pass nestedOptionsKey also want top-level filtering, but the code comment // Enable recursive filtering for hierarchical facet options passed to useOptionsList in UseFilterOptionsList.tsx:49 suggests the intent is to recurse into nested items only — the top-level filter is already handled. Adding a code comment in useOptionsList clarifying that the recursive path also filters the top level would prevent future confusion.

  • [File: src/hooks/useFilter.ts Line: 53-56] filteredFacets is computed with useMemo but setFilter and clearFilters (lines 58–72) are plain functions that are recreated on every render. This is an inconsistency already present in the codebase, but the new useMemo + useCallback additions make it more visible. setFilter and clearFilters should also be wrapped in useCallback (or the memoization of filteredFacets removed for consistency) to avoid unnecessary downstream re-renders when passing these callbacks as props.

  • [File: spec/hooks/useFilter/useFilter.test.js Lines: 333-385] The test description says "Should prioritize isHiddenFilterFn over getIsHiddenFilterField" but the actual assertion tests that both hide independently (OR logic), not that one takes priority over the other. The test name is misleading. Rename to something like "Should hide facets when either isHiddenFilterFn or getIsHiddenFilterField returns true".

  • [File: spec/components/Filters/Filters.test.jsx Line: 854] The describe block label has a leading space: ' - Facet Blacklisting Tests'. This is a minor formatting issue but it produces inconsistent test output in CI/reporters.

  • [File: src/stories/components/Filters/Code Examples.mdx Line: 85] The note uses "blacklisting" terminology (matching the test describe label). Consider replacing with "blocklisting" or "hiding" for consistency with the prop names (isHiddenFilterFn) and the PR title change from "blacklisting" to a neutral term.

💡 Suggestions

  • [File: src/hooks/useOptionsList.ts Line: 58-62] The nestedOptionsKey branch could also be used for the flat case to avoid maintaining two code paths. Passing nestedOptionsKey and an empty string (or a symbol) could unify the logic, though this is a minor refactor suggestion.

  • [File: src/utils/itemFieldGetters.ts Lines: 60-66] getIsHiddenFilterField and getIsHiddenFilterOptionField lack explicit return type annotations, unlike getIsHiddenGroupField above them (which also lacks one). Adding ): boolean | undefined return types would match the ItemFieldGetters interface declaration and make the contract explicit.

  • [File: spec/hooks/useFilter/useFilter.test.js Lines: 299-320] The customGetIsHiddenFilterField test creates its own CioPlpProvider wrapper inline. Since the test file already imports renderHookWithCioPlp, consider exposing a variant of that utility that accepts itemFieldGetters overrides, keeping wrapper setup DRY across the test suite.

Overall Assessment: ⚠️ Needs Work

Copy link
Contributor

@Mudaafi Mudaafi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks for making the changes

} = props;
const { initialNumOptions, modifyRequestMultipleFilter, isCollapsed, isHiddenFilterOptionFn } =
props;
const facet = 'facet' in props ? props.facet : props.multipleFacet;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strange little accommodation, but I assume it's from merging main

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That one is coming from main to support single select filters

@esezen esezen merged commit 397ecd3 into main Mar 3, 2026
11 of 12 checks passed
@esezen esezen deleted the cdx-326-plp-ui-feature-allow-blacklisting-facets-from-filters branch March 3, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants