Skip to content

Commit

Permalink
Update React Sample App according to new Headless State (#54)
Browse files Browse the repository at this point in the history
Update React sample app and tests based on the updated headless state

- update components to use the new vertical and universal state interfaces, and the new `SearchStatusState` and `QueryState`
- update `SearchBar` to manage its own autocomplete results in component state. Added a useAutocomplete hook
  - Note: autocomplete dropdown will flicker between old and recently trigger search results. This will be address through supporting async and non-async in `inputDropdown` in another item
- update `AppliedFilter` and `StaticFilter` to no longer use ID with headless public interface
- as discussed with product, any update to static Filter should reset facet to avoid invalid facet send as part of request. Added call to `resetFacets` on `handleFilterOptionChange`


J=SLAP-1693
TEST=manual

jest tests passed
smoke tested sample-app with new headless version
- see that universal search executed properly
- see that vertical search work with multiple static filters
- see that UI for loading state behave properly with universal/vertical search
- see that autocomplete display options properly
  • Loading branch information
yen-tt committed Nov 17, 2021
1 parent 22d1f6b commit ced7e2c
Show file tree
Hide file tree
Showing 20 changed files with 89 additions and 78 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.2
- @yext/answers-headless@0.1.0-beta.4

These packages each contain the following license and notice below:

Expand Down
16 changes: 8 additions & 8 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.2",
"@yext/answers-headless": "^0.1.0-beta.4",
"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.

6 changes: 3 additions & 3 deletions sample-app/src/components/AlternativeVerticals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ interface Props {
export default function AlternativeVerticals (props: Props): JSX.Element | null {
const { currentVerticalLabel, verticalsConfig, displayAllResults = true } = props;

const alternativeVerticals = useAnswersState(state => state.vertical.alternativeVerticals) || [];
const allResultsForVertical = useAnswersState(state => state.vertical.results?.allResultsForVertical?.verticalResults.results) || [];
const query = useAnswersState(state => state.query.latest);
const alternativeVerticals = useAnswersState(state => state.vertical.noResults?.alternativeVerticals) || [];
const allResultsForVertical = useAnswersState(state => state.vertical.noResults?.allResultsForVertical.results) || [];
const query = useAnswersState(state => state.query.mostRecentSearch);
const actions = useAnswersActions();

const verticalSuggestions = buildVerticalSuggestions(verticalsConfig, alternativeVerticals);
Expand Down
6 changes: 1 addition & 5 deletions sample-app/src/components/AppliedFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,7 @@ function RemovableFilter({ filter }: {filter: DisplayableFilter }): JSX.Element
}

const onRemoveStaticFilterOption = () => {
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.setFilterOption({ ...filter.filter, selected: false });
answersAction.executeVerticalQuery();
}

Expand Down
2 changes: 1 addition & 1 deletion sample-app/src/components/DecoratedAppliedFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ export function DecoratedAppliedFiltersDisplay(props : DecoratedAppliedFiltersCo
export default function DecoratedAppliedFilters(
props : Omit<DecoratedAppliedFiltersConfig, 'appliedQueryFilters'>
): JSX.Element {
const nlpFilters = useAnswersState(state => state.vertical?.results?.verticalResults.appliedQueryFilters) || [];
const nlpFilters = useAnswersState(state => state.vertical?.appliedQueryFilters) || [];
return <DecoratedAppliedFiltersDisplay appliedQueryFilters={nlpFilters} {...props}/>
};
2 changes: 1 addition & 1 deletion sample-app/src/components/InputDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ export default function InputDropdown({
setScreenReaderKey(screenReaderKey + 1);
}}
onClick={() => {
dispatch({ type: 'ShowOptions' });
updateDropdown();
dispatch({ type: 'ShowOptions' });
if (options.length || inputValue) {
setScreenReaderKey(screenReaderKey + 1);
}
Expand Down
4 changes: 2 additions & 2 deletions sample-app/src/components/ResultsCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export function ResultsCountDisplay(props: ResultsCountConfig): JSX.Element {


export default function ResultsCount() {
const resultsCount = useAnswersState(state => state.vertical?.results?.verticalResults.resultsCount) || 0;
const resultsLength = useAnswersState(state => state.vertical?.results?.verticalResults.results.length) || 0;
const resultsCount = useAnswersState(state => state.vertical?.resultsCount) || 0;
const resultsLength = useAnswersState(state => state.vertical?.results?.length) || 0;
const offset = useAnswersState(state => state.vertical?.offset) || 0;
return <ResultsCountDisplay resultsCount={resultsCount} resultsLength={resultsLength} offset={offset}/>;
}
18 changes: 5 additions & 13 deletions sample-app/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useAnswersActions, useAnswersState, StateSelector, AutocompleteResult } from '@yext/answers-headless-react';
import { useAnswersActions, useAnswersState } from '@yext/answers-headless-react';
import InputDropdown from './InputDropdown';
import renderWithHighlighting from './utils/renderWithHighlighting';
import { ReactComponent as MagnifyingGlassIcon } from '../icons/magnifying_glass.svg';
import '../sass/SearchBar.scss';
import '../sass/Autocomplete.scss';
import LoadingIndicator from './LoadingIndicator';
import { useAutocomplete } from '../hooks/useAutocomplete';

const SCREENREADER_INSTRUCTIONS = 'When autocomplete results are available, use up and down arrows to review and enter to select.'

Expand All @@ -19,18 +20,9 @@ interface Props {
*/
export default function SearchBar({ placeholder, isVertical, screenReaderInstructionsId }: Props) {
const answersActions = useAnswersActions();
const query = useAnswersState(state => state.query.query);
const mapStateToAutocompleteResults: StateSelector<AutocompleteResult[] | undefined> = isVertical
? state => state.vertical.autoComplete?.results
: state => state.universal.autoComplete?.results;
const autocompleteResults = useAnswersState(mapStateToAutocompleteResults) || [];
const isLoading = useAnswersState(state => state.vertical.searchLoading || state.universal.searchLoading);

function executeAutocomplete () {
isVertical
? answersActions.executeVerticalAutoComplete()
: answersActions.executeUniversalAutoComplete()
}
const query = useAnswersState(state => state.query.input);
const [ autocompleteResults, executeAutocomplete ] = useAutocomplete(isVertical);
const isLoading = useAnswersState(state => state.searchStatus.isLoading);

function executeQuery () {
isVertical
Expand Down
12 changes: 6 additions & 6 deletions sample-app/src/components/StaticFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ interface FilterOption {

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

function CheckboxFilter({ fieldId, value, label, selected, optionHandler }: CheckBoxProps) {
Expand All @@ -43,11 +42,11 @@ function CheckboxFilter({ fieldId, value, label, selected, optionHandler }: Chec

export default function StaticFilters(props: StaticFiltersProps): JSX.Element {
const answersActions = useAnswersActions();
const { filterCollectionId, options, title } = props;
const { options, title } = props;

const filterCollection = useAnswersState(state => state.filters.static?.[filterCollectionId]);
const selectableFilters = useAnswersState(state => state.filters.static);
const getOptionSelectStatus = (option: FilterOption): boolean => {
const foundFilter = filterCollection?.find(storedSelectableFilter => {
const foundFilter = selectableFilters?.find(storedSelectableFilter => {
const { selected, ...storedFilter } = storedSelectableFilter;
const targetFilter = {
fieldId: option.fieldId,
Expand All @@ -60,7 +59,8 @@ export default function StaticFilters(props: StaticFiltersProps): JSX.Element {
};

const handleFilterOptionChange = (option: Filter, isChecked: boolean) => {
answersActions.setFilterOption({ ...option, selected: isChecked }, filterCollectionId);
answersActions.resetFacets();
answersActions.setFilterOption({ ...option, selected: isChecked });
answersActions.executeVerticalQuery();
}

Expand Down
4 changes: 2 additions & 2 deletions sample-app/src/components/UniversalResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export default function UniversalResults({
verticalConfigs,
appliedFiltersConfig
}: UniversalResultsProps): JSX.Element | null {
const resultsFromAllVerticals = useAnswersState(state => state?.universal?.results?.verticalResults) || [];
const isLoading = useAnswersState(state => state.universal.searchLoading);
const resultsFromAllVerticals = useAnswersState(state => state?.universal?.verticals) || [];
const isLoading = useAnswersState(state => state.searchStatus.isLoading);

if (resultsFromAllVerticals.length === 0) {
return null;
Expand Down
6 changes: 3 additions & 3 deletions sample-app/src/components/VerticalResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ interface VerticalResultsProps {
export default function VerticalResults(props: VerticalResultsProps): JSX.Element | null {
const { displayAllResults = true, ...otherProps } = props;

const verticalResults = useAnswersState(state => state.vertical.results?.verticalResults.results) || [];
const allResultsForVertical = useAnswersState(state => state.vertical.results?.allResultsForVertical?.verticalResults.results) || [];
const isLoading = useAnswersState(state => state.vertical.searchLoading);
const verticalResults = useAnswersState(state => state.vertical.results) || [];
const allResultsForVertical = useAnswersState(state => state.vertical?.noResults?.allResultsForVertical.results) || [];
const isLoading = useAnswersState(state => state.searchStatus.isLoading);

const results = verticalResults.length === 0 && displayAllResults
? allResultsForVertical
Expand Down
19 changes: 19 additions & 0 deletions sample-app/src/hooks/useAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useRef, useState } from "react";
import { AutocompleteResult, useAnswersActions } from '@yext/answers-headless-react';

export function useAutocomplete(isVertical: boolean): [AutocompleteResult[], () => Promise<void>] {
const answersActions = useAnswersActions();
const autocompleteNetworkIds = useRef({ latestRequest: 0, responseInState: 0 });
const [ autocompleteResults, setAutocompleteResults ] = useState<AutocompleteResult[]>([]);
async function executeAutocomplete () {
const requestId = ++autocompleteNetworkIds.current.latestRequest;
const response = isVertical
? await answersActions.executeVerticalAutocomplete()
: await answersActions.executeUniversalAutocomplete();
if (requestId >= autocompleteNetworkIds.current.responseInState) {
setAutocompleteResults(response?.results || []);
autocompleteNetworkIds.current.responseInState = requestId;
}
}
return [ autocompleteResults, executeAutocomplete ]
};
2 changes: 1 addition & 1 deletion sample-app/src/pages/StandardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const navLinks = [
* A LayoutComponent that provides a SearchBar and Navigation tabs to a given page.
*/
const StandardLayout: LayoutComponent = ({ page }) => {
const isVertical = useAnswersState(state => !!state.vertical.key);
const isVertical = useAnswersState(state => !!state.vertical.verticalKey);
return (
<>
<SearchBar
Expand Down
19 changes: 13 additions & 6 deletions sample-app/src/pages/VerticalSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { StandardCard } from '../components/cards/StandardCard';
import { useLayoutEffect } from 'react';
import { useAnswersActions } from '@yext/answers-headless-react';

const staticFilterOptions = [
const countryFilterOptions = [
{
label: 'canada',
fieldId: 'c_employeeCountry',
Expand All @@ -26,7 +26,10 @@ const staticFilterOptions = [
label: 'usa',
fieldId: 'c_employeeCountry',
value: 'United States',
},
}
]

const employeeFilterOptions = [
{
label: 'tech',
fieldId: 'c_employeeDepartment',
Expand All @@ -51,7 +54,8 @@ const facetConfigs = {
}

const staticFiltersGroupLabels = {
c_employeeCountry: 'Employee Country'
c_employeeCountry: 'Employee Country',
c_employeeDepartment: 'Employee Deparment'
}

export default function VerticalSearchPage(props: {
Expand All @@ -71,9 +75,12 @@ export default function VerticalSearchPage(props: {
<div className='VerticalSearchPage'>
<div className='start'>
<StaticFilters
title='~Country and Employee Departments~'
options={staticFilterOptions}
filterCollectionId='someFilterId'
title='~Country~'
options={countryFilterOptions}
/>
<StaticFilters
title='~Employee Departments~'
options={employeeFilterOptions}
/>
<Facets
searchOnChange={true}
Expand Down
2 changes: 1 addition & 1 deletion sample-app/src/sections/StandardSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { StandardCard } from "../components/cards/StandardCard";

const StandardSection: SectionComponent = function (props: SectionConfig): JSX.Element | null {
const { results, verticalKey, cardConfig, viewMore, header } = props;
const latestQuery = useAnswersState(state => state.query.latest);
const latestQuery = useAnswersState(state => state.query.mostRecentSearch);
if (results.length === 0) {
return null;
}
Expand Down
29 changes: 13 additions & 16 deletions sample-app/src/utils/displayablefilterutils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,24 @@ export function getDisplayableAppliedFacets(facets: DisplayableFacet[] | undefin
}

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

Expand Down
4 changes: 2 additions & 2 deletions tests/components/appliedFilters.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('AppliedFilters component work as expected', () => {

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

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

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

0 comments on commit ced7e2c

Please sign in to comment.