From 273caccd647d9f3322ce62ecc2406e6202e11a50 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 25 Aug 2023 11:18:51 -0400 Subject: [PATCH 01/23] Create SearchUIContext SearchUIContext exposes functions for interacting with the elastic search filters and local storage state. This is a first step towards refactoring the search UI. --- components/core/SearchUIContext.jsx | 204 ++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 components/core/SearchUIContext.jsx diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx new file mode 100644 index 0000000..9748a45 --- /dev/null +++ b/components/core/SearchUIContext.jsx @@ -0,0 +1,204 @@ +import { createContext, useContext, useEffect } from 'react' +import { SearchContext } from '@elastic/react-search-ui' + +const SearchUIContext = createContext() + +// Filter type from SearchContext +// +// value filter +// { +// field: "entity_type", +// type: "all", +// values: ["Dataset"] +// } +// +// range filter +// { +// field: "created_timestamp", +// type: "all", +// values: [ +// { from: 1690156800000, to: 1692921599999, name: "created_timestamp" } +// ] +// } +// + +// Filter type returns from useSearchUI +// +// value filter +// { +// field: "entity_type", +// filterType: "all", +// label: "Entity Type", +// type: "value", +// uiType: "checkbox", +// values: ["Dataset"] +// } +// +// range filter +// { +// field: "created_timestamp", +// filterType: "all", +// label: "Created Timestamp", +// type: "range", +// uiType: "date", +// values: [ +// { from: 1690156800000, to: 1692921599999, name: "created_timestamp" } +// ] +// } +// + +export function SearchUIProvider({ children, name = 'new.entities' }) { + const { driver } = useContext(SearchContext) + + useEffect(() => { + if (driver.state.filters && driver.state.filters.length > 0) return + const localFilters = getLocalFilters() + console.log('=====Loading filters from local storage=====', localFilters) + localFilters.forEach((filter) => { + filter.values.forEach((value) => { + addFilter(filter.field, value) + }) + }) + }, []) + + useEffect(() => { + console.log('=====Saving filters to local storage=====', driver.state.filters) + localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) + removeInvalidConditionalFacets() + }, [driver.state]) + + function removeInvalidConditionalFacets() { + const conditionalFacets = driver.searchQuery.conditionalFacets + const filters = getFilters() + filters.forEach((filter) => { + if (conditionalFacets.hasOwnProperty(filter.field)) { + const predicate = conditionalFacets[filter.field] + if (!predicate({ filters })) { + filter.values.forEach((value) => { + removeFilter(filter.field, value) + }) + } + } + }) + } + + // Facets + + function getFacets() { + return driver.searchQuery.facets || {} + } + + function getConditionalFacets() { + return driver.searchQuery.conditionalFacets || {} + } + + function getFacetData() { + return driver.state.facets + } + + // Filters + + function getFilters() { + const facets = driver.searchQuery.facets || {} + return driver.state.filters.map((filter) => { + const facet = facets[filter.field] + return { + ...filter, + type: facet.type, + filterType: facet.filterType, + label: facet.label, + uiType: facet.uiType || 'checkbox' + } + }) + } + + function getFilter(field) { + const filter = driver.state.filters.find((f) => f.field === field) + if (!filter) return null + const facets = driver.searchQuery.facets || {} + const facet = facets[filter.field] + return { + ...filter, + type: facet.type, + filterType: facet.filterType, + label: facet.label, + uiType: facet.uiType || 'checkbox' + } + } + + function filterExists(field, value) { + const filter = getFilter(field) + if (!filter) return false + const includes = filter.values.includes(value) + return includes + } + + function addFilter(field, value) { + const facets = driver.searchQuery.facets || {} + const facet = facets[field] + if (!facet) return + driver.actions.addFilter(field, value, facet.filterType) + } + + function removeFilter(field, value) { + const facets = driver.searchQuery.facets || {} + const facet = facets[field] + if (!facet) return + driver.actions.removeFilter(field, value, facet.filterType) + } + + function setFilter(field, value) { + const facets = driver.searchQuery.facets || {} + const facet = facets[field] + if (!facet) return + driver.actions.setFilter(field, value, facet.filterType) + } + + // Local storage functions + + function getLocalFilters() { + return JSON.parse(localStorage.getItem(`${name}.filters`)) || [] + } + + function getLocalSettings() { + return JSON.parse(localStorage.getItem(`${name}.settings`)) || {} + } + + function isFacetExpanded(field) { + const settings = getLocalSettings() + if (settings.hasOwnProperty(field)) { + return settings[field].isExpanded || driver.searchQuery.facets[field].isExpanded + } else { + return driver.searchQuery.facets[field].isExpanded + } + } + + function setFacetExpanded(field, value) { + const settings = getLocalSettings() + settings[field] = { isExpanded: value } + localStorage.setItem(`${name}.settings`, JSON.stringify(settings)) + } + + return ( + + {children} + + ) +} + +export default SearchUIContext From aa21f8ed611e2782c40f07b09f8d211770546bd5 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Mon, 28 Aug 2023 15:11:09 -0400 Subject: [PATCH 02/23] Add CollapsibleFacetContainer CollapsibleFacetContainer holds the logic for collapsing and expanding its children --- components/core/CollapsibleFacetContainer.jsx | 145 ++++++++++++++++++ ...apsableLayout.js => CollapsibleLayout.jsx} | 7 +- 2 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 components/core/CollapsibleFacetContainer.jsx rename components/core/{CollapsableLayout.js => CollapsibleLayout.jsx} (87%) diff --git a/components/core/CollapsibleFacetContainer.jsx b/components/core/CollapsibleFacetContainer.jsx new file mode 100644 index 0000000..cdc9199 --- /dev/null +++ b/components/core/CollapsibleFacetContainer.jsx @@ -0,0 +1,145 @@ +import React from 'react' +import { helpers } from '@elastic/search-ui' + +import { accentFold, withSearch } from '@elastic/react-search-ui' +import SearchUIContext from './SearchUIContext' +import CollapsibleLayout from './CollapsibleLayout' +import CheckboxFacet from './CheckboxFacet' + +const { markSelectedFacetValuesFromFilters } = helpers + +export class CollapsibleFacetContainer extends React.Component { + static contextType = SearchUIContext + + static defaultProps = { + filterType: 'all', + isFilterable: false, + show: 5 + } + + constructor(props) { + super(props) + this.state = { + more: props.show, + searchTerm: '', + isExpanded: false + } + } + + componentDidMount() { + this.setState({ isExpanded: this.context.isFacetExpanded(this.props.field) }) + } + + handleSetIsExpanded = (isExpanded) => { + this.setState({ isExpanded }) + this.context.setFacetExpanded(this.props.field, isExpanded) + } + + handleClickMore = (totalOptions) => { + this.setState(({ more }) => { + let visibleOptionsCount = more + 10 + const showingAll = visibleOptionsCount >= totalOptions + if (showingAll) visibleOptionsCount = totalOptions + + this.context.a11yNotify('moreFilters', { visibleOptionsCount, showingAll }) + + return { more: visibleOptionsCount } + }) + } + + handleFacetSearch = (searchTerm) => { + this.setState({ searchTerm }) + } + + isFacetVisible(facet, options) { + if (facet.type == 'range') { + return true + } + return options.length > 0 + } + + render() { + const { more, searchTerm } = this.state + const { + field, + facet, + transformFunction, + formatVal, + view, + ...rest + } = this.props + const facets = this.context.getFacetData() + const facetsForField = facets[field]; + + if (!facetsForField) return null; + + const facetData = facetsForField[0]; + + // markSelectedFacetValuesFromFilters looks for type to compare filterType + const filters = this.context.getFilters().map((filter) => { + return { + field: filter.field, + type: filter.filterType, + values: filter.values + } + }) + + let facetValues = markSelectedFacetValuesFromFilters(facetData, filters, field, facet.filterType).data + + if (searchTerm.trim()) { + facetValues = facetValues.filter((option) => { + let valueToSearch + switch (typeof option.value) { + case 'string': + valueToSearch = accentFold(option.value).toLowerCase() + break + case 'number': + valueToSearch = option.value.toString() + break + case 'object': + valueToSearch = + typeof option?.value?.name === 'string' ? accentFold(option.value.name).toLowerCase() : '' + break + + default: + valueToSearch = '' + break + } + return valueToSearch.includes(accentFold(searchTerm).toLowerCase()) + }) + } + + const View = view || CheckboxFacet + + const viewProps = { + field: field, + facet: facet, + options: facetValues.slice(0, more), + formatVal: formatVal, + transformFunction: transformFunction, + showMore: facetValues.length > more, + showSearch: facet.isFilterable, + searchPlaceholder: `Filter ${facet.label}`, + onSearch: (value) => { + this.handleFacetSearch(value) + }, + onMoreClick: this.handleClickMore.bind(this, facetValues.length), + ...rest + } + + return ( + this.isFacetVisible(field, viewProps.options) && ( + + + + ) + ) + } +} + +export default withSearch(({ facets }) => ({ facets }))(CollapsibleFacetContainer); \ No newline at end of file diff --git a/components/core/CollapsableLayout.js b/components/core/CollapsibleLayout.jsx similarity index 87% rename from components/core/CollapsableLayout.js rename to components/core/CollapsibleLayout.jsx index 4dfeabf..527c47a 100644 --- a/components/core/CollapsableLayout.js +++ b/components/core/CollapsibleLayout.jsx @@ -1,16 +1,15 @@ -import React from "react"; import styles from "../../css/collapsableFacets.module.css"; import { ChevronDown, ChevronRight } from "react-bootstrap-icons"; import { Col, Row } from "react-bootstrap"; -const CollapsableLayout = ({ isExpanded, setIsExpanded, label, formatVal, children }) => { +const CollapsibleLayout = ({ isExpanded, setIsExpanded, label, formatVal, children }) => { function formatClassName(label) { return `sui-facet__title sui-facet__title--${formatVal(label)}` } function handleExpandClick() { - setIsExpanded((previous) => !previous) + setIsExpanded(!isExpanded) } return ( @@ -37,4 +36,4 @@ const CollapsableLayout = ({ isExpanded, setIsExpanded, label, formatVal, childr ) } -export default CollapsableLayout +export default CollapsibleLayout From e940408380f0b15012918a26bb92ef64650f9ac1 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Mon, 28 Aug 2023 15:31:49 -0400 Subject: [PATCH 03/23] Add facets component and checkbox facet component --- components/core/CheckboxFacet.jsx | 104 ++++++++++++++++++++++++++++ components/core/Facets.jsx | 109 ++++++++++-------------------- 2 files changed, 141 insertions(+), 72 deletions(-) create mode 100644 components/core/CheckboxFacet.jsx diff --git a/components/core/CheckboxFacet.jsx b/components/core/CheckboxFacet.jsx new file mode 100644 index 0000000..02acdeb --- /dev/null +++ b/components/core/CheckboxFacet.jsx @@ -0,0 +1,104 @@ +import { useContext } from 'react' +import SearchUIContext from './SearchUIContext' + +const CheckboxOptionFacet = ({ field, option, label, formatVal, transformFunction }) => { + const { filterExists, addFilter, removeFilter } = useContext(SearchUIContext) + + const value = option.value + + const handleCheckboxChange = (event) => { + event.preventDefault() + event.stopPropagation() + if (filterExists(field, value)) { + removeFilter(field, value) + } else { + addFilter(field, value) + } + } + + const doesFilterExist = () => { + const exists = filterExists(field, value) + return exists + } + + return ( + + ) +} + +const CheckboxFacet = ({ + field, + options, + facet, + formatVal, + transformFunction, + showMore, + showSearch, + searchPlaceholder, + onSearch, + onMoreClick +}) => { + return ( +
+ {showSearch && ( +
+ { + onSearch(e.target.value) + }} + /> +
+ )} + +
+ {options.length < 1 &&
No matching options
} + {options.map((option) => { + return ( + + ) + })} +
+ + {showMore && ( + + )} +
+ ) +} + +export default CheckboxFacet diff --git a/components/core/Facets.jsx b/components/core/Facets.jsx index 79ac173..afbb156 100644 --- a/components/core/Facets.jsx +++ b/components/core/Facets.jsx @@ -1,91 +1,56 @@ -import React, { Fragment } from "react"; -import { withSearch } from "@elastic/react-search-ui"; -import CollapsableCheckboxFacet from "./CollapsableCheckboxFacet"; -import CollapsableDateRangeFacet from "./CollapsableDateRangeFacet"; -import CollapsableNumericRangeFacet from "./CollapsableNumericRangeFacet"; -import {Sui} from "../../lib/search-tools"; +import React, { Fragment, useContext } from 'react' +import SearchUIContext from './SearchUIContext' +import CollapsibleFacetContainer from './CollapsibleFacetContainer' +import CheckboxFacet from './CheckboxFacet' -const Facets = ({fields, filters, rawResponse, transformFunction, removeFilter}) => { - const conditionalFacets = fields.conditionalFacets; +const Facets = ({ transformFunction }) => { + const { getFacets, getConditionalFacets, getFilters } = useContext(SearchUIContext) function formatVal(id) { - if (typeof id === "string") { - return id.replace(/\W+/g, "") + if (typeof id === 'string') { + return id.replace(/\W+/g, '') } return id } - function isConditionalFacet(facetKey) { - return conditionalFacets.hasOwnProperty(facetKey) - } - - function getConditionalFacet(facetKey) { - return conditionalFacets[facetKey] - } - - function isFacetVisible(facetKey) { - let result = true - if (isConditionalFacet(facetKey)) { - const predicate = getConditionalFacet(facetKey) + function isFacetVisible(field) { + const conditionalFacets = getConditionalFacets() + if (conditionalFacets.hasOwnProperty(field)) { + const predicate = conditionalFacets[field] + const filters = getFilters() if (filters) { - result = predicate({filters}) + return predicate({ filters }) } else { - result = false + return false } } - if (!result && filters && filters.filter(e => e.field === facetKey).length > 0) { - let filterKey = filters.filter(e => e.field === facetKey)[0].values[0] - if (filterKey.hasOwnProperty("name")) { - // Date or numeric range facet - filterKey = filterKey.name - } - const suiFilters = Sui.getFilters() - if (suiFilters.hasOwnProperty(`${facetKey}.${filterKey}`)) { - // Checkbox facet - suiFilters[`${facetKey}.${filterKey}`].selected = false - Sui.saveFilters(suiFilters) - } else if (suiFilters.hasOwnProperty(filterKey)) { - // Date or numeric range facet - delete suiFilters[filterKey]["from"] - delete suiFilters[filterKey]["to"] - } - Sui.saveFilters(suiFilters) - removeFilter(facetKey) - } - return result + return true } - return (<> - {Object.entries(fields.facets) - .map(facet => { - if (!isFacetVisible(facet[0])) { - return + return ( + <> + {Object.entries(getFacets()).map(([field, facet]) => { + if (!isFacetVisible(field)) { + return } - if (facet[1].uiType === "daterange") { - return - } else if (facet[1].uiType === "numrange") { - return + if (!facet.uiType) { + return ( + + ) } else { - return + return } - } - )} - ) + })} + + ) } -export default withSearch(({ removeFilter }) => ({ - removeFilter -}))(Facets) +export default Facets From 93dcd6ae184d72f7ed1b6dd463db03d5ecd848bc Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Tue, 29 Aug 2023 12:39:13 -0400 Subject: [PATCH 04/23] Add removeFiltersForField to search ui context --- components/core/SearchUIContext.jsx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 9748a45..c4a5990 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -140,6 +140,18 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { driver.actions.addFilter(field, value, facet.filterType) } + /** + * Remove a specific filter value in a given field + * @param {string} field The facet field + * @param {string} value The filter value to be removed + * + * @example + * // Remove the filter value "Dataset" from the "entity_type" facet + * removeFilter("entity_type", "Dataset") + * + * // Remove the range filter value from the "created_timestamp" facet + * removeFilter("created_timestamp", { from: 1690156800000, to: 1692921599999, name: "created_timestamp" }}) + */ function removeFilter(field, value) { const facets = driver.searchQuery.facets || {} const facet = facets[field] @@ -147,6 +159,18 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { driver.actions.removeFilter(field, value, facet.filterType) } + /** + * Remove all filter values associated with a given field + * @param {string} field The facet field + */ + function removeFiltersForField(field) { + const filter = getFilter(field) + if (!filter) return + filter.values.forEach((value) => { + removeFilter(field, value) + }) + } + function setFilter(field, value) { const facets = driver.searchQuery.facets || {} const facet = facets[field] @@ -190,6 +214,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { filterExists, addFilter, removeFilter, + removeFiltersForField, setFilter, isFacetExpanded, setFacetExpanded, From d42c957947f262ebb064125dd134a967ee19376e Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Tue, 29 Aug 2023 12:41:12 -0400 Subject: [PATCH 05/23] Add date range facet component --- components/core/DateRangeFacet.jsx | 122 +++++++++++++++++++++++++++++ components/core/Facets.jsx | 18 ++++- 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 components/core/DateRangeFacet.jsx diff --git a/components/core/DateRangeFacet.jsx b/components/core/DateRangeFacet.jsx new file mode 100644 index 0000000..5da1045 --- /dev/null +++ b/components/core/DateRangeFacet.jsx @@ -0,0 +1,122 @@ +import { useContext, useState } from 'react' +import SearchUIContext from './SearchUIContext' +import styles from '../../css/collapsableFacets.module.css' + +const DateRangeFacet = ({ field, label, formatVal }) => { + // default dates + const DEFAULT_MIN_DATE = '1970-01-01' + const DEFAULT_MAX_DATE = '2300-01-01' + + const { getFilter, setFilter, removeFiltersForField } = useContext(SearchUIContext) + + const [values, setValues] = useState(getInitialValues()) + const [errorValues, setErrorValues] = useState({ + start: '', + end: '' + }) + const [dateConstraints, setDateConstraints] = useState({ + startMax: DEFAULT_MAX_DATE, + endMin: DEFAULT_MIN_DATE + }) + + function getInitialValues() { + const filter = getFilter(field) + if (!filter) return { start: '', end: '' } + return { + start: convertTimestampToString(filter.values[0].from), + end: convertTimestampToString(filter.values[0].to) + } + } + + function convertTimestampToString(timestamp) { + if (!timestamp) return '' + return new Date(timestamp).toISOString().split('T')[0] + } + + function convertStringToTimestamp(dateString) { + return Date.parse(`${dateString}T00:00:00.000Z`) + } + + function handleDateChange(newValues, targetName) { + const filter = {} + + const startTimestamp = convertStringToTimestamp(newValues.start) + if (startTimestamp && startTimestamp >= 0) { + filter.from = startTimestamp + } + + const endTimestamp = convertStringToTimestamp(newValues.end) + if (endTimestamp && endTimestamp >= 0) { + // Add 24 hours minus 1 ms to the end date so inclusive of the end date + filter.to = endTimestamp + 24 * 60 * 60 * 1000 - 1 + } + + if (filter.from && filter.to && filter.from > filter.to) { + // values are invalid + if (targetName === 'start') { + setErrorValues({ start: 'Start date must be before end date', end: '' }) + } else { + setErrorValues({ end: 'End date must be after start date', start: '' }) + } + } else { + // values are valid + if (Object.keys(filter).length === 0) { + // remove filter + removeFiltersForField(field) + } else { + // set new filter + filter.name = field + setFilter(field, filter) + } + setErrorValues({ start: '', end: '' }) + setDateConstraints({ + startMax: filter.to ? newValues.end : DEFAULT_MAX_DATE, + endMin: filter.from ? newValues.start : DEFAULT_MIN_DATE + }) + } + + setValues(newValues) + } + + return ( + <> +
+ Start Date + handleDateChange({ ...values, start: e.target.value }, 'start')} + required + pattern='\d{4}-\d{2}-\d{2}' + /> +
+
+ End Date + handleDateChange({ ...values, end: e.target.value }, 'end')} + required + pattern='\d{4}-\d{2}-\d{2}' + /> +
+
+ {Object.values(errorValues).map((error) => { + return {error} + })} +
+ + ) +} + +export default DateRangeFacet diff --git a/components/core/Facets.jsx b/components/core/Facets.jsx index afbb156..ca01088 100644 --- a/components/core/Facets.jsx +++ b/components/core/Facets.jsx @@ -2,6 +2,7 @@ import React, { Fragment, useContext } from 'react' import SearchUIContext from './SearchUIContext' import CollapsibleFacetContainer from './CollapsibleFacetContainer' import CheckboxFacet from './CheckboxFacet' +import DateRangeFacet from './DateRangeFacet' const Facets = ({ transformFunction }) => { const { getFacets, getConditionalFacets, getFilters } = useContext(SearchUIContext) @@ -34,7 +35,7 @@ const Facets = ({ transformFunction }) => { return } - if (!facet.uiType) { + if (facet.uiType === 'daterange') { return ( { facet={facet} transformFunction={transformFunction} formatVal={formatVal} - view={CheckboxFacet} + view={DateRangeFacet} /> ) - } else { + } else if (facet.uiType === 'numrange') { return + } else { + return ( + + ) } })} From 1d1b1312e537ecc003899407f3a59952eae95925 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Tue, 29 Aug 2023 15:00:24 -0400 Subject: [PATCH 06/23] Change filters to state in search ui context --- components/core/CollapsibleFacetContainer.jsx | 2 +- components/core/Facets.jsx | 3 +-- components/core/SearchUIContext.jsx | 9 ++++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/components/core/CollapsibleFacetContainer.jsx b/components/core/CollapsibleFacetContainer.jsx index cdc9199..a1556e4 100644 --- a/components/core/CollapsibleFacetContainer.jsx +++ b/components/core/CollapsibleFacetContainer.jsx @@ -76,7 +76,7 @@ export class CollapsibleFacetContainer extends React.Component { const facetData = facetsForField[0]; // markSelectedFacetValuesFromFilters looks for type to compare filterType - const filters = this.context.getFilters().map((filter) => { + const filters = this.context.filters.map((filter) => { return { field: filter.field, type: filter.filterType, diff --git a/components/core/Facets.jsx b/components/core/Facets.jsx index ca01088..faecf9a 100644 --- a/components/core/Facets.jsx +++ b/components/core/Facets.jsx @@ -5,7 +5,7 @@ import CheckboxFacet from './CheckboxFacet' import DateRangeFacet from './DateRangeFacet' const Facets = ({ transformFunction }) => { - const { getFacets, getConditionalFacets, getFilters } = useContext(SearchUIContext) + const { getFacets, getConditionalFacets, filters } = useContext(SearchUIContext) function formatVal(id) { if (typeof id === 'string') { @@ -18,7 +18,6 @@ const Facets = ({ transformFunction }) => { const conditionalFacets = getConditionalFacets() if (conditionalFacets.hasOwnProperty(field)) { const predicate = conditionalFacets[field] - const filters = getFilters() if (filters) { return predicate({ filters }) } else { diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index c4a5990..a66bdb0 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect } from 'react' +import { createContext, useContext, useEffect, useState } from 'react' import { SearchContext } from '@elastic/react-search-ui' const SearchUIContext = createContext() @@ -50,6 +50,8 @@ const SearchUIContext = createContext() export function SearchUIProvider({ children, name = 'new.entities' }) { const { driver } = useContext(SearchContext) + const [filters, setFilters] = useState(getFilters()) + useEffect(() => { if (driver.state.filters && driver.state.filters.length > 0) return const localFilters = getLocalFilters() @@ -62,7 +64,8 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { }, []) useEffect(() => { - console.log('=====Saving filters to local storage=====', driver.state.filters) + if (driver.state.isLoading) return + setFilters(getFilters()) localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) removeInvalidConditionalFacets() }, [driver.state]) @@ -209,7 +212,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { getFacets, getConditionalFacets, getFacetData, - getFilters, + filters, getFilter, filterExists, addFilter, From 2444187f1298f1125a0c5d58f55d8b94e7ac4ff9 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Tue, 29 Aug 2023 15:25:14 -0400 Subject: [PATCH 07/23] Add numeric range facet component --- components/core/Facets.jsx | 12 +++- components/core/NumericRangeFacet.jsx | 97 +++++++++++++++++++++++++++ components/core/SearchUIContext.jsx | 3 + 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 components/core/NumericRangeFacet.jsx diff --git a/components/core/Facets.jsx b/components/core/Facets.jsx index faecf9a..5c9b11b 100644 --- a/components/core/Facets.jsx +++ b/components/core/Facets.jsx @@ -3,6 +3,7 @@ import SearchUIContext from './SearchUIContext' import CollapsibleFacetContainer from './CollapsibleFacetContainer' import CheckboxFacet from './CheckboxFacet' import DateRangeFacet from './DateRangeFacet' +import NumericRangeFacet from './NumericRangeFacet' const Facets = ({ transformFunction }) => { const { getFacets, getConditionalFacets, filters } = useContext(SearchUIContext) @@ -46,7 +47,16 @@ const Facets = ({ transformFunction }) => { /> ) } else if (facet.uiType === 'numrange') { - return + return ( + + ) } else { return ( { + const valueRange = facet.uiRange + const { getFilter, setFilter, removeFiltersForField, aggregations } = useContext(SearchUIContext) + + const [values, setValues] = useState(getInitialValues()) + const [histogramData, setHistogramData] = useState([]) + + useEffect(() => { + if (aggregations && aggregations.hasOwnProperty(`${field}_histogram`)) { + setHistogramData(aggregations[`${field}_histogram`]['buckets'] || []) + } + }, [aggregations]) + + function getInitialValues() { + const filter = getFilter(field) + if (!filter) return facet.uiRange + return [filter.values[0].from || facet.uiRange[0], filter.values[0].to || facet.uiRange[1]] + } + + const marks = [ + { + value: valueRange[0], + label: valueRange[0] + }, + { + value: valueRange[1], + label: valueRange[1] + } + ] + + function updateFilters(newValues) { + const minValue = newValues[0] !== '' ? newValues[0] : valueRange[0] + const maxValue = newValues[1] !== '' ? newValues[1] : valueRange[1] + + const filter = {} + if (minValue !== valueRange[0]) { + filter.from = minValue + } + + if (maxValue !== valueRange[1]) { + filter.to = maxValue + } + + if (Object.keys(filter).length === 0) { + // remove filter + removeFiltersForField(field) + } else { + // set new filter + filter.name = field + setFilter(field, filter) + } + } + + function handleSliderChange(_, newValues) { + setValues(newValues) + } + + function handleSliderCommitted(_, newValues) { + updateFilters(newValues) + } + + function valueText(value) { + return `${value}` + } + + return ( + <> +
+
+ + { + label + }} + marks={marks} + value={values} + min={valueRange[0]} + max={valueRange[1]} + onChange={handleSliderChange} + valueLabelDisplay='auto' + getAriaValueText={valueText} + /> +
+
+ + ) +} + +export default NumericRangeFacet diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index a66bdb0..47f2ecd 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -51,6 +51,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { const { driver } = useContext(SearchContext) const [filters, setFilters] = useState(getFilters()) + const [aggregations, setAggregations] = useState({}) useEffect(() => { if (driver.state.filters && driver.state.filters.length > 0) return @@ -66,6 +67,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { useEffect(() => { if (driver.state.isLoading) return setFilters(getFilters()) + setAggregations(driver.state.rawResponse.aggregations || {}) localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) removeInvalidConditionalFacets() }, [driver.state]) @@ -219,6 +221,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { removeFilter, removeFiltersForField, setFilter, + aggregations, isFacetExpanded, setFacetExpanded, a11yNotify: driver.a11yNotify From d57aa472601273aa7f84fa7a7f132d69c0d6fed7 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Wed, 30 Aug 2023 10:08:22 -0400 Subject: [PATCH 08/23] Fix filterExists comparison for range facets --- components/core/SearchUIContext.jsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 47f2ecd..26e7195 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -96,7 +96,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { function getConditionalFacets() { return driver.searchQuery.conditionalFacets || {} } - + function getFacetData() { return driver.state.facets } @@ -134,7 +134,15 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { function filterExists(field, value) { const filter = getFilter(field) if (!filter) return false - const includes = filter.values.includes(value) + let includes = false + if (filter.type === 'range') { + // compare range values manually because JavaScript + includes = filter.values.some((range) => { + return range.name == value.name && range.from === value.from && range.to === value.to + }) + } else { + includes = filter.values.includes(value) + } return includes } @@ -149,14 +157,14 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { * Remove a specific filter value in a given field * @param {string} field The facet field * @param {string} value The filter value to be removed - * + * * @example * // Remove the filter value "Dataset" from the "entity_type" facet * removeFilter("entity_type", "Dataset") - * + * * // Remove the range filter value from the "created_timestamp" facet * removeFilter("created_timestamp", { from: 1690156800000, to: 1692921599999, name: "created_timestamp" }}) - */ + */ function removeFilter(field, value) { const facets = driver.searchQuery.facets || {} const facet = facets[field] From 46f9b544c31c02f66b68cacfebe101e2df9e5857 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Wed, 30 Aug 2023 11:12:12 -0400 Subject: [PATCH 09/23] Add filter changed callback to search ui context --- components/core/DateRangeFacet.jsx | 31 ++++++++++++++++--------- components/core/SearchUIContext.jsx | 36 +++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/components/core/DateRangeFacet.jsx b/components/core/DateRangeFacet.jsx index 5da1045..bd28fa3 100644 --- a/components/core/DateRangeFacet.jsx +++ b/components/core/DateRangeFacet.jsx @@ -1,4 +1,4 @@ -import { useContext, useState } from 'react' +import { useContext, useEffect, useState } from 'react' import SearchUIContext from './SearchUIContext' import styles from '../../css/collapsableFacets.module.css' @@ -7,9 +7,9 @@ const DateRangeFacet = ({ field, label, formatVal }) => { const DEFAULT_MIN_DATE = '1970-01-01' const DEFAULT_MAX_DATE = '2300-01-01' - const { getFilter, setFilter, removeFiltersForField } = useContext(SearchUIContext) + const { registerFilterChangeCallback, unregisterFilterChangeCallback, getFilter, setFilter, removeFiltersForField, filterExists } = useContext(SearchUIContext) - const [values, setValues] = useState(getInitialValues()) + const [values, setValues] = useState(getValuesFromFilter()) const [errorValues, setErrorValues] = useState({ start: '', end: '' @@ -19,7 +19,15 @@ const DateRangeFacet = ({ field, label, formatVal }) => { endMin: DEFAULT_MIN_DATE }) - function getInitialValues() { + useEffect(() => { + registerFilterChangeCallback(field, (value, changedBy) => { + if (changedBy === field) return + handleDateChange(getValuesFromFilter(), 'start') + }) + return () => { unregisterFilterChangeCallback(field) } + }, []) + + function getValuesFromFilter() { const filter = getFilter(field) if (!filter) return { start: '', end: '' } return { @@ -38,7 +46,7 @@ const DateRangeFacet = ({ field, label, formatVal }) => { } function handleDateChange(newValues, targetName) { - const filter = {} + const filter = { name: field } const startTimestamp = convertStringToTimestamp(newValues.start) if (startTimestamp && startTimestamp >= 0) { @@ -60,13 +68,14 @@ const DateRangeFacet = ({ field, label, formatVal }) => { } } else { // values are valid - if (Object.keys(filter).length === 0) { + if (!filter.from && !filter.to) { // remove filter removeFiltersForField(field) } else { - // set new filter - filter.name = field - setFilter(field, filter) + // set new filter if it doesn't exist + if (!filterExists(field, filter)) { + setFilter(field, filter) + } } setErrorValues({ start: '', end: '' }) setDateConstraints({ @@ -111,8 +120,8 @@ const DateRangeFacet = ({ field, label, formatVal }) => { />
- {Object.values(errorValues).map((error) => { - return {error} + {Object.values(errorValues).map((error, idx) => { + return {error} })}
diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 26e7195..26e22d9 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -53,6 +53,8 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { const [filters, setFilters] = useState(getFilters()) const [aggregations, setAggregations] = useState({}) + const [filterChangeCallbacks, setFilterChangeCallbacks] = useState({}) + useEffect(() => { if (driver.state.filters && driver.state.filters.length > 0) return const localFilters = getLocalFilters() @@ -103,6 +105,17 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { // Filters + function registerFilterChangeCallback(field, callback) { + setFilterChangeCallbacks({ ...filterChangeCallbacks, [field]: callback }) + } + + function unregisterFilterChangeCallback(field) { + setFilterChangeCallbacks(current => { + delete current[field] + return current; + }) + } + function getFilters() { const facets = driver.searchQuery.facets || {} return driver.state.filters.map((filter) => { @@ -146,11 +159,14 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { return includes } - function addFilter(field, value) { + function addFilter(field, value, changedBy) { const facets = driver.searchQuery.facets || {} const facet = facets[field] if (!facet) return driver.actions.addFilter(field, value, facet.filterType) + if (filterChangeCallbacks.hasOwnProperty(field)) { + filterChangeCallbacks[field](value, changedBy || field) + } } /** @@ -165,30 +181,40 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { * // Remove the range filter value from the "created_timestamp" facet * removeFilter("created_timestamp", { from: 1690156800000, to: 1692921599999, name: "created_timestamp" }}) */ - function removeFilter(field, value) { + function removeFilter(field, value, changedBy) { const facets = driver.searchQuery.facets || {} const facet = facets[field] if (!facet) return driver.actions.removeFilter(field, value, facet.filterType) + if (filterChangeCallbacks.hasOwnProperty(field)) { + filterChangeCallbacks[field](value, changedBy || field) + } } /** * Remove all filter values associated with a given field * @param {string} field The facet field */ - function removeFiltersForField(field) { + function removeFiltersForField(field, changedBy) { const filter = getFilter(field) if (!filter) return filter.values.forEach((value) => { removeFilter(field, value) }) + if (filterChangeCallbacks.hasOwnProperty(field)) { + const value = filter.values[0] + filterChangeCallbacks[field](value, changedBy || field) + } } - function setFilter(field, value) { + function setFilter(field, value, changedBy) { const facets = driver.searchQuery.facets || {} const facet = facets[field] if (!facet) return driver.actions.setFilter(field, value, facet.filterType) + if (filterChangeCallbacks.hasOwnProperty(field)) { + filterChangeCallbacks[field](value, changedBy || field) + } } // Local storage functions @@ -222,6 +248,8 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { getFacets, getConditionalFacets, getFacetData, + registerFilterChangeCallback, + unregisterFilterChangeCallback, filters, getFilter, filterExists, From f01f47c9c95218fce72e2bef39e7cb61f7dae9d5 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Wed, 30 Aug 2023 14:46:08 -0400 Subject: [PATCH 10/23] Add filter change callback to numeric range facet --- components/core/NumericRangeFacet.jsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/components/core/NumericRangeFacet.jsx b/components/core/NumericRangeFacet.jsx index b1b8b4d..d525709 100644 --- a/components/core/NumericRangeFacet.jsx +++ b/components/core/NumericRangeFacet.jsx @@ -5,7 +5,7 @@ import Histogram from './Histogram' const NumericRangeFacet = ({ field, label, facet }) => { const valueRange = facet.uiRange - const { getFilter, setFilter, removeFiltersForField, aggregations } = useContext(SearchUIContext) + const {registerFilterChangeCallback, unregisterFilterChangeCallback, getFilter, setFilter, removeFiltersForField, aggregations } = useContext(SearchUIContext) const [values, setValues] = useState(getInitialValues()) const [histogramData, setHistogramData] = useState([]) @@ -22,6 +22,14 @@ const NumericRangeFacet = ({ field, label, facet }) => { return [filter.values[0].from || facet.uiRange[0], filter.values[0].to || facet.uiRange[1]] } + useEffect(() => { + registerFilterChangeCallback(field, (value, changedBy) => { + if (changedBy === field) return + setValues(getInitialValues()) + }) + return () => { unregisterFilterChangeCallback(field) } + }, []) + const marks = [ { value: valueRange[0], From d640ce2655884b88dd8a1bd605a11624683abf81 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Thu, 31 Aug 2023 10:13:42 -0400 Subject: [PATCH 11/23] Add clear search term to search ui context --- components/core/SearchUIContext.jsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 26e22d9..909b8e5 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -169,6 +169,19 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { } } + function clearSearchTerm(shouldClearFilters = true) { + driver.actions.setSearchTerm("", {shouldClearFilters}) + if (!shouldClearFilters) return + + filters.forEach((filter) => { + if (filterChangeCallbacks.hasOwnProperty(filter.field)) { + filter.values.forEach((value) => { + filterChangeCallbacks[filter.field](value, "clearSearchTerm") + }) + } + }) + } + /** * Remove a specific filter value in a given field * @param {string} field The facet field @@ -257,10 +270,11 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { removeFilter, removeFiltersForField, setFilter, + clearSearchTerm, aggregations, isFacetExpanded, setFacetExpanded, - a11yNotify: driver.a11yNotify + a11yNotify: driver.a11yNotify, }} > {children} From c6590afda8076a91bfa52b0267a0711f2cd0311d Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Thu, 31 Aug 2023 14:00:32 -0400 Subject: [PATCH 12/23] Add was searched to search ui context --- components/core/SearchUIContext.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 909b8e5..7b8786e 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -52,6 +52,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { const [filters, setFilters] = useState(getFilters()) const [aggregations, setAggregations] = useState({}) + const [wasSearched, setWasSearched] = useState(false) const [filterChangeCallbacks, setFilterChangeCallbacks] = useState({}) @@ -70,6 +71,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { if (driver.state.isLoading) return setFilters(getFilters()) setAggregations(driver.state.rawResponse.aggregations || {}) + setWasSearched(driver.state.wasSearched) localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) removeInvalidConditionalFacets() }, [driver.state]) @@ -270,6 +272,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { removeFilter, removeFiltersForField, setFilter, + wasSearched, clearSearchTerm, aggregations, isFacetExpanded, From f730acac4f2a8b310f4e3e52eee051af75b03059 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Thu, 31 Aug 2023 14:15:33 -0400 Subject: [PATCH 13/23] Add local schema versioning to search ui context --- components/core/SearchUIContext.jsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 7b8786e..5859a6a 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -48,6 +48,8 @@ const SearchUIContext = createContext() // export function SearchUIProvider({ children, name = 'new.entities' }) { + const LOCAL_SCHEMA_VERSION = 1 + const { driver } = useContext(SearchContext) const [filters, setFilters] = useState(getFilters()) @@ -57,6 +59,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { const [filterChangeCallbacks, setFilterChangeCallbacks] = useState({}) useEffect(() => { + checkLocalStorageSchema() if (driver.state.filters && driver.state.filters.length > 0) return const localFilters = getLocalFilters() console.log('=====Loading filters from local storage=====', localFilters) @@ -234,6 +237,14 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { // Local storage functions + function checkLocalStorageSchema() { + const schemaVersion = localStorage.getItem('schemaVersion') || 0 + if (schemaVersion < LOCAL_SCHEMA_VERSION) { + localStorage.clear() + localStorage.setItem('schemaVersion', LOCAL_SCHEMA_VERSION) + } + } + function getLocalFilters() { return JSON.parse(localStorage.getItem(`${name}.filters`)) || [] } From a27a720f96c3492b1ba9ed1cb816da7bd0c806af Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Thu, 31 Aug 2023 15:19:53 -0400 Subject: [PATCH 14/23] Fix histogram load animation --- components/core/Histogram.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/core/Histogram.jsx b/components/core/Histogram.jsx index a4de928..8155655 100644 --- a/components/core/Histogram.jsx +++ b/components/core/Histogram.jsx @@ -38,6 +38,9 @@ class Histogram extends React.Component { }; const options = { + animation: { + duration: 0 + }, responsive: true, plugins: { legend: { From a33599ad89d552af201660e799097b2ca1015559 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Thu, 31 Aug 2023 16:40:23 -0400 Subject: [PATCH 15/23] Change was searched initial value --- components/core/SearchUIContext.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 5859a6a..da7c3e7 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -54,7 +54,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { const [filters, setFilters] = useState(getFilters()) const [aggregations, setAggregations] = useState({}) - const [wasSearched, setWasSearched] = useState(false) + const [wasSearched, setWasSearched] = useState(driver.state.wasSearched) const [filterChangeCallbacks, setFilterChangeCallbacks] = useState({}) From 55a2f92bea765140a7c6352014ade0e523b4d8d6 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Thu, 31 Aug 2023 16:40:47 -0400 Subject: [PATCH 16/23] Create search ui container --- components/core/SearchUIContainer.jsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 components/core/SearchUIContainer.jsx diff --git a/components/core/SearchUIContainer.jsx b/components/core/SearchUIContainer.jsx new file mode 100644 index 0000000..acc89f3 --- /dev/null +++ b/components/core/SearchUIContainer.jsx @@ -0,0 +1,18 @@ +import { SearchProvider, WithSearch } from '@elastic/react-search-ui' +import { SearchUIProvider } from './SearchUIContext' + +function SearchUIContainer({ config, name, children }) { + return ( + + ({ filters, wasSearched, rawResponse })} + > + {({ filters, wasSearched, rawResponse }) => { + return {children} + }} + + + ) +} + +export default SearchUIContainer From 056d5d3c9a765ada3fb4571740fbed7b242ef689 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 1 Sep 2023 09:57:46 -0400 Subject: [PATCH 17/23] Add raw response to search ui context --- components/core/SearchUIContext.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index da7c3e7..69df97f 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -54,6 +54,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { const [filters, setFilters] = useState(getFilters()) const [aggregations, setAggregations] = useState({}) + const [rawResponse, setRawResponse] = useState(driver.state.rawResponse) const [wasSearched, setWasSearched] = useState(driver.state.wasSearched) const [filterChangeCallbacks, setFilterChangeCallbacks] = useState({}) @@ -74,6 +75,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { if (driver.state.isLoading) return setFilters(getFilters()) setAggregations(driver.state.rawResponse.aggregations || {}) + setRawResponse(driver.state.rawResponse) setWasSearched(driver.state.wasSearched) localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) removeInvalidConditionalFacets() @@ -286,6 +288,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { wasSearched, clearSearchTerm, aggregations, + rawResponse, isFacetExpanded, setFacetExpanded, a11yNotify: driver.a11yNotify, From 5ed844df74c5c9308ead7299e73dc2d589416a1d Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 1 Sep 2023 11:10:22 -0400 Subject: [PATCH 18/23] Add documentation to search ui context --- components/core/SearchUIContext.jsx | 170 ++++++++++++++++++++-------- 1 file changed, 121 insertions(+), 49 deletions(-) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 69df97f..8c8ce7c 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -3,51 +3,12 @@ import { SearchContext } from '@elastic/react-search-ui' const SearchUIContext = createContext() -// Filter type from SearchContext -// -// value filter -// { -// field: "entity_type", -// type: "all", -// values: ["Dataset"] -// } -// -// range filter -// { -// field: "created_timestamp", -// type: "all", -// values: [ -// { from: 1690156800000, to: 1692921599999, name: "created_timestamp" } -// ] -// } -// - -// Filter type returns from useSearchUI -// -// value filter -// { -// field: "entity_type", -// filterType: "all", -// label: "Entity Type", -// type: "value", -// uiType: "checkbox", -// values: ["Dataset"] -// } -// -// range filter -// { -// field: "created_timestamp", -// filterType: "all", -// label: "Created Timestamp", -// type: "range", -// uiType: "date", -// values: [ -// { from: 1690156800000, to: 1692921599999, name: "created_timestamp" } -// ] -// } -// - -export function SearchUIProvider({ children, name = 'new.entities' }) { +/** + * Provider to get access to the SearchUIContext + * @param {string} name The name of the search UI. This is used to namespace local storage. + */ +export function SearchUIProvider({ name, children }) { + // Used to check if local storage should be cleared const LOCAL_SCHEMA_VERSION = 1 const { driver } = useContext(SearchContext) @@ -63,7 +24,6 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { checkLocalStorageSchema() if (driver.state.filters && driver.state.filters.length > 0) return const localFilters = getLocalFilters() - console.log('=====Loading filters from local storage=====', localFilters) localFilters.forEach((filter) => { filter.values.forEach((value) => { addFilter(filter.field, value) @@ -73,10 +33,12 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { useEffect(() => { if (driver.state.isLoading) return + setFilters(getFilters()) setAggregations(driver.state.rawResponse.aggregations || {}) setRawResponse(driver.state.rawResponse) setWasSearched(driver.state.wasSearched) + localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) removeInvalidConditionalFacets() }, [driver.state]) @@ -98,24 +60,50 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { // Facets + /** + * Get an object of all facets. Facets are defined in the search config file passed to SearchUIContainer or SearchProvider. + * @return {object} An object of facet objects. + */ function getFacets() { return driver.searchQuery.facets || {} } + /** + * Get an object of all conditional facets. Conditional facets are defined in the search config file passed to SearchUIContainer or SearchProvider. + * @return {object} An object of conditional facet objects. + */ function getConditionalFacets() { return driver.searchQuery.conditionalFacets || {} } + /** + * Get an object of all facet data. Facet data is returned from the search API. + * @returns {object} An object of facet data objects. See SearchState.facets in @elastic/search-ui for object structure. + */ function getFacetData() { return driver.state.facets } // Filters + /** + * + * @param {string} field The facet field to receive callbacks for + * @param {function} callback The callback function to be called when a filter value changes + * Callback function should have the following signature: + * + * See filterExists for value structure. changedBy is the identifier of the calling component. + * @example + * (value: (string|object), changedBy: string) => {} + */ function registerFilterChangeCallback(field, callback) { setFilterChangeCallbacks({ ...filterChangeCallbacks, [field]: callback }) } + /** + * + * @param {string} field The facet field to unregister callbacks for + */ function unregisterFilterChangeCallback(field) { setFilterChangeCallbacks(current => { delete current[field] @@ -123,6 +111,10 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { }) } + /** + * Get an array of all filters + * @return {Array} An array of filter objects. See getFilter for object structure. + */ function getFilters() { const facets = driver.searchQuery.facets || {} return driver.state.filters.map((filter) => { @@ -137,6 +129,34 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { }) } + /** + * Get an specific filter by field + * @param {string} field The facet field + * @return {object|null} The specific filter or null if doesn't exist + * + * Values filters will have a structure similar to: + * { + * field: "entity_type", + * filterType: "all", + * label: "Entity Type", + * type: "value", + * uiType: "checkbox", + * values: ["Dataset"] + * } + * + * Range filters will have a structure similar to: + * { + * field: "created_timestamp", + * filterType: "all", + * label: "Created Timestamp", + * type: "range", + * uiType: "date", + * values: [ + * // object will have either from or to; or both + * { from: 1690156800000, to: 1692921599999, name: "created_timestamp" } + * ] + * } + */ function getFilter(field) { const filter = driver.state.filters.find((f) => f.field === field) if (!filter) return null @@ -151,6 +171,18 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { } } + /** + * Check if a specific filter value exists for a given field + * @param {string} field The facet field + * @param {string | object} value The specific filter value to check for + * + * Values filter values should a have structure similar to: + * "Dataset" + * + * Range filters will have a structure similar to: + * Object can have either from or to; or both + * { from: 1690156800000, to: 1692921599999, name: "created_timestamp" } + */ function filterExists(field, value) { const filter = getFilter(field) if (!filter) return false @@ -166,6 +198,19 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { return includes } + /** + * Add a specific filter value for a given field + * @param {string} field The facet field + * @param {string | object} value The specific filter value to add. See filterExists for object structure. + * @param {string} changedBy The identifier of the calling component. This is used for filterChangeCallback. + * + * @example + * // Add the filter value "Dataset" from the "entity_type" facet + * addFilter("entity_type", "Dataset", "MyComponent") + * + * // Add the range filter value from the "created_timestamp" facet + * addFilter("created_timestamp", { from: 1690156800000, to: 1692921599999, name: "created_timestamp" }, "MyComponent") + */ function addFilter(field, value, changedBy) { const facets = driver.searchQuery.facets || {} const facet = facets[field] @@ -176,6 +221,10 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { } } + /** + * Clear the search term and optionally clear all filters + * @param {boolean} shouldClearFilters Whether or not to clear all filters (default: true) + */ function clearSearchTerm(shouldClearFilters = true) { driver.actions.setSearchTerm("", {shouldClearFilters}) if (!shouldClearFilters) return @@ -193,13 +242,14 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { * Remove a specific filter value in a given field * @param {string} field The facet field * @param {string} value The filter value to be removed + * @param {string} changedBy The identifier of the calling component. This is used for filterChangeCallback. * * @example * // Remove the filter value "Dataset" from the "entity_type" facet - * removeFilter("entity_type", "Dataset") + * removeFilter("entity_type", "Dataset", "MyComponent") * * // Remove the range filter value from the "created_timestamp" facet - * removeFilter("created_timestamp", { from: 1690156800000, to: 1692921599999, name: "created_timestamp" }}) + * removeFilter("created_timestamp", { from: 1690156800000, to: 1692921599999, name: "created_timestamp" }, "MyComponent") */ function removeFilter(field, value, changedBy) { const facets = driver.searchQuery.facets || {} @@ -214,6 +264,7 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { /** * Remove all filter values associated with a given field * @param {string} field The facet field + * @param {string} changedBy The identifier of the calling component. This is used for filterChangeCallback. */ function removeFiltersForField(field, changedBy) { const filter = getFilter(field) @@ -227,6 +278,19 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { } } + /** + * Set a specific filter value in a given field + * @param {string} field The facet field + * @param {string} value The filter value to be set + * @param {string} changedBy The identifier of the calling component. This is used for filterChangeCallback. + * + * @example + * // Set the filter value "Dataset" from the "entity_type" facet + * setFilter("entity_type", "Dataset", "MyComponent") + * + * // Set the range filter value from the "created_timestamp" facet + * setFilter("created_timestamp", { from: 1690156800000, to: 1692921599999, name: "created_timestamp" }, "MyComponent") + */ function setFilter(field, value, changedBy) { const facets = driver.searchQuery.facets || {} const facet = facets[field] @@ -254,7 +318,11 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { function getLocalSettings() { return JSON.parse(localStorage.getItem(`${name}.settings`)) || {} } - + + /** + * @param {string} field The facet field + * @returns {boolean} Whether or not the facet is expanded + */ function isFacetExpanded(field) { const settings = getLocalSettings() if (settings.hasOwnProperty(field)) { @@ -264,6 +332,10 @@ export function SearchUIProvider({ children, name = 'new.entities' }) { } } + /** + * @param {string} field The facet field + * @param {boolean} value Whether or not the facet should be expanded + */ function setFacetExpanded(field, value) { const settings = getLocalSettings() settings[field] = { isExpanded: value } From 4318fa568abc99eb667c261ce895c33064c2e592 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 1 Sep 2023 11:25:46 -0400 Subject: [PATCH 19/23] Only remove .filters and .settings from storage --- components/core/SearchUIContext.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 8c8ce7c..ee3bf48 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -306,7 +306,11 @@ export function SearchUIProvider({ name, children }) { function checkLocalStorageSchema() { const schemaVersion = localStorage.getItem('schemaVersion') || 0 if (schemaVersion < LOCAL_SCHEMA_VERSION) { - localStorage.clear() + Object.keys(localStorage).forEach((key) => { + if (key.endsWith('.filters') || key.endsWith('.settings')) { + localStorage.removeItem(key) + } + }) localStorage.setItem('schemaVersion', LOCAL_SCHEMA_VERSION) } } From 9219f009da89aa954384a7472bf74c973063da4a Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 1 Sep 2023 11:52:30 -0400 Subject: [PATCH 20/23] Remove old unused components --- components/core/CheckboxOptionFacet.js | 61 ------ components/core/CollapsableCheckboxFacet.js | 83 ------- components/core/CollapsableDateRangeFacet.js | 203 ------------------ .../core/CollapsableNumericRangeFacet.js | 182 ---------------- 4 files changed, 529 deletions(-) delete mode 100644 components/core/CheckboxOptionFacet.js delete mode 100644 components/core/CollapsableCheckboxFacet.js delete mode 100644 components/core/CollapsableDateRangeFacet.js delete mode 100644 components/core/CollapsableNumericRangeFacet.js diff --git a/components/core/CheckboxOptionFacet.js b/components/core/CheckboxOptionFacet.js deleted file mode 100644 index db7b91c..0000000 --- a/components/core/CheckboxOptionFacet.js +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect } from "react"; -import {Sui} from "../../lib/search-tools"; - -const CheckboxOptionFacet = ({ - label, - option, - transformFunction, - formatVal, - onSelect, - onRemove - }) => { - const value = option.value; - - const getChecked = () => { - let filters = Sui.getFilters() - const selected = filters[`${option.key}.${option.value}`]?.selected || option.selected - filters[`${option.key}.${option.value}`] = {selected, key: option.key} - Sui.saveFilters(filters) - return selected - } - - const clearCheck = (value) => { - let filters = Sui.getFilters() - filters[`${option.key}.${option.value}`].selected = false - Sui.saveFilters(filters) - onRemove(value) - } - - const setCheck = (value) => { - let filters = Sui.getFilters() - filters[`${option.key}.${option.value}`].selected = true - Sui.saveFilters(filters) - onSelect(value) - } - - return ( - - ) -} - -export default CheckboxOptionFacet \ No newline at end of file diff --git a/components/core/CollapsableCheckboxFacet.js b/components/core/CollapsableCheckboxFacet.js deleted file mode 100644 index 1405e48..0000000 --- a/components/core/CollapsableCheckboxFacet.js +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from "react"; -import CheckboxOptionFacet from "./CheckboxOptionFacet"; -import CollapsableLayout from "./CollapsableLayout"; -import {Sui} from "../../lib/search-tools"; -import FacetContainer from "./FacetContainer"; - -const CollapsableCheckboxFacet = ({facet, transformFunction, formatVal}) => { - const label = facet[1].label; - const facetKey = facet[0]; - const [isExpanded, setIsExpanded] = useState(Sui.isExpandedFacetCategory(facet, facetKey)); - - return ( - options.length > 0 && - - -
- {showSearch && ( -
- { - onSearch(e.target.value); - }} - /> -
- )} - -
- {options.length < 1 &&
No matching options
} - {options.map((option) => { - return - })} -
- - {showMore && ( - - )} -
-
- )} - /> -}; - -export default CollapsableCheckboxFacet; \ No newline at end of file diff --git a/components/core/CollapsableDateRangeFacet.js b/components/core/CollapsableDateRangeFacet.js deleted file mode 100644 index 8c3a278..0000000 --- a/components/core/CollapsableDateRangeFacet.js +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { withSearch } from "@elastic/react-search-ui"; -import styles from "../../css/collapsableFacets.module.css"; -import CollapsableLayout from "./CollapsableLayout"; -import {Sui} from "../../lib/search-tools"; - -const CollapsableDateRangeFacet = ({ facet, formatVal, filters, setFilter, removeFilter }) => { - const label = facet[1].label; - const field = facet[1].field.replace(".keyword", ""); - const initialValues = getInitialValues() - - const [isExpanded, setIsExpanded] = useState(Sui.isExpandedDateCategory(facet, field)); - - // default dates - const DEFAULT_MIN_DATE = "1970-01-01"; - const DEFAULT_MAX_DATE = "2300-01-01"; - - // States - const [startDate, setStartDate] = useState(initialValues[0]); - const [endDate, setEndDate] = useState(initialValues[1]); - - function getInitialValues() { - const filters = Sui.getFilters() - const filter = filters[field] ?? {} - const min = (filter.from) ? new Date(filter.from).toISOString().split("T")[0] : "" - const max = (filter.to) ? new Date(filter.to).toISOString().split("T")[0] : "" - return [min, max] - } - - // These initial values constrain the date input to 4 digit years. Inputs will change widths without them. - const [startMaxDate, setStartMaxDate] = useState(DEFAULT_MAX_DATE); - const [endMinDate, setEndMinDate] = useState(DEFAULT_MIN_DATE); - - const [startDateError, setStartDateError] = useState(""); - const [endDateError, setEndDateError] = useState(""); - - const handleExpanded = () => { - let filters = Sui.getFilters() - if (typeof filters[field] !== "object") { - filters[field] = {} - } - filters[field].key = field - filters[field].isExpanded = !isExpanded - Sui.saveFilters(filters) - setIsExpanded(!isExpanded) - } - - const setInputsFromSui = () => { - const filters = Sui.getFilters() - if (typeof filters[field] !== "object") { - filters[field] = {} - } - const start = filters[field].from - const end = filters[field].to - - if (start) { - const startStr = new Date(start).toISOString().split("T")[0] - setEndMinDate(startStr) - setStartDate(startStr) - } else { - setStartDate("") - setEndMinDate(DEFAULT_MIN_DATE) - } - - if (end) { - const endStr = new Date(end).toISOString().split("T")[0] - setStartMaxDate(endStr) - setEndDate(endStr) - } else { - setEndDate("") - setStartMaxDate(DEFAULT_MAX_DATE) - } - } - - useEffect(() => { - setInputsFromSui() - }, [filters]) - - useEffect(() => { - const filter = {} - - const startTimestamp = Date.parse(`${startDate}T00:00:00.000Z`) - if (startTimestamp && startTimestamp >= 0) { - filter.from = startTimestamp - } - - const endTimestamp = Date.parse(`${endDate}T00:00:00.000Z`) - if (endTimestamp && endTimestamp >= 0) { - // Add 24 hours minus 1 ms to the end date so inclusive of the end date - filter.to = endTimestamp + 24 * 60 * 60 * 1000 - 1 - } - - const sui = Sui.getFilters() - if (typeof sui[field] !== "object") { - sui[field] = {} - } - - if (Object.keys(filter).length < 1) { - const found = filters.find((f) => f.field === field) - if (found) { - removeFilter(found.field, found.value, facet[1]["filterType"]) - sui[field] = { key: field, isExpanded: sui[field].isExpanded ?? false} - } - } else { - sui[field] = {...filter, key: field, isExpanded: sui[field].isExpanded ?? false} - filter.name = field - const foundFilter = filters.find((f) => f.values === filter) - setFilter(field, filter, facet[1]["filterType"]) - } - Sui.saveFilters(sui) - }, [startDate, endDate]) - - function handleDateChange(targetName, dateStr) { - // Validate date - const timestamp = Date.parse(`${dateStr}T00:00:00.000Z`) - const otherTimestamp = Date.parse(`${targetName === "startdate" ? endDate : startDate}T00:00:00.000Z`) - - if (timestamp && otherTimestamp) { - if (targetName === "startdate") { - if (timestamp > otherTimestamp) { - setStartDateError("Start date must be before end date") - setEndDateError("") - } else { - setStartDateError("") - setEndDateError("") - } - } - - if (targetName === "enddate") { - if (timestamp < otherTimestamp) { - setStartDateError("") - setEndDateError("End date must be after start date") - } else { - setStartDateError("") - setEndDateError("") - } - } - } - - if (targetName === "startdate") { - setStartDate(dateStr) - if (timestamp) { - setEndMinDate(dateStr) - } - } else { - setEndDate(dateStr) - if (timestamp) { - setStartMaxDate(dateStr) - } - } - } - - return ( - - -
- Start Date - handleDateChange("startdate", e.target.value)} - required - pattern="\d{4}-\d{2}-\d{2}" - /> -
-
- End Date - handleDateChange("enddate", e.target.value)} - required - pattern="\d{4}-\d{2}-\d{2}" - /> -
-
- {startDateError && {startDateError}} - {endDateError && {endDateError}} -
-
- ) -} - -export default withSearch(({ filters, setFilter, removeFilter }) => ({ - filters, - setFilter, - removeFilter, -}))(CollapsableDateRangeFacet) \ No newline at end of file diff --git a/components/core/CollapsableNumericRangeFacet.js b/components/core/CollapsableNumericRangeFacet.js deleted file mode 100644 index 8fcb698..0000000 --- a/components/core/CollapsableNumericRangeFacet.js +++ /dev/null @@ -1,182 +0,0 @@ -import {useState} from "react" -import {withSearch} from "@elastic/react-search-ui" -import CollapsableLayout from "./CollapsableLayout" -import {Sui} from "../../lib/search-tools" -import Slider from "@mui/material/Slider" -import FacetContainer from "./FacetContainer" -import Histogram from "./Histogram"; - -const NumericRangeFacet = ({ - label, - field, - valueRange, - aggregations, - filters, - onChange, - onRemove - }) => { - const [values, setValues] = useState(getInitialValues()) - - function getInitialValues() { - const filters = Sui.getFilters() - const filter = filters[field] ?? {} - const min = filter.from ?? valueRange[0] - const max = filter.to ?? valueRange[1] - return [parseInt(min), parseInt(max)] - } - - const marks = [ - { - value: valueRange[0], - label: valueRange[0], - }, - { - value: valueRange[1], - label: valueRange[1], - } - ] - - function updateFilters(values) { - const minValue = values[0] !== "" ? values[0] : valueRange[0] - const maxValue = values[1] !== "" ? values[1] : valueRange[1] - - const filter = {} - if (minValue !== valueRange[0]) { - filter.from = minValue - } - - if (maxValue !== valueRange[1]) { - filter.to = maxValue - } - - let f = Sui.getFilters() - if (!f[field]) { - f[field] = {key: field} - } - delete f[field].from - delete f[field].to - - if (Object.keys(filter).length < 1) { - Sui.saveFilters(f) - const found = filters.find((f) => f.field === field) - if (found) { - found.values.forEach((val) => onRemove(val)) - } - } else { - filter.name = field - f[field].key = field - if (filter.from) { - f[field].from = minValue - } - if (filter.to) { - f[field].to = maxValue - } - Sui.saveFilters(f) - onChange(filter) - } - } - - function handleSliderChange(_, newValues) { - setValues(newValues) - } - - function handleSliderCommitted(_, newValues) { - updateFilters(newValues) - } - - function valueText(value) { - return `${value}` - } - - return ( - <> -
-
- - { - label - }} - marks={marks} - value={values} - min={valueRange[0]} - max={valueRange[1]} - onChange={handleSliderChange} - valueLabelDisplay='auto' - getAriaValueText={valueText} - /> -
-
- - ) -} - -const CollapsableNumericRangeFacet = ({facet, rawResponse, formatVal, filters}) => { - const label = facet[1].label - let aggregations = null - if (rawResponse.hasOwnProperty("aggregations") && rawResponse["aggregations"].hasOwnProperty([facet[0] + "_histogram"])) { - aggregations = rawResponse["aggregations"][facet[0] + "_histogram"]["buckets"] - } - const field = facet[1].field.replace(".keyword", "") - const facetKey = facet[0] - const [isExpanded, setIsExpanded] = useState(Sui.isExpandedNumericCategory(facet, facetKey)) - - const handleExpanded = () => { - let f = Sui.getFilters() - if (!f[field]) { - f[field] = {key: field} - } - f[field].key = field - f[field].isExpanded = !isExpanded - Sui.saveFilters(f) - setIsExpanded(!isExpanded) - } - - return ( - ( - -
-
- -
-
-
- )} - /> - ) -} - -export default withSearch(({filters, setFilter, removeFilter}) => ({ - filters, - setFilter, - removeFilter, -}))(CollapsableNumericRangeFacet) From 736eb7241e9f9a766e72d10f7f024a39c95ae31b Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 8 Sep 2023 11:36:48 -0400 Subject: [PATCH 21/23] Add optional name prop in search ui provider --- components/core/SearchUIContext.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index ee3bf48..19b1c87 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -5,7 +5,7 @@ const SearchUIContext = createContext() /** * Provider to get access to the SearchUIContext - * @param {string} name The name of the search UI. This is used to namespace local storage. + * @param {string} name The name of the search UI. This is used to namespace local storage. If not provided, local storage will not be used. */ export function SearchUIProvider({ name, children }) { // Used to check if local storage should be cleared @@ -39,7 +39,9 @@ export function SearchUIProvider({ name, children }) { setRawResponse(driver.state.rawResponse) setWasSearched(driver.state.wasSearched) - localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) + if (name) { + localStorage.setItem(`${name}.filters`, JSON.stringify(driver.state.filters)) + } removeInvalidConditionalFacets() }, [driver.state]) @@ -316,10 +318,12 @@ export function SearchUIProvider({ name, children }) { } function getLocalFilters() { + if (!name) return [] return JSON.parse(localStorage.getItem(`${name}.filters`)) || [] } function getLocalSettings() { + if (!name) return {} return JSON.parse(localStorage.getItem(`${name}.settings`)) || {} } @@ -341,6 +345,7 @@ export function SearchUIProvider({ name, children }) { * @param {boolean} value Whether or not the facet should be expanded */ function setFacetExpanded(field, value) { + if (!name) return const settings = getLocalSettings() settings[field] = { isExpanded: value } localStorage.setItem(`${name}.settings`, JSON.stringify(settings)) From 1d428c25c2aebe149705496ea003067d4412fe68 Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 8 Sep 2023 12:09:24 -0400 Subject: [PATCH 22/23] Add check to ensure local storage filters is array --- components/core/SearchUIContext.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/core/SearchUIContext.jsx b/components/core/SearchUIContext.jsx index 19b1c87..72879b8 100644 --- a/components/core/SearchUIContext.jsx +++ b/components/core/SearchUIContext.jsx @@ -317,9 +317,15 @@ export function SearchUIProvider({ name, children }) { } } + /** + * Get the local filters from local storage in the given namespace + * @return {Array} An array of ES filter objects. See filterExists for object structures. + */ function getLocalFilters() { if (!name) return [] - return JSON.parse(localStorage.getItem(`${name}.filters`)) || [] + const localFilters = JSON.parse(localStorage.getItem(`${name}.filters`)) || [] + if (!Array.isArray(localFilters)) return [] + return localFilters } function getLocalSettings() { From 31d791b8a42905e54d016b350dfdf8fa56f44ede Mon Sep 17 00:00:00 2001 From: Tyler Madonna Date: Fri, 8 Sep 2023 12:52:53 -0400 Subject: [PATCH 23/23] Fix label in date and numeric range facets --- components/core/DateRangeFacet.jsx | 4 +++- components/core/NumericRangeFacet.jsx | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/core/DateRangeFacet.jsx b/components/core/DateRangeFacet.jsx index bd28fa3..32e1b88 100644 --- a/components/core/DateRangeFacet.jsx +++ b/components/core/DateRangeFacet.jsx @@ -2,11 +2,13 @@ import { useContext, useEffect, useState } from 'react' import SearchUIContext from './SearchUIContext' import styles from '../../css/collapsableFacets.module.css' -const DateRangeFacet = ({ field, label, formatVal }) => { +const DateRangeFacet = ({ field, facet, formatVal }) => { // default dates const DEFAULT_MIN_DATE = '1970-01-01' const DEFAULT_MAX_DATE = '2300-01-01' + const label = facet.label + const { registerFilterChangeCallback, unregisterFilterChangeCallback, getFilter, setFilter, removeFiltersForField, filterExists } = useContext(SearchUIContext) const [values, setValues] = useState(getValuesFromFilter()) diff --git a/components/core/NumericRangeFacet.jsx b/components/core/NumericRangeFacet.jsx index d525709..ccdc583 100644 --- a/components/core/NumericRangeFacet.jsx +++ b/components/core/NumericRangeFacet.jsx @@ -3,7 +3,7 @@ import SearchUIContext from './SearchUIContext' import Slider from '@mui/material/Slider' import Histogram from './Histogram' -const NumericRangeFacet = ({ field, label, facet }) => { +const NumericRangeFacet = ({ field, facet }) => { const valueRange = facet.uiRange const {registerFilterChangeCallback, unregisterFilterChangeCallback, getFilter, setFilter, removeFiltersForField, aggregations } = useContext(SearchUIContext) @@ -85,9 +85,7 @@ const NumericRangeFacet = ({ field, label, facet }) => { onChangeCommitted={handleSliderCommitted} style={{ color: '#0d6efd' }} size='small' - getAriaLabel={() => { - label - }} + getAriaLabel={() => { facet.label }} marks={marks} value={values} min={valueRange[0]}