Skip to content

Commit

Permalink
Refactor StaticFilters (#50)
Browse files Browse the repository at this point in the history
Refactor StaticFilters and update AppliedFilters to follow the new public interface from answers-headless. 

- update StaticFilters to be a functional component
- update StaticFilters to use new interface from answers-headless changes in [this PR](yext/search-headless#42) instead of managing its own filter state.
- update filter utils to reflect the new interfaces from answers-headless
- update AppliedFilters to use answers-headless `setFilterOption` function instead of performing a click event


J=SLAP-1661
TEST=manual

- hook to local answers-headless changes, and start sample-app. See that filters work accordingly.
  • Loading branch information
yen-tt committed Nov 8, 2021
1 parent 7ea865e commit 9ec7dbb
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 118 deletions.
2 changes: 1 addition & 1 deletion THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ The following NPM packages may be included in this product:

- @yext/answers-core@1.3.2
- @yext/answers-headless-react@0.4.0-beta.0
- @yext/answers-headless@0.1.0-beta.0
- @yext/answers-headless@0.1.0-beta.2

These packages each contain the following license and notice below:

Expand Down
42 changes: 35 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"dependencies": {
"@reduxjs/toolkit": "^1.6.2",
"@types/react": "^17.0.15",
"@yext/answers-headless": "^0.1.0-beta.0",
"@yext/answers-headless": "^0.1.0-beta.2",
"typescript": "^4.3.5"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions sample-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion sample-app/src/components/AppliedFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ function RemovableFilter({ filter }: {filter: DisplayableFilter }): JSX.Element
}

const onRemoveStaticFilterOption = () => {
document.getElementById(`${filter.filter.fieldId + "_" + filter.filter.value}`)?.click();
if (!filter.filterCollectionId) {
console.error(`Undefined filter collection id. Unable to remove this filter in AppliedFilters component:\n${JSON.stringify(filter)}`);
return;
}
answersAction.setFilterOption({ ...filter.filter, selected: false }, filter.filterCollectionId);
answersAction.executeVerticalQuery();
}

const onRemoveFilter = filter.filterType === 'FACET' ? onRemoveFacetOption : onRemoveStaticFilterOption;
Expand Down
147 changes: 58 additions & 89 deletions sample-app/src/components/StaticFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import React, { Fragment } from 'react';
import { Filter, CombinedFilter, FilterCombinator, Matcher } from '@yext/answers-core';
import { AnswersHeadlessContext } from '@yext/answers-headless-react';
import { Fragment } from 'react';
import { Filter, Matcher } from '@yext/answers-core';
import { useAnswersActions, useAnswersState } from '@yext/answers-headless-react';
import { isDuplicateFilter } from '../utils/filterutils';

interface CheckBoxProps {
fieldId: string,
value: string,
label: string,
selected: boolean,
optionHandler: Function
}
interface FilterBoxProps {
options: {
fieldId: string,
value: string,
label: string
}[],
title: string
}

interface FiltersState {
[fieldId: string]: Filter[]
interface FilterOption {
fieldId: string,
value: string,
label: string
}
interface State {
filtersState: FiltersState

interface StaticFiltersProps {
options: FilterOption[],
title: string,
filterCollectionId: string
}

function CheckboxFilter({ fieldId, value, label, optionHandler }: CheckBoxProps) {
function CheckboxFilter({ fieldId, value, label, selected, optionHandler }: CheckBoxProps) {
const filter = {
fieldId: fieldId,
matcher: Matcher.Equals,
Expand All @@ -34,85 +33,55 @@ function CheckboxFilter({ fieldId, value, label, optionHandler }: CheckBoxProps)
return (
<Fragment>
<label htmlFor={id}>{label}</label>
<input type="checkbox" id={id} onChange={evt => optionHandler(filter, evt.target.checked)}/>
<input
type="checkbox"
id={id}
checked={selected}
onChange={evt => optionHandler(filter, evt.target.checked)}
/>
</Fragment>
);
}

export default class StaticFilters extends React.Component<FilterBoxProps, State> {
constructor(props: FilterBoxProps) {
super(props)
const filtersState: FiltersState = {}
props.options.forEach(option => {
filtersState[option.fieldId] = []
})
this.state = { filtersState };
}

handleOptionSelection = (filter: Filter, isChecked: boolean) => {
const filtersState = this.state.filtersState
const filters = filtersState[filter.fieldId]

isChecked
? filtersState[filter.fieldId] = [...filters, filter]
: filtersState[filter.fieldId] = filters.filter(filterOption => filterOption.value !== filter.value)
this.setState({
filtersState: filtersState
}, () => {
this.setFilters()
})
}
export default function StaticFilters(props: StaticFiltersProps): JSX.Element {
const answersActions = useAnswersActions();
const { filterCollectionId, options, title } = props;

setFilters() {
const formattedFilter = formatFilters(this.state.filtersState)
this.context.setFilter(formattedFilter)
this.context.executeVerticalQuery();
}
const filterCollection = useAnswersState(state => state.filters.static?.[filterCollectionId]);
const getOptionSelectStatus = (option: FilterOption): boolean => {
const foundFilter = filterCollection?.find(storedSelectableFilter => {
const { selected, ...storedFilter } = storedSelectableFilter;
const targetFilter = {
fieldId: option.fieldId,
matcher: Matcher.Equals,
value: option.value
};
return isDuplicateFilter(storedFilter, targetFilter);
});
return !!foundFilter && foundFilter.selected;
};

render() {
return (
<fieldset>
<legend>{this.props.title}</legend>
<div>
{this.props.options.map((option, index) => (
<div key={index}>
<CheckboxFilter
fieldId={option.fieldId}
value={option.value}
label={option.label}
optionHandler={this.handleOptionSelection}
/>
</div>
))}
</div>
</fieldset>
);
const handleFilterOptionChange = (option: Filter, isChecked: boolean) => {
answersActions.setFilterOption({ ...option, selected: isChecked }, filterCollectionId);
answersActions.executeVerticalQuery();
}
}

function formatFilters(filtersState: FiltersState): Filter | CombinedFilter | null {
let fieldIds = Object.keys(filtersState).filter(fieldId => filtersState[fieldId].length > 0)
if (fieldIds.length === 0) {
return null
}
let filtersArrays = fieldIds.map(fieldId => filtersState[fieldId])
if (filtersArrays.length === 1) {
return formatOrFilters(filtersArrays[0])
}
return {
combinator: FilterCombinator.AND,
filters: filtersArrays.map(filter => formatOrFilters(filter))
}
}

function formatOrFilters(filters: Filter[]) {
if (filters.length === 1) {
return filters[0]
}
return {
combinator: FilterCombinator.OR,
filters: filters
}
return (
<fieldset>
<legend>{title}</legend>
<div>
{options.map((option, index) => (
<div key={index}>
<CheckboxFilter
fieldId={option.fieldId}
value={option.value}
label={option.label}
selected={getOptionSelectStatus(option)}
optionHandler={handleFilterOptionChange}
/>
</div>
))}
</div>
</fieldset>
);
}

StaticFilters.contextType = AnswersHeadlessContext;
3 changes: 2 additions & 1 deletion sample-app/src/models/displayableFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export interface DisplayableFilter {
filterType: 'NLP_FILTER' | 'STATIC_FILTER' | 'FACET',
filter: Filter,
groupLabel: string,
label: string
label: string,
filterCollectionId?: string
}
1 change: 1 addition & 0 deletions sample-app/src/pages/VerticalSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default function VerticalSearchPage(props: {
<StaticFilters
title='~Country and Employee Departments~'
options={staticFilterOptions}
filterCollectionId='someFilterId'
/>
<Facets
searchOnChange={true}
Expand Down
4 changes: 2 additions & 2 deletions sample-app/src/utils/appliedfilterutils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FiltersState } from "@yext/answers-headless/lib/esm/models/slices/filte
import { DisplayableFilter } from "../models/displayableFilter";
import { GroupedFilters } from "../models/groupedFilters";
import { mapArrayToObject } from "./arrayutils";
import { isDuplicateFilter, flattenFilters } from './filterutils';
import { isDuplicateFilter } from './filterutils';
import {
getDisplayableStaticFilters,
getDisplayableAppliedFacets,
Expand Down Expand Up @@ -66,7 +66,7 @@ export function getGroupedAppliedFilters(
hiddenFields: string[],
staticFiltersGroupLabels: Record<string, string>
): Array<GroupedFilters> {
const displayableStaticFilters = getDisplayableStaticFilters(flattenFilters(appliedFiltersState?.static), staticFiltersGroupLabels);
const displayableStaticFilters = getDisplayableStaticFilters(appliedFiltersState?.static, staticFiltersGroupLabels);
const displayableFacets = getDisplayableAppliedFacets(appliedFiltersState?.facets);
const displayableNlpFilters = getDisplayableNlpFilters(nlpFilters);

Expand Down
29 changes: 18 additions & 11 deletions sample-app/src/utils/displayablefilterutils.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppliedQueryFilter, Filter, DisplayableFacet } from '@yext/answers-core';
import { AppliedQueryFilter, DisplayableFacet } from '@yext/answers-core';
import { SelectableFilter } from '@yext/answers-headless/lib/esm/models/utils/selectablefilter';
import { DisplayableFilter } from '../models/displayableFilter';
import { getFilterDisplayValue } from './filterutils';

Expand Down Expand Up @@ -27,21 +28,27 @@ export function getDisplayableAppliedFacets(facets: DisplayableFacet[] | undefin
}

/**
* Convert a list of static filters to DisplayableFilter format.
* Convert a map of static filters to DisplayableFilter format with only selected filters returned.
*/
export function getDisplayableStaticFilters(
filters: Filter[],
staticFilters: Record<string, SelectableFilter[]> | null | undefined,
groupLabels: Record<string, string>
): DisplayableFilter[] {
let appliedStaticFilters: DisplayableFilter[] = [];
filters?.forEach(filter => {
appliedStaticFilters.push({
filterType: 'STATIC_FILTER',
filter: filter,
groupLabel: groupLabels?.[filter.fieldId] || filter.fieldId,
label: getFilterDisplayValue(filter),
});
});
staticFilters && Object.entries(staticFilters).forEach(([filterCollectionId, filterCollection]) =>
filterCollection.forEach(selectableFilter => {
const { selected, ...filter } = selectableFilter;
if (selected) {
appliedStaticFilters.push({
filterType: 'STATIC_FILTER',
filter: filter,
groupLabel: groupLabels?.[filter.fieldId] || filter.fieldId,
label: getFilterDisplayValue(filter),
filterCollectionId: filterCollectionId
});
}
})
);
return appliedStaticFilters;
}

Expand Down
7 changes: 4 additions & 3 deletions tests/components/appliedFilters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ describe('AppliedFilters component work as expected', () => {
const mockedFilter = {
fieldId: 'c_employeeCountry',
matcher: Matcher.Equals,
value: 'United States'
value: 'United States',
selected: true
};

const MockedStaticFilter = () => {
const onChange = useCallback(() => {
answers.setFilter(null);
answers.setStaticFilters({ someId: [] });
}, []);
return <button id='c_employeeCountry_United States' onClick={onChange}></button>;
};
Expand All @@ -35,7 +36,7 @@ describe('AppliedFilters component work as expected', () => {

act(() => answers.setQuery('someQuery'));
await act( () => answers.executeVerticalQuery());
act(() => answers.setFilter(mockedFilter));
act(() => answers.setStaticFilters({ someId: [mockedFilter] }));

let filterLabels = container.getElementsByClassName('AppliedFilters__filterValueText');
expect(filterLabels.length).toBe(1);
Expand Down

0 comments on commit 9ec7dbb

Please sign in to comment.