Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

Copy link
Copy Markdown

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 when showAsQueryListing is true. Add null check like the pattern used elsewhere.

Suggested change
? Math.ceil(querystringResults[subrequestID].total / b_size)
? Math.ceil(querystringResults[subrequestID]?.total / b_size)

: 0;

const prevBatch = showAsFolderListing
? content.batching?.prev
: showAsQueryListing
? querystringResults[subrequestID].batching?.prev

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Same potential undefined access issue here - should check if querystringResults[subrequestID] exists before accessing batching.

Suggested change
? querystringResults[subrequestID].batching?.prev
? 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The isfunc helper function duplicates lodash's isFunction. Consider using only the imported isFunction from lodash for consistency.

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


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Logic in _filtered appears inverted - reject variable name suggests filtering out, but the condition returns the property when reject is true

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)),
);
Loading