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

Template Parts: Add search to replacement modal #42459

Merged
merged 12 commits into from
Jul 20, 2022
104 changes: 65 additions & 39 deletions packages/block-library/src/template-part/edit/selection-modal.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { useCallback, useMemo } from '@wordpress/element';
import { useCallback, useMemo, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { useDispatch } from '@wordpress/data';
Expand All @@ -11,6 +11,10 @@ import {
__experimentalBlockPatternsList as BlockPatternsList,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
SearchControl,
__experimentalHStack as HStack,
} from '@wordpress/components';

/**
* Internal dependencies
Expand All @@ -21,6 +25,7 @@ import {
useCreateTemplatePartFromBlocks,
} from './utils/hooks';
import { createTemplatePartId } from './utils/create-template-part-id';
import { searchItems } from './utils/search';

export default function TemplatePartSelectionModal( {
setAttributes,
Expand All @@ -29,6 +34,8 @@ export default function TemplatePartSelectionModal( {
area,
clientId,
} ) {
const [ searchValue, setSearchValue ] = useState( '' );

// When the templatePartId is undefined,
// it means the user is creating a new one from the placeholder.
const isReplacingTemplatePartContent = !! templatePartId;
Expand All @@ -37,18 +44,24 @@ export default function TemplatePartSelectionModal( {
templatePartId
);
// We can map template parts to block patters to reuse the BlockPatternsList UI
const templartPartsAsBlockPatterns = useMemo( () => {
return templateParts.map( ( templatePart ) => ( {
const filteredTemplateParts = useMemo( () => {
const partsAsPatterns = templateParts.map( ( templatePart ) => ( {
name: createTemplatePartId( templatePart.theme, templatePart.slug ),
title: templatePart.title.rendered,
blocks: parse( templatePart.content.raw ),
templatePart,
} ) );
}, [ templateParts ] );
const shownTemplateParts = useAsyncList( templartPartsAsBlockPatterns );
const { createSuccessNotice } = useDispatch( noticesStore );

return searchItems( partsAsPatterns, searchValue );
}, [ templateParts, searchValue ] );
const shownTemplateParts = useAsyncList( filteredTemplateParts );
const blockPatterns = useAlternativeBlockPatterns( area, clientId );
const shownBlockPatterns = useAsyncList( blockPatterns );
const filteredBlockPatterns = useMemo( () => {
return searchItems( blockPatterns, searchValue );
}, [ blockPatterns, searchValue ] );
const shownBlockPatterns = useAsyncList( filteredBlockPatterns );

const { createSuccessNotice } = useDispatch( noticesStore );
const { replaceInnerBlocks } = useDispatch( blockEditorStore );

const onTemplatePartSelect = useCallback( ( templatePart ) => {
Expand Down Expand Up @@ -76,40 +89,53 @@ export default function TemplatePartSelectionModal( {
);

return (
<>
<div className="block-library-template-part__selection-content">
{ !! templartPartsAsBlockPatterns.length && (
<div>
<h2>{ __( 'Existing template parts' ) }</h2>
<BlockPatternsList
blockPatterns={ templartPartsAsBlockPatterns }
shownPatterns={ shownTemplateParts }
onClickPattern={ ( pattern ) => {
onTemplatePartSelect( pattern.templatePart );
} }
/>
</div>
) }
<div className="block-library-template-part__selection-content">
<div className="block-library-template-part__selection-search">
<SearchControl
onChange={ setSearchValue }
value={ searchValue }
label={ __( 'Search for replacements' ) }
placeholder={ __( 'Search' ) }
/>
</div>
{ !! filteredTemplateParts.length && (
<div>
<h2>{ __( 'Existing template parts' ) }</h2>
<BlockPatternsList
blockPatterns={ filteredTemplateParts }
shownPatterns={ shownTemplateParts }
onClickPattern={ ( pattern ) => {
onTemplatePartSelect( pattern.templatePart );
} }
/>
</div>
) }

{ !! blockPatterns.length && (
<div>
<h2>{ __( 'Patterns' ) }</h2>
<BlockPatternsList
blockPatterns={ blockPatterns }
shownPatterns={ shownBlockPatterns }
onClickPattern={ ( pattern, blocks ) => {
if ( isReplacingTemplatePartContent ) {
replaceInnerBlocks( clientId, blocks );
} else {
createFromBlocks( blocks, pattern.title );
}
{ !! filteredBlockPatterns.length && (
<div>
<h2>{ __( 'Patterns' ) }</h2>
<BlockPatternsList
blockPatterns={ filteredBlockPatterns }
shownPatterns={ shownBlockPatterns }
onClickPattern={ ( pattern, blocks ) => {
if ( isReplacingTemplatePartContent ) {
replaceInnerBlocks( clientId, blocks );
} else {
createFromBlocks( blocks, pattern.title );
}

onClose();
} }
/>
</div>
onClose();
} }
/>
</div>
) }

{ ! filteredTemplateParts.length &&
! filteredBlockPatterns.length && (
<HStack alignment="center">
Mamaduka marked this conversation as resolved.
Show resolved Hide resolved
<p>{ __( 'No results found.' ) }</p>
</HStack>
) }
</div>
</>
</div>
);
}
20 changes: 20 additions & 0 deletions packages/block-library/src/template-part/edit/utils/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Filters an item list given a search term.
*
* @param {Array} items Item list
* @param {string} searchValue Search input.
*
* @return {Array} Filtered item list.
*/
export function searchItems( items, searchValue ) {
if ( ! searchValue ) {
return items;
}

const normalizedSearchValue = searchValue.toLowerCase();
return items.filter( ( item ) => {
const normalizedTitle = item.title.toLowerCase();

return normalizedTitle.includes( normalizedSearchValue );
} );
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a very basic search. I'm not sure if we need a ranked search like inserter here.

Copy link
Contributor

@talldan talldan Jul 18, 2022

Choose a reason for hiding this comment

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

I think it's ok to start with something simple, but also looks like it probably wouldn't be too difficult to improve it. I had a look at the code for the inserter to see how it works: https://github.com/WordPress/gutenberg/blob/trunk/packages/block-editor/src/components/inserter/search-items.js#L145-L161

The biggest win would probably be to still consider it a match if all of the search term words are in the title but order doesn't matter (e.g. 'large dark' and 'dark large' produce the same search results), which seems to be what this bit of code does using the words function:

} else {
const terms = [
name,
title,
description,
...keywords,
category,
collection,
].join( ' ' );
const normalizedSearchTerms = words( normalizedSearchInput );
const unmatchedTerms = removeMatchingTerms(
normalizedSearchTerms,
terms
);
if ( unmatchedTerms.length === 0 ) {
rank += 10;
}
}

It'll be a bit easier for patterns since there's only one title field to check against.

edit: Also just saw that lodash words is about to be replaced in #42467.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very basic search. I'm not sure if we need a ranked search like inserter here.

We don't actually have a ranking system right now and the one we need would be different than the inserter, that uses frecency.

Having said that, I think we can start small with a couple of tweaks maybe:

  1. Rename the function to something more specific(ex. searchPatterns) as is not generic as some other similar functions
  2. Probably add simple accents, trim handling to normalizedTitle.

In general I think integrating pattern explorer here would be fitting, but until we have something like that, it's okay to have at least a basic search by title. Also when we'll work on a patterns ranking system we might convert similar functions to a selector..

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't actually have a ranking system right now

There is a fairly basic ranking system when searching in the inserter. Exact matches have the highest ranking, partial matches where the title starts with the search term are next, and then individual word matches are ranked lowest. Finally, core blocks are given a small boost in the results.

see: https://github.com/WordPress/gutenberg/blob/dfc4b8b8b831c6d5e2c280b06c5871d91e66f337/packages/block-editor/src/components/inserter/search-items.js

Copy link
Contributor

Choose a reason for hiding this comment

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

and the one we need would be different than the inserter, that uses frecency.

Sorry for not being clear here. I didn't expand on all the details about the other ranking system and only mentioned frecency. What I really meant to say is that we would need to also include a sorting system for patterns in every list/search results, which is the missing piece. For example in patterns we should have a way to rank better patterns from current theme, or external patterns loaded from theme.json etc..

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in d16c616.

}