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

TemplateContentPanel: Don't show content blocks that are in a Query Loop #63732

Merged
merged 6 commits into from
Jul 25, 2024
Merged
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
Expand Up @@ -3,43 +3,46 @@
*/
import { useSelect, useRegistry } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { useEffect } from '@wordpress/element';
import { useEffect, useMemo } from '@wordpress/element';
import { applyFilters } from '@wordpress/hooks';

const DEFAULT_CONTENT_ONLY_BLOCKS = [
/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';

const POST_CONTENT_BLOCK_TYPES = [
'core/post-title',
'core/post-featured-image',
'core/post-content',
'core/template-part',
];

/**
* Component that when rendered, makes it so that the site editor allows only
* page content to be edited.
*/
export default function DisableNonPageContentBlocks() {
const contentOnlyBlocks = applyFilters(
'editor.postContentBlockTypes',
DEFAULT_CONTENT_ONLY_BLOCKS
const contentOnlyBlockTypes = useMemo(
() => [
...applyFilters(
'editor.postContentBlockTypes',
POST_CONTENT_BLOCK_TYPES
),
'core/template-part',
Copy link
Member

@Mamaduka Mamaduka Jul 24, 2024

Choose a reason for hiding this comment

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

The core/template-part was removed from the const because we don't want extenders to remove it, correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking along the lines of:

  • editor.postContentBlockType allows extenders to control which block types we consider to be "post content".
  • We're enabling selecting template parts via the same mechanism but it's not "post content".

],
[]
);

// Note that there are two separate subscription because the result for each
// Note that there are two separate subscriptions because the result for each
// returns a new array.
const contentOnlyIds = useSelect( ( select ) => {
const { getBlocksByName, getBlockParents, getBlockName } =
select( blockEditorStore );
return getBlocksByName( contentOnlyBlocks ).filter( ( clientId ) =>
getBlockParents( clientId ).every( ( parentClientId ) => {
const parentBlockName = getBlockName( parentClientId );
return (
// Ignore descendents of the query block.
parentBlockName !== 'core/query' &&
// Enable only the top-most block.
! contentOnlyBlocks.includes( parentBlockName )
);
} )
);
}, [] );
const contentOnlyIds = useSelect(
( select ) => {
const { getPostBlocksByName } = unlock( select( editorStore ) );
return getPostBlocksByName( contentOnlyBlockTypes );
},
[ contentOnlyBlockTypes ]
);
const disabledIds = useSelect( ( select ) => {
const { getBlocksByName, getBlockOrder } = select( blockEditorStore );
return getBlocksByName( [ 'core/template-part' ] ).flatMap(
Expand Down
57 changes: 36 additions & 21 deletions packages/editor/src/components/template-content-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import {
store as blockEditorStore,
privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { PanelBody } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { store as interfaceStore } from '@wordpress/interface';
import { applyFilters } from '@wordpress/hooks';
import { useMemo } from '@wordpress/element';

/**
* Internal dependencies
Expand All @@ -19,30 +18,46 @@ import { store as editorStore } from '../../store';

const { BlockQuickNavigation } = unlock( blockEditorPrivateApis );

const PAGE_CONTENT_BLOCKS = [
'core/post-content',
'core/post-featured-image',
const POST_CONTENT_BLOCK_TYPES = [
'core/post-title',
'core/post-featured-image',
'core/post-content',
];

const TEMPLATE_PART_BLOCK = 'core/template-part';

export default function TemplateContentPanel() {
const { enableComplementaryArea } = useDispatch( interfaceStore );
const { clientIds, postType, renderingMode } = useSelect( ( select ) => {
const { getBlocksByName } = select( blockEditorStore );
const { getCurrentPostType } = select( editorStore );
const _postType = getCurrentPostType();
return {
postType: _postType,
clientIds: getBlocksByName(
TEMPLATE_POST_TYPE === _postType
? TEMPLATE_PART_BLOCK
: PAGE_CONTENT_BLOCKS
const postContentBlockTypes = useMemo(
() =>
applyFilters(
'editor.postContentBlockTypes',
POST_CONTENT_BLOCK_TYPES
),
renderingMode: select( editorStore ).getRenderingMode(),
};
}, [] );
[]
);

const { clientIds, postType, renderingMode } = useSelect(
( select ) => {
const {
getCurrentPostType,
getPostBlocksByName,
getRenderingMode,
} = unlock( select( editorStore ) );
const _postType = getCurrentPostType();
return {
postType: _postType,
clientIds: getPostBlocksByName(
TEMPLATE_POST_TYPE === _postType
? TEMPLATE_PART_BLOCK
: postContentBlockTypes
),
renderingMode: getRenderingMode(),
};
},
[ postContentBlockTypes ]
);

const { enableComplementaryArea } = useDispatch( interfaceStore );

if ( renderingMode === 'post-only' && postType !== TEMPLATE_POST_TYPE ) {
return null;
Expand Down
33 changes: 33 additions & 0 deletions packages/editor/src/store/private-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,36 @@ export const hasPostMetaChanges = createRegistrySelector(
export function getEntityActions( state, ...args ) {
return _getEntityActions( state.dataviews, ...args );
}

/**
* Similar to getBlocksByName in @wordpress/block-editor, but only returns the top-most
* blocks that aren't descendants of the query block.
*
* @param {Object} state Global application state.
* @param {Array|string} blockNames Block names of the blocks to retrieve.
*
* @return {Array} Block client IDs.
*/
export const getPostBlocksByName = createRegistrySelector( ( select ) =>
createSelector(
( state, blockNames ) => {
blockNames = Array.isArray( blockNames )
? blockNames
: [ blockNames ];
const { getBlocksByName, getBlockParents, getBlockName } =
select( blockEditorStore );
return getBlocksByName( blockNames ).filter( ( clientId ) =>
getBlockParents( clientId ).every( ( parentClientId ) => {
const parentBlockName = getBlockName( parentClientId );
return (
// Ignore descendents of the query block.
parentBlockName !== 'core/query' &&
// Enable only the top-most block.
! blockNames.includes( parentBlockName )
);
} )
);
},
() => [ select( blockEditorStore ).getBlocks() ]
)
);
78 changes: 78 additions & 0 deletions packages/editor/src/store/test/private-selectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Internal dependencies
*/
import { getPostBlocksByName } from '../private-selectors';

describe( 'getPostBlocksByName', () => {
const state = {
blocks: {
byClientId: new Map( [
[ 'block1', { name: 'core/paragraph' } ],
[ 'block2', { name: 'core/heading' } ],
[ 'block3', { name: 'core/paragraph' } ],
[ 'block4', { name: 'core/query' } ],
[ 'block5', { name: 'core/paragraph' } ],
[ 'block6', { name: 'core/heading' } ],
] ),
order: new Map( [
[ '', [ 'block1', 'block2', 'block3', 'block4' ] ],
[ 'block4', [ 'block5', 'block6' ] ],
] ),
parents: new Map( [
[ 'block1', '' ],
[ 'block2', '' ],
[ 'block3', '' ],
[ 'block4', '' ],
[ 'block5', 'block4' ],
[ 'block6', 'block4' ],
] ),
},
};

getPostBlocksByName.registry = {
select: () => ( {
getBlocksByName: ( blockNames ) =>
Array.from( state.blocks.byClientId.keys() ).filter(
( clientId ) =>
blockNames.includes(
state.blocks.byClientId.get( clientId ).name
)
),
getBlockParents: ( clientId ) => {
const parents = [];
let parent = state.blocks.parents.get( clientId );
while ( parent ) {
parents.push( parent );
parent = state.blocks.parents.get( parent );
}
return parents;
},
getBlockName: ( clientId ) =>
state.blocks.byClientId.get( clientId ).name,
getBlocks: () => [],
} ),
};

it( 'should return top-level blocks of the specified name', () => {
const result = getPostBlocksByName( state, 'core/paragraph' );
expect( result ).toEqual( [ 'block1', 'block3' ] );
} );

it( 'should return an empty array if no blocks match', () => {
const result = getPostBlocksByName( state, 'core/non-existent' );
expect( result ).toEqual( [] );
} );

it( 'should ignore blocks inside a query block', () => {
const result = getPostBlocksByName( state, 'core/paragraph' );
expect( result ).toEqual( [ 'block1', 'block3' ] );
} );

it( 'should handle multiple block names', () => {
const result = getPostBlocksByName( state, [
'core/paragraph',
'core/heading',
] );
expect( result ).toEqual( [ 'block1', 'block2', 'block3' ] );
} );
} );
Loading