Skip to content

Commit

Permalink
Template Sidebar: Fix list of current template part areas to show nes…
Browse files Browse the repository at this point in the history
…ted template parts (#47232)

* Try: Update list of current template part areas to recursively check for template parts

* Simplify logic, add memoized call

* Move logic to a single function to reduce multiple loops, move to utils, add a test

* Add test to cover memoization, only export memoized function
  • Loading branch information
andrewserong committed Jan 24, 2023
1 parent c160a7f commit a065bb5
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 26 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/edit-site/package.json
Expand Up @@ -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"
},
Expand Down
32 changes: 6 additions & 26 deletions packages/edit-site/src/store/selectors.js
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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 );
}
);

Expand Down
181 changes: 181 additions & 0 deletions 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 );
} );
} );
} );
69 changes: 69 additions & 0 deletions 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 };

0 comments on commit a065bb5

Please sign in to comment.