design-comuni-plone-theme
Version:
Volto Theme for Italia design guidelines
357 lines (315 loc) • 10.2 kB
JSX
/* CUSTOMIZATIONS:
- Read puntual comments in code
*/
import qs from 'query-string';
import React from 'react';
import { useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { resolveExtension } from '@plone/volto/helpers/Extensions/withBlockExtensions';
import config from '@plone/volto/registry';
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
const SEARCH_ENDPOINT_FIELDS = [
'SearchableText',
'b_size',
'limit',
'sort_on',
'sort_order',
];
const PAQO = 'plone.app.querystring.operation';
/**
* Based on URL state, gets an initial internal state for the search
*
* @function getInitialState
*
*/
function getInitialState(data, facets, urlSearchText, id) {
const { types: facetWidgetTypes } =
config.blocks.blocksConfig.search.extensions.facetWidgets;
const facetSettings = data?.facets || [];
return {
query: [
...(data.query?.query || []),
...(facetSettings || [])
.map((facet) => {
if (!facet?.field) return null;
const { valueToQuery } = resolveExtension(
'type',
facetWidgetTypes,
facet,
);
const name = facet.field.value;
const value = facets[name];
return valueToQuery({ value, facet });
})
.filter((f) => !!f),
...(urlSearchText
? [
{
i: 'SearchableText',
o: 'plone.app.querystring.operation.string.contains',
v: urlSearchText,
},
]
: []),
],
sort_on: data.query?.sort_on,
sort_order: data.query?.sort_order,
b_size: data.query?.b_size,
limit: data.query?.limit,
block: id,
};
}
/**
* "Normalizes" the search state to something that's serializable
* (for querying) and used to compute data for the ListingBody
*
* @function normalizeState
*
*/
function normalizeState({
query, // base query
facets, // facet values
id, // block id
searchText, // SearchableText
sortOn,
sortOrder,
facetSettings, // data.facets extracted from block data
}) {
const { types: facetWidgetTypes } =
config.blocks.blocksConfig.search.extensions.facetWidgets;
const params = {
query: [
...(query.query || []),
...(facetSettings || []).map((facet) => {
if (!facet?.field) return null;
const { valueToQuery } = resolveExtension(
'type',
facetWidgetTypes,
facet,
);
const name = facet.field.value;
const value = facets[name];
return valueToQuery({ value, facet });
}),
].filter((o) => !!o),
sort_on: sortOn || query.sort_on,
sort_order: sortOrder || query.sort_order,
b_size: query.b_size,
limit: query.limit,
block: id,
};
// Cani infami, se tolgo la searchabletext non funzionera' mai cosi'
// Note Ideally the searchtext functionality should be restructured as being just
// another facet. But right now it's the same. This means that if a searchText
// is provided, it will override the SearchableText facet.
// If there is no searchText, the SearchableText in the query remains in effect.
// TODO eventually the searchText should be a distinct facet from SearchableText, and
// the two conditions could be combined, in comparison to the current state, when
// one overrides the other.
if (searchText) {
params.query = params.query.reduce(
// Remove SearchableText from query
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
[],
);
params.query.push({
i: 'SearchableText',
o: 'plone.app.querystring.operation.string.contains',
v: searchText,
});
} else if (
!searchText &&
params.query.some((kvp) => kvp.i === 'SearchableText')
)
params.query = params.query.reduce(
// Remove SearchableText from query
(acc, kvp) => (kvp.i === 'SearchableText' ? acc : [...acc, kvp]),
[],
);
return params;
}
const getSearchFields = (searchData) => {
return Object.assign(
{},
...SEARCH_ENDPOINT_FIELDS.map((k) => {
return searchData[k] ? { [k]: searchData[k] } : {};
}),
searchData.query ? { query: serializeQuery(searchData['query']) } : {},
);
};
/**
* A HOC that will mirror the search block state to a hash location
*/
const useHashState = () => {
const location = useLocation();
const history = useHistory();
const oldState = React.useMemo(() => {
return {
...qs.parse(location.search),
...qs.parse(location.hash),
};
}, [location.hash, location.search]);
// This creates a shallow copy. Why is this needed?
const current = Object.assign(
{},
...Array.from(Object.keys(oldState)).map((k) => ({ [k]: oldState[k] })),
);
const setSearchData = React.useCallback(
(searchData) => {
const newParams = qs.parse(location.hash);
let changed = false;
Object.keys(searchData)
.sort()
.forEach((k) => {
if (searchData[k]) {
newParams[k] = searchData[k];
if (oldState[k] !== searchData[k]) {
changed = true;
}
}
});
if (changed) {
history.push({
hash: qs.stringify(newParams),
});
}
},
[history, oldState, location.hash],
);
return [current, setSearchData];
};
/**
* A hook to make it possible to switch disable mirroring the search block
* state to the window location. When using the internal state we "start from
* scratch", as it's intended to be used in the edit page.
*/
const useSearchBlockState = (uniqueId, isEditMode) => {
const [hashState, setHashState] = useHashState();
const [internalState, setInternalState] = React.useState({});
return isEditMode
? [internalState, setInternalState]
: [hashState, setHashState];
};
// Simple compress/decompress the state in URL by replacing the lengthy string
const deserializeQuery = (q) => {
return JSON.parse(q)?.map((kvp) => ({
...kvp,
o: kvp.o.replace(/^paqo/, PAQO),
}));
};
const serializeQuery = (q) => {
return JSON.stringify(
q?.map((kvp) => ({ ...kvp, o: kvp.o.replace(PAQO, 'paqo') })),
);
};
const withSearch = (options) => (WrappedComponent) => {
const { inputDelay = 1000 } = options || {};
function WithSearch(props) {
const { data, id, editable = false, properties } = props;
const [locationSearchData, setLocationSearchData] = useSearchBlockState(
id,
editable,
);
const urlQuery = locationSearchData.query
? deserializeQuery(locationSearchData.query)
: [];
const urlSearchText =
locationSearchData.SearchableText ||
urlQuery.find(({ i }) => i === 'SearchableText')?.v ||
'';
// TODO: refactor, should use only useLocationStateManager()!!!
const [searchText, setSearchText] = React.useState(urlSearchText);
const [facets, setFacets] = React.useState(
Object.assign(
{},
// Cani infami parte due: la ricerca testuale NON E' una faccetta configurata
// ...urlQuery.map(({ i, v }) => ({ [i]: v })), // TODO: the 'o' should be kept. This would be a major refactoring of the facets
...urlQuery.reduce((acc, curr) => {
if (curr.i !== 'SearchableText') {
if (curr.o.includes('boolean')) return [...acc, { [curr.i]: true }];
else return [...acc, { [curr.i]: curr.v }];
} else return acc;
}, []),
),
);
const [sortOn, setSortOn] = React.useState(data?.query?.sort_on);
const [sortOrder, setSortOrder] = React.useState(data?.query?.sort_order);
const [searchData, setSearchData] = React.useState(
getInitialState(data, facets, urlSearchText, id),
);
const timeoutRef = React.useRef();
const facetSettings = data?.facets;
const deepQuery = JSON.stringify(data.query);
const onTriggerSearch = React.useCallback(
(
toSearchText = undefined,
toSearchFacets = undefined,
toSortOn = undefined,
toSortOrder = undefined,
) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(
() => {
const searchData = normalizeState({
id,
query: data.query || {},
facets: toSearchFacets || facets,
searchText:
toSearchText !== undefined ? toSearchText : searchText,
sortOn: toSortOn || sortOn,
sortOrder: toSortOrder || sortOrder,
facetSettings,
});
if (toSearchFacets) setFacets(toSearchFacets);
if (toSortOn) setSortOn(toSortOn);
if (toSortOrder) setSortOrder(toSortOrder);
setSearchData(searchData);
setLocationSearchData(getSearchFields(searchData));
},
toSearchFacets ? inputDelay / 3 : inputDelay,
);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
// Use deep comparison of data.query
deepQuery,
facets,
id,
setLocationSearchData,
searchText,
sortOn,
sortOrder,
facetSettings,
],
);
const querystringResults = useSelector(
(state) => state.querystringsearch.subrequests,
);
const subrequestID = properties?.UID + '-' + id;
const totalItems =
querystringResults[subrequestID]?.total ||
querystringResults[subrequestID]?.items?.length;
return (
<WrappedComponent
{...props}
searchData={searchData}
facets={facets}
setFacets={setFacets}
setSortOn={setSortOn}
setSortOrder={setSortOrder}
sortOn={sortOn}
sortOrder={sortOrder}
searchedText={urlSearchText}
searchText={searchText}
setSearchText={setSearchText}
onTriggerSearch={onTriggerSearch}
totalItems={totalItems}
/>
);
}
WithSearch.displayName = `WithSearch(${getDisplayName(WrappedComponent)})`;
return WithSearch;
};
export default withSearch;