diff --git a/package-lock.json b/package-lock.json index f663b519ab567..75f24270366df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17809,6 +17809,7 @@ "fast-deep-equal": "^3.1.3", "history": "^5.1.0", "lodash": "^4.17.21", + "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", "rememo": "^4.0.0" }, diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index dc1ba91a20f14..70403fdbc5e20 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -62,6 +62,7 @@ "fast-deep-equal": "^3.1.3", "history": "^5.1.0", "lodash": "^4.17.21", + "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", "rememo": "^4.0.0" }, diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index 2137525b07b63..97f9184cbf60d 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -10,10 +10,14 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { createRegistrySelector } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { uploadMedia } from '@wordpress/media-utils'; -import { isTemplatePart } from '@wordpress/blocks'; import { Platform } from '@wordpress/element'; import { store as preferencesStore } from '@wordpress/preferences'; +/** + * Internal dependencies + */ +import { getFilteredTemplatePartBlocks } from './utils'; + /** * @typedef {'template'|'template_type'} TemplateType Template type. */ @@ -268,32 +272,8 @@ export const getCurrentTemplateTemplateParts = createRegistrySelector( 'wp_template_part', { per_page: -1 } ); - const templatePartsById = templateParts - ? // Key template parts by their ID. - templateParts.reduce( - ( newTemplateParts, part ) => ( { - ...newTemplateParts, - [ part.id ]: part, - } ), - {} - ) - : {}; - - return ( template.blocks ?? [] ) - .filter( ( block ) => isTemplatePart( block ) ) - .map( ( block ) => { - const { - attributes: { theme, slug }, - } = block; - const templatePartId = `${ theme }//${ slug }`; - const templatePart = templatePartsById[ templatePartId ]; - return { - templatePart, - block, - }; - } ) - .filter( ( { templatePart } ) => !! templatePart ); + return getFilteredTemplatePartBlocks( template.blocks, templateParts ); } ); diff --git a/packages/edit-site/src/store/test/utils.js b/packages/edit-site/src/store/test/utils.js new file mode 100644 index 0000000000000..fd10317be5b3b --- /dev/null +++ b/packages/edit-site/src/store/test/utils.js @@ -0,0 +1,181 @@ +/** + * Internal dependencies + */ +import { getFilteredTemplatePartBlocks } from '../utils'; + +const NESTED_BLOCKS = [ + { + clientId: '1', + name: 'core/group', + innerBlocks: [ + { + clientId: '2', + name: 'core/template-part', + attributes: { + slug: 'header', + theme: 'my-theme', + }, + innerBlocks: [ + { + clientId: '3', + name: 'core/group', + innerBlocks: [], + }, + ], + }, + { + clientId: '4', + name: 'core/template-part', + attributes: { + slug: 'aside', + theme: 'my-theme', + }, + innerBlocks: [], + }, + ], + }, + { + clientId: '5', + name: 'core/paragraph', + innerBlocks: [], + }, + { + clientId: '6', + name: 'core/template-part', + attributes: { + slug: 'footer', + theme: 'my-theme', + }, + innerBlocks: [], + }, +]; + +const FLATTENED_BLOCKS = [ + { + block: { + clientId: '2', + name: 'core/template-part', + attributes: { + slug: 'header', + theme: 'my-theme', + }, + }, + templatePart: { + id: 'my-theme//header', + slug: 'header', + theme: 'my-theme', + }, + }, + { + block: { + clientId: '4', + name: 'core/template-part', + attributes: { + slug: 'aside', + theme: 'my-theme', + }, + }, + templatePart: { + id: 'my-theme//aside', + slug: 'aside', + theme: 'my-theme', + }, + }, + { + block: { + clientId: '6', + name: 'core/template-part', + attributes: { + slug: 'footer', + theme: 'my-theme', + }, + }, + templatePart: { + id: 'my-theme//footer', + slug: 'footer', + theme: 'my-theme', + }, + }, +]; + +const SINGLE_TEMPLATE_PART_BLOCK = { + clientId: '1', + name: 'core/template-part', + innerBlocks: [], + attributes: { + slug: 'aside', + theme: 'my-theme', + }, +}; + +const TEMPLATE_PARTS = [ + { + id: 'my-theme//header', + slug: 'header', + theme: 'my-theme', + }, + { + id: 'my-theme//aside', + slug: 'aside', + theme: 'my-theme', + }, + { + id: 'my-theme//footer', + slug: 'footer', + theme: 'my-theme', + }, +]; + +describe( 'utils', () => { + describe( 'getFilteredTemplatePartBlocks', () => { + it( 'returns a flattened list of filtered template parts preserving a depth-first order', () => { + const flattenedFilteredTemplateParts = + getFilteredTemplatePartBlocks( NESTED_BLOCKS, TEMPLATE_PARTS ); + expect( flattenedFilteredTemplateParts ).toEqual( + FLATTENED_BLOCKS + ); + } ); + + it( 'returns a cached result when passed the same params', () => { + // Clear the cache and call the function twice. + getFilteredTemplatePartBlocks.clear(); + getFilteredTemplatePartBlocks( NESTED_BLOCKS, TEMPLATE_PARTS ); + expect( + getFilteredTemplatePartBlocks( NESTED_BLOCKS, TEMPLATE_PARTS ) + ).toEqual( FLATTENED_BLOCKS ); + + // The function has been called twice with the same params, so the cache size should be 1. + const [ , , originalSize ] = + getFilteredTemplatePartBlocks.getCache(); + expect( originalSize ).toBe( 1 ); + + // Call the function again, with different params. + expect( + getFilteredTemplatePartBlocks( + [ SINGLE_TEMPLATE_PART_BLOCK ], + TEMPLATE_PARTS + ) + ).toEqual( [ + { + block: { + clientId: '1', + name: 'core/template-part', + attributes: { + slug: 'aside', + theme: 'my-theme', + }, + }, + templatePart: { + id: 'my-theme//aside', + slug: 'aside', + theme: 'my-theme', + }, + }, + ] ); + + // The function has been called with different params, so the cache size should now be 2. + const [ , , finalSize ] = getFilteredTemplatePartBlocks.getCache(); + expect( finalSize ).toBe( 2 ); + } ); + } ); +} ); diff --git a/packages/edit-site/src/store/utils.js b/packages/edit-site/src/store/utils.js new file mode 100644 index 0000000000000..af24684ace615 --- /dev/null +++ b/packages/edit-site/src/store/utils.js @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import memoize from 'memize'; + +/** + * WordPress dependencies + */ +import { isTemplatePart } from '@wordpress/blocks'; + +const EMPTY_ARRAY = []; + +/** + * Get a flattened and filtered list of template parts and the matching block for that template part. + * + * Takes a list of blocks defined within a template, and a list of template parts, and returns a + * flattened list of template parts and the matching block for that template part. + * + * @param {Array} blocks Blocks to flatten. + * @param {?Array} templateParts Available template parts. + * @return {Array} An array of template parts and their blocks. + */ +function getFilteredTemplatePartBlocks( blocks = EMPTY_ARRAY, templateParts ) { + const templatePartsById = templateParts + ? // Key template parts by their ID. + templateParts.reduce( + ( newTemplateParts, part ) => ( { + ...newTemplateParts, + [ part.id ]: part, + } ), + {} + ) + : {}; + + const result = []; + + // Iterate over all blocks, recursing into inner blocks. + // Output will be based on a depth-first traversal. + const stack = [ ...blocks ]; + while ( stack.length ) { + const { innerBlocks, ...block } = stack.shift(); + // Place inner blocks at the beginning of the stack to preserve order. + stack.unshift( ...innerBlocks ); + + if ( isTemplatePart( block ) ) { + const { + attributes: { theme, slug }, + } = block; + const templatePartId = `${ theme }//${ slug }`; + const templatePart = templatePartsById[ templatePartId ]; + + // Only add to output if the found template part block is in the list of available template parts. + if ( templatePart ) { + result.push( { + templatePart, + block, + } ); + } + } + } + + return result; +} + +const memoizedGetFilteredTemplatePartBlocks = memoize( + getFilteredTemplatePartBlocks +); + +export { memoizedGetFilteredTemplatePartBlocks as getFilteredTemplatePartBlocks };