diff --git a/searchlib/components/Facets/Facet.jsx b/searchlib/components/Facets/Facet.jsx index 03166e28d..0d4701346 100644 --- a/searchlib/components/Facets/Facet.jsx +++ b/searchlib/components/Facets/Facet.jsx @@ -22,6 +22,9 @@ const FacetContainer = (props) => { label, view, isFilterable = false, + onSelect, + onChange, + onRemove, ...rest } = props; const searchContext = useSearchContext(); @@ -66,12 +69,15 @@ const FacetContainer = (props) => { label={label} onRemove={(value) => { removeFilter(field, value, filterType); + onRemove && onRemove(field, value, filterType); }} onChange={(value) => { setFilter(field, value, filterType); + onChange && onChange(field, value, filterType); }} onSelect={(value) => { addFilter(field, value, filterType); + onSelect && onSelect(field, value, filterType); }} options={facetValues} values={selectedValues} diff --git a/searchlib/components/Result/HorizontalCardItem.jsx b/searchlib/components/Result/HorizontalCardItem.jsx index fac8f0c52..5e1a89084 100644 --- a/searchlib/components/Result/HorizontalCardItem.jsx +++ b/searchlib/components/Result/HorizontalCardItem.jsx @@ -61,6 +61,7 @@ const HorizontalCardItem = (props) => { const UniversalCard = registry.resolve['UniversalCard'].component; const item = { + '@id': result.href, title: ( <> diff --git a/searchlib/components/SearchApp/BasicSearchApp.jsx b/searchlib/components/SearchApp/BasicSearchApp.jsx index 28e526a7b..787131dd5 100644 --- a/searchlib/components/SearchApp/BasicSearchApp.jsx +++ b/searchlib/components/SearchApp/BasicSearchApp.jsx @@ -89,6 +89,7 @@ export default function BasicSearchApp(props) { paramOnAutocomplete, initialState, uniqueId, + mode, }); // console.log('driver', driverInstance); diff --git a/searchlib/components/SearchApp/FacetApp.jsx b/searchlib/components/SearchApp/FacetApp.jsx index 7cd462a02..9421e96d5 100644 --- a/searchlib/components/SearchApp/FacetApp.jsx +++ b/searchlib/components/SearchApp/FacetApp.jsx @@ -1,12 +1,15 @@ /** * A Search app that wraps and provides access to a single facet + * + * Note, unlike BasicSearchApp, this is not a standalone search app, + * it needs to be executed in the context of a search app */ import React from 'react'; -import { isEqual } from 'lodash'; -import useDeepCompareEffect from 'use-deep-compare-effect'; +// import { isEqual } from 'lodash'; +// import useDeepCompareEffect from 'use-deep-compare-effect'; import { Facet as SUIFacet } from '@eeacms/search/components'; -import { useSearchContext } from '@eeacms/search/lib/hocs'; // , useSearchDriver +import { useSearchContext, useSearchDriver } from '@eeacms/search/lib/hocs'; // , useSearchDriver // const sorter = (fa, fb) => // fa.field === fb.field ? 0 : fa.field < fb.field ? -1 : 0; @@ -15,7 +18,7 @@ export default function FacetApp(props) { const searchContext = useSearchContext(); const { appConfig, registry, field, onChange, value } = props; // const { field, onChange, value } = props; - // const driver = useSearchDriver(); + const driver = useSearchDriver(); // console.log({ searchContext, props, driver }); const { filters, removeFilter, addFilter } = searchContext; // driver.state @@ -54,7 +57,7 @@ export default function FacetApp(props) { // } // }, [value, filters, field, setFilter, driver]); // searchContext - const activeValue = filters.find((f) => f.field === field); + // const activeValue = filters.find((f) => f.field === field); // const dirty = !isEqual(activeValue, value); // console.log('redraw facet', { value, activeValue, dirty }); @@ -63,15 +66,15 @@ export default function FacetApp(props) { // const sortedFilters = [...filters].sort(sorter); - useDeepCompareEffect(() => { - timerRef.current && clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => { - if (!isEqual(activeValue, value)) { - // console.log('onchange', { activeValue, value }); - onChange(activeValue); - } - }, 200); - }, [removeFilter, field, activeValue, value, onChange]); + // useDeepCompareEffect(() => { + // timerRef.current && clearTimeout(timerRef.current); + // timerRef.current = setTimeout(() => { + // if (!isEqual(activeValue, value)) { + // console.log('onchange', { activeValue, value }); + // onChange(activeValue); + // } + // }, 200); + // }, [removeFilter, field, activeValue, value, onChange]); React.useEffect( () => () => { @@ -125,6 +128,21 @@ export default function FacetApp(props) { view={FacetComponent} filterType={localFilterType} onChangeFilterType={onChangeFilterType} + onRemove={() => { + const filter = driver.state.filters.find((f) => f.field === field); + onChange(filter); + // console.log('onRemove', JSON.stringify(driver.state.filters)); + }} + onChange={() => { + const filter = driver.state.filters.find((f) => f.field === field); + onChange(filter); + // console.log('onChange', JSON.stringify(driver.state.filters)); + }} + onSelect={() => { + const filter = driver.state.filters.find((f) => f.field === field); + onChange(filter); + // console.log('onSelect', JSON.stringify(driver.state.filters)); + }} /> ); } diff --git a/searchlib/components/SearchApp/useSearchApp.js b/searchlib/components/SearchApp/useSearchApp.js index b8b9cc26c..7a869991d 100644 --- a/searchlib/components/SearchApp/useSearchApp.js +++ b/searchlib/components/SearchApp/useSearchApp.js @@ -46,11 +46,11 @@ export default function useSearchApp(props) { paramOnSearch, paramOnAutocomplete, initialState, + mode = 'view', } = props; // useWhyDidYouUpdate('sss', props); const appConfig = React.useMemo(() => { - // console.log('redo appConfig'); return { ...applyConfigurationSchema(rebind(registry.searchui[appName])), appName, @@ -101,9 +101,21 @@ export default function useSearchApp(props) { // we don't want to track the URL if our search app is configured as // a simple separate app (for ex. search input or landing page that // trampolines to another instance) - trackUrlState: appConfig.url ? false : appConfig.trackUrlState, + trackUrlState: + mode === 'edit' + ? false + : appConfig.url + ? false + : appConfig.trackUrlState, }), - [appConfig, onAutocomplete, onSearch, locationSearchTerm, initialState], + [ + appConfig, + onAutocomplete, + onSearch, + locationSearchTerm, + initialState, + mode, + ], ); const { facetOptions } = React.useState(useFacetsWithAllOptions(appConfig)); diff --git a/searchlib/components/SearchView/FilterAsideContentView.jsx b/searchlib/components/SearchView/FilterAsideContentView.jsx index 642cbb26c..015c5db84 100644 --- a/searchlib/components/SearchView/FilterAsideContentView.jsx +++ b/searchlib/components/SearchView/FilterAsideContentView.jsx @@ -70,33 +70,37 @@ export const FilterAsideContentView = (props) => { const loadingAtom = loadingFamily(appConfig.appName); const isLoading = useAtomValue(loadingAtom); + const { showFilters, showFacets, showClusters, showSorting } = appConfig; + return ( <> {appConfig.mode === 'edit' && (
Active filters are always shown in edit mode
)} - {(appConfig.showFilters || appConfig.mode === 'edit') && ( - - )} - {appConfig.showFilters && } + {(showFilters || appConfig.mode === 'edit') && } + {showClusters && }
- {appConfig.showFilters && ( + {(showFacets || showSorting) && (
- + {showFacets && }
- - - {/* */} + {showSorting && ( + <> + + + {/* */} + + )}
)} diff --git a/searchlib/lib/utils.js b/searchlib/lib/utils.js index 608150d71..2049bcd74 100644 --- a/searchlib/lib/utils.js +++ b/searchlib/lib/utils.js @@ -63,6 +63,7 @@ export function applyConfigurationSchema(config) { config.disjunctiveFacets.push(facet.field); } }); + return config; } diff --git a/searchlib/registry.js b/searchlib/registry.js index f3f56bd80..a9c8b695a 100644 --- a/searchlib/registry.js +++ b/searchlib/registry.js @@ -275,7 +275,12 @@ const config = { defaultPromptQueries: [], // offered as possible queries, in a prompt below text input. One per line promptQueryInterval: 20000, alwaysSearchOnInitialLoad: false, // used in elastic search driver + showFilters: true, // enables the filters interface, to allow falling back to just a simple results list + showClusters: true, // enables the tab clusters + showSorting: true, // show the sorting controls + showFacets: true, // show the facets dropdowns and sidebar facets + getActiveFilters: 'defaultGetActiveFilters', // highlight: { diff --git a/src/SearchBlock/SearchBlockView.jsx b/src/SearchBlock/SearchBlockView.jsx index d65d03a55..6ce13305c 100644 --- a/src/SearchBlock/SearchBlockView.jsx +++ b/src/SearchBlock/SearchBlockView.jsx @@ -24,7 +24,19 @@ function SearchBlockView(props) { path, } = props; const { appName = 'default' } = data; - const blacklist = ['slotFills', 'defaultFilters', 'defaultSort']; + + const schemaFields = [ + 'availableFacets', + 'defaultFacets', + 'defaultFilters', + 'showClusters', + 'defaultSort', + 'showFacets', + 'showFilters', + 'showSorting', + ]; // mutating data in these fields makes the search engine to lose facets + + const blacklist = ['slotFills', ...(mode === 'edit' ? schemaFields : [])]; const stableData = useDebouncedStableData( Object.assign( diff --git a/src/SearchBlock/hocs.js b/src/SearchBlock/hocs.js index a4d1c45d6..9ba5a017c 100644 --- a/src/SearchBlock/hocs.js +++ b/src/SearchBlock/hocs.js @@ -1,7 +1,7 @@ import React from 'react'; import { isEqual } from 'lodash'; -export const useDebouncedStableData = (data, timeout = 100) => { +export const useDebouncedStableData = (data, timeout = 1000) => { const [stableData, setStableData] = React.useState(data); const timer = React.useRef(); diff --git a/src/SearchBlock/templates/SearchResultsView.jsx b/src/SearchBlock/templates/SearchResultsView.jsx index dd487412a..f7fe70f99 100644 --- a/src/SearchBlock/templates/SearchResultsView.jsx +++ b/src/SearchBlock/templates/SearchResultsView.jsx @@ -78,6 +78,11 @@ SearchResultsView.schemaEnhancer = ({ schema, formData }) => { 'defaultResultView', 'alwaysSearchOnInitialLoad', 'showFilters', + 'showFacets', + 'showSorting', + 'showClusters', + 'availableFacets', + 'defaultFacets', 'defaultFilters', 'defaultSort', ], @@ -93,17 +98,45 @@ SearchResultsView.schemaEnhancer = ({ schema, formData }) => { configPath: 'alwaysSearchOnInitialLoad', }, showFilters: { - title: 'Show filters?', + title: 'Show active filters?', type: 'boolean', default: true, configPath: 'showFilters', }, + showFacets: { + title: 'Show facets?', + type: 'boolean', + default: true, + configPath: 'showFacets', + }, + showClusters: { + title: 'Show tab clusters?', + type: 'boolean', + default: true, + configPath: 'showClusters', + }, + showSorting: { + title: 'Show sorting?', + type: 'boolean', + default: true, + configPath: 'showSorting', + }, defaultFilters: { - title: 'Default filters', + title: 'Pre-applied filters', widget: 'object_list', schema: FilterSchema({ formData }), schemaExtender: (schema) => schema, }, + availableFacets: { + title: 'Available Facets', + widget: 'array', + choices: [], + }, + defaultFacets: { + title: 'Default Facets', + widget: 'array', + choices: [], + }, defaultSort: { title: 'Default sort', widget: 'sort_widget', @@ -113,6 +146,15 @@ SearchResultsView.schemaEnhancer = ({ schema, formData }) => { if (appConfig) { const { resultViews } = appConfig; + // console.log(appConfig); + + const availableFacets = appConfig.facets?.map(({ field, label }) => [ + field, + label, + ]); + + schema.properties.availableFacets.choices = availableFacets; + schema.properties.defaultFacets.choices = availableFacets; // fill in defaultResultView choices schema.properties.defaultResultView = { diff --git a/src/SearchBlock/utils.js b/src/SearchBlock/utils.js index 1bd06b014..70adfeeed 100644 --- a/src/SearchBlock/utils.js +++ b/src/SearchBlock/utils.js @@ -45,6 +45,37 @@ const _applyBlockSettings = (config, appName, data, schema) => { view.isDefault = view.id === data.defaultResultView; }); } + + // console.log(settings, data); + const availableFacets = [ + ...(data.availableFacets || []), + ...(data.defaultFacets || []), + ]; + + if (data.availableFacets) { + settings.facets.forEach((f) => { + f.showInFacetsList = availableFacets.indexOf(f.field) > -1 ? true : false; + }); + } + + if (data.defaultFacets) { + settings.facets.forEach((f) => { + f.alwaysVisible = data.defaultFacets.indexOf(f.field) > -1 ? true : false; + }); + } + + if (data.defaultFilters) { + const filters = data.defaultFilters + .map((f) => ({ [f.name]: f.value })) + .reduce((acc, cur) => ({ ...acc, ...cur })); + settings.facets.forEach((f) => { + if (filters[f.field]) { + f.default = filters[f.field]; + } + }); + } + // console.log(data, settings); + return config; };