Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(search): show facets count #210

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
@@ -0,0 +1,161 @@
import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import hoistNonReactStatics from 'hoist-non-react-statics';
import useDeepCompareEffect from 'use-deep-compare-effect';

import { getContent, getQueryStringResults } from '@plone/volto/actions';
import { usePagination, getBaseUrl } from '@plone/volto/helpers';

import config from '@plone/volto/registry';

function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

export default function withQuerystringResults(WrappedComponent) {
function WithQuerystringResults(props) {
const {
data = {},
id = data.block,
properties: content,
path,
variation,
} = props;
const { settings } = config;
const querystring = data.querystring || data; // For backwards compat with data saved before Blocks schema. Note, this is also how the Search block passes data to ListingBody
const subrequestID = content?.UID ? `${content?.UID}-${id}` : id;
const { b_size = settings.defaultPageSize } = querystring; // batchsize

// save the path so it won't trigger dispatch on eager router location change
const [initialPath] = React.useState(getBaseUrl(path));

const copyFields = [
'limit',
'query',
'sort_on',
'sort_order',
'depth',
'facets',
];
const { currentPage, setCurrentPage } = usePagination(id, 1);
const adaptedQuery = Object.assign(
variation?.fullobjects ? { fullobjects: 1 } : { metadata_fields: '_all' },
{
b_size: b_size,
},
...copyFields.map((name) =>
Object.keys(querystring).includes(name)
? { [name]: querystring[name] }
: {},
),
);
const adaptedQueryRef = useRef(adaptedQuery);
const currentPageRef = useRef(currentPage);

const querystringResults = useSelector(
(state) => state.querystringsearch.subrequests,
);
const dispatch = useDispatch();

const folderItems = content?.is_folderish ? content.items : [];
const hasQuery = querystring?.query?.length > 0;
const hasLoaded = hasQuery
? querystringResults?.[subrequestID]?.loaded
: true;

const listingItems = hasQuery
? querystringResults?.[subrequestID]?.items || []
: folderItems;

const showAsFolderListing = !hasQuery && content?.items_total > b_size;
const showAsQueryListing =
hasQuery && querystringResults?.[subrequestID]?.total > b_size;

const totalPages = showAsFolderListing
? Math.ceil(content.items_total / b_size)
: showAsQueryListing
? Math.ceil(querystringResults[subrequestID].total / b_size)
: 0;

const prevBatch = showAsFolderListing
? content.batching?.prev
: showAsQueryListing
? querystringResults[subrequestID].batching?.prev
: null;
const nextBatch = showAsFolderListing
? content.batching?.next
: showAsQueryListing
? querystringResults[subrequestID].batching?.next
: null;

const isImageGallery =
(!data.variation && data.template === 'imageGallery') ||
data.variation === 'imageGallery';

useDeepCompareEffect(() => {
if (hasQuery) {
dispatch(
getQueryStringResults(
initialPath,
adaptedQuery,
subrequestID,
currentPage,
),
);
} else if (isImageGallery && !hasQuery) {
// when used as image gallery, it doesn't need a query to list children
dispatch(
getQueryStringResults(
initialPath,
{
...adaptedQuery,
b_size: 10000000000,
query: [
{
i: 'path',
o: 'plone.app.querystring.operation.string.relativePath',
v: '',
},
],
},
subrequestID,
),
);
} else {
dispatch(getContent(initialPath, null, null, currentPage));
}
adaptedQueryRef.current = adaptedQuery;
currentPageRef.current = currentPage;
}, [
subrequestID,
isImageGallery,
adaptedQuery,
hasQuery,
initialPath,
dispatch,
currentPage,
]);

return (
<WrappedComponent
{...props}
onPaginationChange={(e, { activePage }) => setCurrentPage(activePage)}
total={querystringResults?.[subrequestID]?.total}
batch_size={b_size}
currentPage={currentPage}
totalPages={totalPages}
prevBatch={prevBatch}
nextBatch={nextBatch}
listingItems={listingItems}
hasLoaded={hasLoaded}
isFolderContentsListing={showAsFolderListing}
/>
);
}

WithQuerystringResults.displayName = `WithQuerystringResults(${getDisplayName(
WrappedComponent,
)})`;

return hoistNonReactStatics(WithQuerystringResults, WrappedComponent);
}
@@ -0,0 +1,100 @@
import React, { useEffect } from 'react';
import { defineMessages } from 'react-intl';
import { compose } from 'redux';

import { SidebarPortal, BlockDataForm } from '@plone/volto/components';
import { addExtensionFieldToSchema } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer';
import { getBaseUrl } from '@plone/volto/helpers';
import config from '@plone/volto/registry';

import { SearchBlockViewComponent } from './SearchBlockView';
import Schema from '@plone/volto/components/manage/Blocks/Search/schema';
import { withSearch, withQueryString, withFacetsCount } from './hocs';
import { cloneDeep } from 'lodash';

const messages = defineMessages({
template: {
id: 'Results template',
defaultMessage: 'Results template',
},
});

const SearchBlockEdit = (props) => {
const {
block,
onChangeBlock,
data,
selected,
intl,
navRoot,
contentType,
onTriggerSearch,
querystring = {},
} = props;
const { sortable_indexes = {} } = querystring;

let schema = Schema({ data, intl });

schema = addExtensionFieldToSchema({
schema,
name: 'listingBodyTemplate',
items: config.blocks.blocksConfig.listing.variations,
intl,
title: { id: intl.formatMessage(messages.template) },
});
const listingVariations = config.blocks.blocksConfig?.listing?.variations;
let activeItem = listingVariations.find(
(item) => item.id === data.listingBodyTemplate,
);
const listingSchemaEnhancer = activeItem?.schemaEnhancer;
if (listingSchemaEnhancer)
schema = listingSchemaEnhancer({
schema: cloneDeep(schema),
data,
intl,
});
schema.properties.sortOnOptions.items = {
choices: Object.keys(sortable_indexes).map((k) => [
k,
sortable_indexes[k].title,
]),
};

const { query = {} } = data || {};
// We don't need deep compare here, as this is just json serializable data.
const deepQuery = JSON.stringify(query);
useEffect(() => {
onTriggerSearch();
}, [deepQuery, onTriggerSearch]);

return (
<>
<SearchBlockViewComponent
{...props}
path={getBaseUrl(props.pathname)}
mode="edit"
/>
<SidebarPortal selected={selected}>
<BlockDataForm
schema={schema}
onChangeField={(id, value) => {
onChangeBlock(block, {
...data,
[id]: value,
});
}}
onChangeBlock={onChangeBlock}
formData={data}
navRoot={navRoot}
contentType={contentType}
/>
</SidebarPortal>
</>
);
};

export default compose(
withQueryString,
withFacetsCount,
withSearch(),
)(SearchBlockEdit);
@@ -0,0 +1,111 @@
import React from 'react';

import ListingBody from '@plone/volto/components/manage/Blocks/Listing/ListingBody';
import { withBlockExtensions } from '@plone/volto/helpers';

import config from '@plone/volto/registry';

import { withSearch, withQueryString, withFacetsCount } from './hocs';
import { compose } from 'redux';
import { useSelector } from 'react-redux';
import { isEqual, isFunction } from 'lodash';
import cx from 'classnames';

const getListingBodyVariation = (data) => {
const { variations } = config.blocks.blocksConfig.listing;

let variation = data.listingBodyTemplate
? variations.find(({ id }) => id === data.listingBodyTemplate)
: variations.find(({ isDefault }) => isDefault);

if (!variation) variation = variations[0];

return variation;
};

const isfunc = (obj) => isFunction(obj) || typeof obj === 'function';

const _filtered = (obj) =>
Object.assign(
{},
...Object.keys(obj).map((k) => {
const reject = k !== 'properties' && !isfunc(obj[k]);
return reject ? { [k]: obj[k] } : {};
}),
);

const blockPropsAreChanged = (prevProps, nextProps) => {
const prev = _filtered(prevProps);
const next = _filtered(nextProps);

return isEqual(prev, next);
};

const applyDefaults = (data, root) => {
const defaultQuery = [
{
i: 'path',
o: 'plone.app.querystring.operation.string.absolutePath',
v: root || '/',
},
];
return {
...data,
sort_on: data?.sort_on || 'effective',
sort_order: data?.sort_order || 'descending',
query: data?.query?.length ? data.query : defaultQuery,
};
};

const SearchBlockView = (props) => {
const { id, data, searchData, mode = 'view', variation, className } = props;

const Layout = variation.view;

const dataListingBodyVariation = getListingBodyVariation(data).id;
const [selectedView, setSelectedView] = React.useState(
dataListingBodyVariation,
);

// in the block edit you can change the used listing block variation,
// but it's cached here in the state. So we reset it.
React.useEffect(() => {
if (mode !== 'view') {
setSelectedView(dataListingBodyVariation);
}
}, [dataListingBodyVariation, mode]);

const root = useSelector((state) => state.breadcrumbs.root);
const listingBodyData = applyDefaults(searchData, root);

const { variations } = config.blocks.blocksConfig.listing;
const listingBodyVariation = variations.find(({ id }) => id === selectedView);

return (
<div className={cx('block search', selectedView, className)}>
<Layout
{...props}
isEditMode={mode === 'edit'}
selectedView={selectedView}
setSelectedView={setSelectedView}
>
<ListingBody
id={id}
variation={{ ...data, ...listingBodyVariation }}
data={listingBodyData}
path={props.path}
isEditMode={mode === 'edit'}
/>
</Layout>
</div>
);
};

export const SearchBlockViewComponent = compose(
withBlockExtensions,
(Component) => React.memo(Component, blockPropsAreChanged),
)(SearchBlockView);

export default withSearch()(
withQueryString(withFacetsCount(SearchBlockViewComponent)),
);