-
Notifications
You must be signed in to change notification settings - Fork 1
feat(search): show facets count #210
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
Changes from all commits
606d193
5711ee4
ac09ec1
9c8c893
566d6ce
e3d77f4
a5da887
edc80cc
2e14c6a
1d52333
86953d9
cd7be54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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 | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Same potential undefined access issue here - should check if
Suggested change
|
||||||
| : 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, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: This extremely large b_size value (10 billion) could cause performance issues and memory problems. Consider using a more reasonable limit. |
||||||
| 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); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: The
Suggested change
|
||||||
|
|
||||||
| const _filtered = (obj) => | ||||||
| Object.assign( | ||||||
| {}, | ||||||
| ...Object.keys(obj).map((k) => { | ||||||
| const reject = k !== 'properties' && !isfunc(obj[k]); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Logic in |
||||||
| 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)), | ||||||
| ); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Potential runtime error if
querystringResults[subrequestID]is undefined whenshowAsQueryListingis true. Add null check like the pattern used elsewhere.