import { NearFilterValue, FieldValueFilter, NumberRangeValue, Matcher, SearchActions, DisplayableFacet, SelectableStaticFilter, StaticFilter } from '@yext/search-headless-react'; import isEqual from 'lodash/isEqual.js'; import { isNumberRangeFilter } from '../models/NumberRangeFilter'; import { SelectableFieldValueFilter } from '../models/SelectableFieldValueFilter'; /** * Check if the object follows NearFilterValue interface. */ export function isNearFilterValue(obj: unknown): obj is NearFilterValue { return typeof obj === 'object' && !!obj && 'radius' in obj && 'lat' in obj && 'long' in obj; } /** * Checks if the object follows the NumberRangeValue interface. */ export function isNumberRangeValue(obj: unknown): obj is NumberRangeValue { return typeof obj === 'object' && !!obj && ('start' in obj || 'end' in obj); } /** * Checks if the facet is a string facet with string options. */ export function isStringFacet(facet: DisplayableFacet): boolean { return facet.options.length > 0 && typeof facet.options[0].value === 'string'; } /** * Checks if the facet is a numerical facet with number range filter options. */ export function isNumericalFacet(facet: DisplayableFacet): boolean { return facet.options.length > 0 && facet.options.some(option => isNumberRangeFilter(option)); } /** * Returns true if the two given field value filters are the same. */ export function isDuplicateFieldValueFilter( thisFilter: FieldValueFilter, otherFilter: FieldValueFilter ): boolean { if (thisFilter.fieldId !== otherFilter.fieldId) { return false; } if (thisFilter.matcher !== otherFilter.matcher) { return false; } return isEqual(thisFilter.value, otherFilter.value); } /** * Returns true if the two given static filters are the same. */ export function isDuplicateStaticFilter(thisFilter: StaticFilter, otherFilter: StaticFilter): boolean { if (thisFilter.kind === 'fieldValue') { return otherFilter.kind === 'fieldValue' ? isDuplicateFieldValueFilter(thisFilter, otherFilter) : false; } if (otherFilter.kind === 'fieldValue') { return false; } return thisFilter.combinator === otherFilter.combinator && thisFilter.filters.length === otherFilter.filters.length && thisFilter.filters.every(t => otherFilter.filters.some(o => isDuplicateStaticFilter(t, o))) && otherFilter.filters.every(o => thisFilter.filters.some(t => isDuplicateStaticFilter(o, t))); } /** * Finds the {@link SelectableFieldValueFilter} from the list provided that matches * the given {@link FieldValueFilter}. If no matching {@link SelectableFieldValueFilter} * can be found, undefined is returned. * * @param filter - The filter to match against * @param selectableFilters - The list of {@link SelectableFieldValueFilter}s to search against */ export function findSelectableFieldValueFilter( filter: FieldValueFilter, selectableFilters: SelectableFieldValueFilter[] ): SelectableFieldValueFilter | undefined { return selectableFilters.find(selectableFilter => { const { displayName: _, ...storedFilter } = selectableFilter; return isDuplicateFieldValueFilter(storedFilter, filter); }); } /** * Creates a number range value based on a min and max from user input. */ export function parseNumberRangeInput(minRangeInput: string, maxRangeInput: string): NumberRangeValue { const minRange = parseNumber(minRangeInput); const maxRange = parseNumber(maxRangeInput); return { ...(minRange !== undefined && { start: { matcher: Matcher.GreaterThanOrEqualTo, value: minRange } }), ...(maxRange !== undefined && { end: { matcher: Matcher.LessThanOrEqualTo, value: maxRange } }) }; } /** * Given a string, returns the corresponding number, or undefined if it is NaN. */ function parseNumber(num: string) { const parsedNum = parseFloat(num); if (isNaN(parsedNum)) { return undefined; } return parsedNum; } /** * Deselects the selected static number range filters in state. If fieldIds are * provided, only filters corresponding to one of those fieldIds are deselected. * Otherwise, all selected filters are deselected. */ export function clearStaticRangeFilters(searchActions: SearchActions, fieldIds?: Set) { const selectedStaticRangeFilters = searchActions.state?.filters?.static?.filter(filter => isNumberRangeFilter(filter) && filter.selected === true && (!fieldIds || fieldIds.has(filter.fieldId)) ); selectedStaticRangeFilters?.forEach(filter => { searchActions.setFilterOption({ ...filter, selected: false }); }); } /** * Returns a set of fieldIds of the numerical facets in state that have at * least one option selected. */ export function getSelectedNumericalFacetFields(searchActions: SearchActions): Set { const selectedNumericalFacets = searchActions.state.filters.facets?.filter( f => isNumericalFacet(f) && f.options.some(o => o.selected) ) ?? []; return new Set(selectedNumericalFacets.map(f => f.fieldId)); } /** * Goes through the provided static filters and returns all that are field value * filters. * * @param staticFilters - The list of {@link SelectableStaticFilter}s to search through * @returns All filters that are {@link SelectableFieldValueFilter}s */ export function getSelectableFieldValueFilters( staticFilters: SelectableStaticFilter[] ): SelectableFieldValueFilter[] { return staticFilters.map(s => { const { filter: { kind, ...filterFields }, ...displayFields } = s; if (kind === 'fieldValue') { return { ...displayFields, ...filterFields }; } return undefined; }).filter((s): s is SelectableFieldValueFilter => !!s); } /** * Creates the filter's display name based on the number range. */ export function getDefaultFilterDisplayName(numberRange: NumberRangeValue) { const start = numberRange.start; const end = numberRange.end; if (start && end) { return `${start.value} - ${end.value}`; } else if (start && !end) { return `Over ${start.value}`; } else if (end && !start) { return `Up to ${end.value}`; } return ''; }