From 0e09edcdceb2a2d6d350eb34792577e1f8efc3a0 Mon Sep 17 00:00:00 2001 From: Yen Truong <36055303+yen-tt@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:24:53 -0400 Subject: [PATCH] Add facets to AppliedFilters (#43) - 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 --- sample-app/src/components/AppliedFilters.tsx | 4 +- .../components/DecoratedAppliedFilters.tsx | 17 +--- sample-app/src/models/displayableFilter.ts | 5 +- sample-app/src/utils/appliedfilterutils.tsx | 77 ++++++++++++++++ .../src/utils/displayablefilterutils.tsx | 59 ++++++++++++ sample-app/src/utils/filterutils.tsx | 89 +++++-------------- 6 files changed, 168 insertions(+), 83 deletions(-) create mode 100644 sample-app/src/utils/appliedfilterutils.tsx create mode 100644 sample-app/src/utils/displayablefilterutils.tsx diff --git a/sample-app/src/components/AppliedFilters.tsx b/sample-app/src/components/AppliedFilters.tsx index 6ed0b20f..d182d227 100644 --- a/sample-app/src/components/AppliedFilters.tsx +++ b/sample-app/src/components/AppliedFilters.tsx @@ -40,8 +40,8 @@ function renderFilterLabel(label: string): JSX.Element { function renderAppliedFilters(filters: Array): JSX.Element { const filterElems = filters.map((filter: DisplayableFilter, index: number) => { return ( -
- {filter.filterLabel} +
+ {filter.label} {index < filters.length - 1 && ,}
); diff --git a/sample-app/src/components/DecoratedAppliedFilters.tsx b/sample-app/src/components/DecoratedAppliedFilters.tsx index f6b81879..7e32195f 100644 --- a/sample-app/src/components/DecoratedAppliedFilters.tsx +++ b/sample-app/src/components/DecoratedAppliedFilters.tsx @@ -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, @@ -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 = createGroupedFilters(nlpFilters, appliedFilters); - + const { hiddenFields = [], appliedQueryFilters = [], ...otherProps } = props; + const filterState = useAnswersState(state => state.filters); + const groupedFilters: Array = getGroupedAppliedFilters(filterState, appliedQueryFilters, hiddenFields); return } diff --git a/sample-app/src/models/displayableFilter.ts b/sample-app/src/models/displayableFilter.ts index 29d0fe12..8d427fad 100644 --- a/sample-app/src/models/displayableFilter.ts +++ b/sample-app/src/models/displayableFilter.ts @@ -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 } diff --git a/sample-app/src/utils/appliedfilterutils.tsx b/sample-app/src/utils/appliedfilterutils.tsx new file mode 100644 index 00000000..5fa6f76a --- /dev/null +++ b/sample-app/src/utils/appliedfilterutils.tsx @@ -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 { + const getGroupLabel = (filter: DisplayableFilter) => filter.groupLabel; + const allFilters = [...appliedFilters, ...nlpFilters]; + const groupedFilters: Record = 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 { + 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); +} \ No newline at end of file diff --git a/sample-app/src/utils/displayablefilterutils.tsx b/sample-app/src/utils/displayablefilterutils.tsx new file mode 100644 index 00000000..80286867 --- /dev/null +++ b/sample-app/src/utils/displayablefilterutils.tsx @@ -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; +} \ No newline at end of file diff --git a/sample-app/src/utils/filterutils.tsx b/sample-app/src/utils/filterutils.tsx index 6b486cba..ba59c9ce 100644 --- a/sample-app/src/utils/filterutils.tsx +++ b/sample-app/src/utils/filterutils.tsx @@ -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 @@ -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 { - 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 { - const appliedStaticFilters = flattenFilters(appliedFiltersState?.static); - return appliedStaticFilters; -} \ No newline at end of file