Skip to content

Commit

Permalink
Add facets to AppliedFilters (#43)
Browse files Browse the repository at this point in the history
- update DisplayableFilter interface to include additional fields to help identify and unify the different applied filter types (static, nlp, facet)
- update functions in filterUtils to support applied facets

J=1656
TEST=manual

- in vertical page, input query 'virginia', and select some static filters and facet options, see that all 3 label types are displayed
  • Loading branch information
yen-tt committed Oct 22, 2021
1 parent c85e3de commit 0e09edc
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 83 deletions.
4 changes: 2 additions & 2 deletions sample-app/src/components/AppliedFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ function renderFilterLabel(label: string): JSX.Element {
function renderAppliedFilters(filters: Array<DisplayableFilter>): JSX.Element {
const filterElems = filters.map((filter: DisplayableFilter, index: number) => {
return (
<div className="AppliedFilters__filterValue" key={filter.filterLabel}>
<span className="AppliedFilters__filterValueText">{filter.filterLabel}</span>
<div className="AppliedFilters__filterValue" key={filter.label}>
<span className="AppliedFilters__filterValueText">{filter.label}</span>
{index < filters.length - 1 && <span className="AppliedFilters__filterValueComma">,</span>}
</div>
);
Expand Down
17 changes: 4 additions & 13 deletions sample-app/src/components/DecoratedAppliedFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import AppliedFilters from "./AppliedFilters";
import { AppliedQueryFilter } from "@yext/answers-core";
import { useAnswersState } from '@yext/answers-headless-react';
import { GroupedFilters } from '../models/groupedFilters';
import {
getAppliedFilters,
pruneAppliedFilters,
pruneNlpFilters,
createGroupedFilters
} from '../utils/filterutils';
import { getGroupedAppliedFilters } from '../utils/appliedfilterutils';

export interface DecoratedAppliedFiltersConfig {
showFieldNames?: boolean,
Expand All @@ -21,13 +16,9 @@ export interface DecoratedAppliedFiltersConfig {
* Container component for AppliedFilters
*/
export function DecoratedAppliedFiltersDisplay(props : DecoratedAppliedFiltersConfig): JSX.Element {
const { hiddenFields = [], appliedQueryFilters, ...otherProps } = props;
let appliedFilters = getAppliedFilters(useAnswersState(state => state.filters));
appliedFilters = pruneAppliedFilters(appliedFilters, hiddenFields);
let nlpFilters = appliedQueryFilters || [];
nlpFilters = pruneNlpFilters(nlpFilters, appliedFilters, hiddenFields);
const groupedFilters: Array<GroupedFilters> = createGroupedFilters(nlpFilters, appliedFilters);

const { hiddenFields = [], appliedQueryFilters = [], ...otherProps } = props;
const filterState = useAnswersState(state => state.filters);
const groupedFilters: Array<GroupedFilters> = getGroupedAppliedFilters(filterState, appliedQueryFilters, hiddenFields);
return <AppliedFilters appliedFilters={groupedFilters} {...otherProps}/>
}

Expand Down
5 changes: 3 additions & 2 deletions sample-app/src/models/displayableFilter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Filter } from '@yext/answers-core';

export interface DisplayableFilter {
filterType: 'NLP_FILTER' | 'STATIC_FILTER' | 'FACET',
filter: Filter,
filterGroupLabel: string,
filterLabel: string
groupLabel: string,
label: string
}
77 changes: 77 additions & 0 deletions sample-app/src/utils/appliedfilterutils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { AppliedQueryFilter } from "@yext/answers-core";
import { FiltersState } from "@yext/answers-headless/lib/esm/models/slices/filters";
import { DisplayableFilter } from "../models/displayableFilter";
import { GroupedFilters } from "../models/groupedFilters";
import { mapArrayToObject } from "./arrayutils";
import { isDuplicateFilter, flattenFilters } from './filterutils';
import {
getDisplayableStaticFilters,
getDisplayableAppliedFacets,
getDisplayableNlpFilters
} from "./displayablefilterutils";

/**
* Returns a new list of nlp filters with duplicates of other filters and
* filter listed in hiddenFields removed from the given nlp filter list.
*/
function pruneNlpFilters (
nlpFilters: DisplayableFilter[],
appliedFilters: DisplayableFilter[],
hiddenFields: string[]
): DisplayableFilter[] {
const duplicatesRemoved = nlpFilters.filter(nlpFilter => {
const isDuplicate = appliedFilters.find(appliedFilter =>
isDuplicateFilter(nlpFilter.filter, appliedFilter.filter)
);
return !isDuplicate;
});
return pruneAppliedFilters(duplicatesRemoved, hiddenFields);
}

/**
* Returns a new list of applied filters with filter on hiddenFields removed
* from the given applied filter list.
*/
function pruneAppliedFilters(
appliedFilters: DisplayableFilter[], hiddenFields: string[]): DisplayableFilter[] {
return appliedFilters.filter(appliedFilter => {
return !hiddenFields.includes(appliedFilter.filter.fieldId);
});
}

/**
* Combine all of the applied filters into a list of GroupedFilters where each contains a label and
* list of filters under that same label or category.
*/
function createGroupedFilters(
appliedFilters: DisplayableFilter[],
nlpFilters: DisplayableFilter[]
): Array<GroupedFilters> {
const getGroupLabel = (filter: DisplayableFilter) => filter.groupLabel;
const allFilters = [...appliedFilters, ...nlpFilters];
const groupedFilters: Record<string, DisplayableFilter[]> = mapArrayToObject(allFilters, getGroupLabel);
return Object.keys(groupedFilters).map(label => ({
label: label,
filters: groupedFilters[label]
}));
}

/**
* Process all applied filter types (facets, static filters, and nlp filters) by removing
* duplicates and specified hidden fields, and grouped the applied filters into categories.
*/
export function getGroupedAppliedFilters(
appliedFiltersState: FiltersState,
nlpFilters: AppliedQueryFilter[],
hiddenFields: string[]
): Array<GroupedFilters> {
const displayableStaticFilters = getDisplayableStaticFilters(flattenFilters(appliedFiltersState?.static));
const displayableFacets = getDisplayableAppliedFacets(appliedFiltersState?.facets);
const displayableNlpFilters = getDisplayableNlpFilters(nlpFilters);

const appliedFilters = [...displayableStaticFilters, ...displayableFacets];
const prunedAppliedFilters = pruneAppliedFilters(appliedFilters, hiddenFields);
const prunedNlpFilters = pruneNlpFilters (displayableNlpFilters, prunedAppliedFilters, hiddenFields);

return createGroupedFilters(appliedFilters, prunedNlpFilters);
}
59 changes: 59 additions & 0 deletions sample-app/src/utils/displayablefilterutils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { AppliedQueryFilter, Filter, DisplayableFacet } from '@yext/answers-core';
import { DisplayableFilter } from '../models/displayableFilter';
import { getFilterDisplayValue } from './filterutils';

/**
* Convert a list of facets to DisplayableFilter format with only selected facets returned.
*/
export function getDisplayableAppliedFacets(facets: DisplayableFacet[] | undefined): DisplayableFilter[] {
let appliedFacets: DisplayableFilter[] = [];
facets?.forEach(facet => {
facet.options.forEach(option => {
if(option.selected) {
appliedFacets.push({
filterType: 'FACET',
filter: {
fieldId: facet.fieldId,
matcher: option.matcher,
value: option.value
},
groupLabel: facet.displayName,
label: option.displayName
});
}
});
});
return appliedFacets;
}

/**
* Convert a list of static filters to DisplayableFilter format.
*/
export function getDisplayableStaticFilters(filters: Filter[]): DisplayableFilter[] {
let appliedStaticFilters: DisplayableFilter[] = [];
filters?.forEach(filter => {
appliedStaticFilters.push({
filterType: 'STATIC_FILTER',
filter: filter,
groupLabel: filter.fieldId,
label: getFilterDisplayValue(filter),
});
});
return appliedStaticFilters;
}

/**
* Convert a list of nlp filters to DisplayableFilter format.
*/
export function getDisplayableNlpFilters(filters: AppliedQueryFilter[]): DisplayableFilter[] {
let appliedNlpFilters: DisplayableFilter[] = [];
filters?.forEach(filter => {
appliedNlpFilters.push({
filterType: 'NLP_FILTER',
filter: filter.filter,
groupLabel: filter.displayKey,
label: filter.displayValue,
});
});
return appliedNlpFilters;
}
89 changes: 23 additions & 66 deletions sample-app/src/utils/filterutils.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import { AppliedQueryFilter, CombinedFilter, Filter } from '@yext/answers-core';
import { FiltersState } from '@yext/answers-headless/lib/esm/models/slices/filters';
import { mapArrayToObject } from '../utils/arrayutils';
import { GroupedFilters } from '../models/groupedFilters';
import { NearFilterValue, CombinedFilter, Filter } from '@yext/answers-core';

/**
* Check if the object follows NearFilterValue interface
*/
export function isNearFilterValue(obj: Object): obj is NearFilterValue {
return 'radius' in obj && 'lat' in obj && 'long' in obj;
}

/**
* Get a filter's display value or label in string format
*/
export function getFilterDisplayValue(filter: Filter): string {
const value = filter.value;
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value.toString();
}
if (isNearFilterValue(value)) {
return `within ${value.radius}m radius`;
}
throw Error('unrecognized filter value type');
}


/**
* Check if the object follows CombinedFilter interface
Expand Down Expand Up @@ -42,65 +61,3 @@ export function isDuplicateFilter(thisFilter: Filter, otherFilter: Filter): bool
}
return true;
}

/**
* Returns a new list of nlp filters with duplicates of other filters and
* filter listed in hiddenFields removed from the given nlp filter list.
*/
export function pruneNlpFilters (nlpFilters: AppliedQueryFilter[], appliedFilters: Filter[],
hiddenFields: string[]): AppliedQueryFilter[] {
const duplicatesRemoved = nlpFilters.filter(nlpFilter => {
const isDuplicate = appliedFilters.find(appliedFilter =>
isDuplicateFilter(nlpFilter.filter, appliedFilter)
);
return !isDuplicate;
});
return duplicatesRemoved.filter(nlpFilter => {
return !hiddenFields.includes(nlpFilter.filter.fieldId);
});
}

/**
* Returns a new list of applied filters with filter on hiddenFields removed
* from the given nlp filter list.
*/
export function pruneAppliedFilters(appliedFilters: Filter[], hiddenFields: string[]): Filter[] {
return appliedFilters.filter(filter => {
return !hiddenFields.includes(filter.fieldId);
});
}

/**
* Combine all of the applied filters into a list of GroupedFilters where each contains a label and
* list of filters under that same label or category. All filters will convert to DisplayableFilter format
* where displayValue will be used in the JSX element construction.
*/
export function createGroupedFilters(nlpFilters: AppliedQueryFilter[], appliedFilters: Filter[]): Array<GroupedFilters> {
const getFieldName = (filter: Filter) => filter.fieldId;
const getNlpFieldName = (filter: AppliedQueryFilter) => filter.displayKey;
const getDisplayableAppliedFilter = (filter: Filter) => ({
filter: filter,
filterGroupLabel: filter.fieldId,
filterLabel: filter.value
});
const getDisplayableNlpFilter = (filter: AppliedQueryFilter, index: number) => ({
filter: filter.filter,
filterGroupLabel: filter.displayKey,
filterLabel: filter.displayValue
});

let groupedFilters = mapArrayToObject(appliedFilters, getFieldName, getDisplayableAppliedFilter);
groupedFilters = mapArrayToObject(nlpFilters, getNlpFieldName, getDisplayableNlpFilter, groupedFilters);
return Object.keys(groupedFilters).map(label => ({
label: label,
filters: groupedFilters[label]
}));
}

/**
* Restructure and combine static filters from given FiltersState into a list of Filter objects
*/
export function getAppliedFilters(appliedFiltersState: FiltersState | undefined): Array<Filter> {
const appliedStaticFilters = flattenFilters(appliedFiltersState?.static);
return appliedStaticFilters;
}

0 comments on commit 0e09edc

Please sign in to comment.