From a12fa9064f9bb3744de2c1f3a94fbc2a54b5348c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 29 May 2024 12:05:59 +0100 Subject: [PATCH] Block Editor: Check for multiple block usage in the block-editor package (#62086) Co-authored-by: youknowriad Co-authored-by: ellatrix --- .../src/components/block-edit/index.js | 15 +- .../block-edit/multiple-usage-warning.js | 46 +++++ .../src/components/block-list/block.js | 18 ++ packages/edit-post/src/hooks/index.js | 4 - .../src/hooks/validate-multiple-use/index.js | 163 ------------------ packages/edit-post/src/index.js | 1 - 6 files changed, 77 insertions(+), 170 deletions(-) create mode 100644 packages/block-editor/src/components/block-edit/multiple-usage-warning.js delete mode 100644 packages/edit-post/src/hooks/index.js delete mode 100644 packages/edit-post/src/hooks/validate-multiple-use/index.js diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js index 57df36c7c74a0..0c29c0e98b1bf 100644 --- a/packages/block-editor/src/components/block-edit/index.js +++ b/packages/block-editor/src/components/block-edit/index.js @@ -1,9 +1,9 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; - +import { useMemo, useContext } from '@wordpress/element'; import { hasBlockSupport } from '@wordpress/blocks'; + /** * Internal dependencies */ @@ -17,6 +17,8 @@ import { blockBindingsKey, isPreviewModeKey, } from './context'; +import { MultipleUsageWarning } from './multiple-usage-warning'; +import { PrivateBlockContext } from '../block-list/private-block-context'; /** * The `useBlockEditContext` hook provides information about the block this hook is being used in. @@ -49,6 +51,8 @@ export default function BlockEdit( { const layoutSupport = hasBlockSupport( name, 'layout', false ) || hasBlockSupport( name, '__experimentalLayout', false ); + const { originalBlockClientId } = useContext( PrivateBlockContext ); + return ( + { originalBlockClientId && ( + + ) } ); } diff --git a/packages/block-editor/src/components/block-edit/multiple-usage-warning.js b/packages/block-editor/src/components/block-edit/multiple-usage-warning.js new file mode 100644 index 0000000000000..4acd4d1f349dd --- /dev/null +++ b/packages/block-editor/src/components/block-edit/multiple-usage-warning.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { getBlockType } from '@wordpress/blocks'; +import { Button } from '@wordpress/components'; +import { useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import Warning from '../warning'; + +export function MultipleUsageWarning( { + originalBlockClientId, + name, + onReplace, +} ) { + const { selectBlock } = useDispatch( blockEditorStore ); + const blockType = getBlockType( name ); + + return ( + selectBlock( originalBlockClientId ) } + > + { __( 'Find original' ) } + , + , + ] } + > + { blockType?.title }: + { __( 'This block can only be used once.' ) } + + ); +} diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index cbb1b769b5336..0220a9877eba2 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -23,6 +23,7 @@ import { isUnmodifiedBlock, isReusableBlock, getBlockDefaultClassName, + hasBlockSupport, store as blocksStore, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; @@ -534,6 +535,7 @@ function BlockListBlockProvider( props ) { isFirstMultiSelectedBlock, getMultiSelectedBlockClientIds, hasSelectedInnerBlock, + getBlocksByName, getBlockIndex, isBlockMultiSelected, @@ -607,6 +609,17 @@ function BlockListBlockProvider( props ) { const movingClientId = hasBlockMovingClientId(); const blockEditingMode = getBlockEditingMode( clientId ); + const multiple = hasBlockSupport( blockName, 'multiple', true ); + + // For block types with `multiple` support, there is no "original + // block" to be found in the content, as the block itself is valid. + const blocksWithSameName = multiple + ? [] + : getBlocksByName( blockName ); + const isInvalid = + blocksWithSameName.length && + blocksWithSameName[ 0 ] !== clientId; + return { ...previewContext, mode: getBlockMode( clientId ), @@ -664,6 +677,9 @@ function BlockListBlockProvider( props ) { hasEditableOutline: blockEditingMode !== 'disabled' && getBlockEditingMode( rootClientId ) === 'disabled', + originalBlockClientId: isInvalid + ? blocksWithSameName[ 0 ] + : false, }; }, [ clientId, rootClientId ] @@ -707,6 +723,7 @@ function BlockListBlockProvider( props ) { hasEditableOutline, className, defaultClassName, + originalBlockClientId, } = selectedProps; // Users of the editor.BlockListBlock filter used to be able to @@ -754,6 +771,7 @@ function BlockListBlockProvider( props ) { defaultClassName, mayDisplayControls, mayDisplayParentControls, + originalBlockClientId, themeSupportsLayout, }; diff --git a/packages/edit-post/src/hooks/index.js b/packages/edit-post/src/hooks/index.js deleted file mode 100644 index 0397f1fee936c..0000000000000 --- a/packages/edit-post/src/hooks/index.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Internal dependencies - */ -import './validate-multiple-use'; diff --git a/packages/edit-post/src/hooks/validate-multiple-use/index.js b/packages/edit-post/src/hooks/validate-multiple-use/index.js deleted file mode 100644 index bf771f4522036..0000000000000 --- a/packages/edit-post/src/hooks/validate-multiple-use/index.js +++ /dev/null @@ -1,163 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createBlock, - findTransform, - getBlockTransforms, - getBlockType, - hasBlockSupport, -} from '@wordpress/blocks'; -import { Button } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { Warning, store as blockEditorStore } from '@wordpress/block-editor'; -import { addFilter } from '@wordpress/hooks'; -import { __ } from '@wordpress/i18n'; -import { compose, createHigherOrderComponent } from '@wordpress/compose'; - -/** - * Recursively find very first block of an specific block type. - * - * @param {Object[]} blocks List of blocks. - * @param {string} name Block name to search. - * - * @return {Object|undefined} Return block object or undefined. - */ -function findFirstOfSameType( blocks, name ) { - if ( ! Array.isArray( blocks ) || ! blocks.length ) { - return; - } - - for ( const block of blocks ) { - if ( block.name === name ) { - return block; - } - - // Search inside innerBlocks. - const firstBlock = findFirstOfSameType( block.innerBlocks, name ); - - if ( firstBlock ) { - return firstBlock; - } - } -} - -const enhance = compose( - /** - * For blocks whose block type doesn't support `multiple`, provides the - * wrapped component with `originalBlockClientId` -- a reference to the - * first block of the same type in the content -- if and only if that - * "original" block is not the current one. Thus, an inexisting - * `originalBlockClientId` prop signals that the block is valid. - * - * @param {Component} WrappedBlockEdit A filtered BlockEdit instance. - * - * @return {Component} Enhanced component with merged state data props. - */ - withSelect( ( select, block ) => { - const multiple = hasBlockSupport( block.name, 'multiple', true ); - - // For block types with `multiple` support, there is no "original - // block" to be found in the content, as the block itself is valid. - if ( multiple ) { - return {}; - } - - // Otherwise, only pass `originalBlockClientId` if it refers to a different - // block from the current one. - const blocks = select( blockEditorStore ).getBlocks(); - const firstOfSameType = findFirstOfSameType( blocks, block.name ); - const isInvalid = - firstOfSameType && firstOfSameType.clientId !== block.clientId; - return { - originalBlockClientId: isInvalid && firstOfSameType.clientId, - }; - } ), - withDispatch( ( dispatch, { originalBlockClientId } ) => ( { - selectFirst: () => - dispatch( blockEditorStore ).selectBlock( originalBlockClientId ), - } ) ) -); - -const withMultipleValidation = createHigherOrderComponent( ( BlockEdit ) => { - return enhance( ( { originalBlockClientId, selectFirst, ...props } ) => { - if ( ! originalBlockClientId ) { - return ; - } - - const blockType = getBlockType( props.name ); - const outboundType = getOutboundType( props.name ); - - return [ -
- -
, - - { __( 'Find original' ) } - , - , - outboundType && ( - - ), - ] } - > - { blockType?.title }: - { __( 'This block can only be used once.' ) } - , - ]; - } ); -}, 'withMultipleValidation' ); - -/** - * Given a base block name, returns the default block type to which to offer - * transforms. - * - * @param {string} blockName Base block name. - * - * @return {?Object} The chosen default block type. - */ -function getOutboundType( blockName ) { - // Grab the first outbound transform. - const transform = findTransform( - getBlockTransforms( 'to', blockName ), - ( { type, blocks } ) => type === 'block' && blocks.length === 1 // What about when .length > 1? - ); - - if ( ! transform ) { - return null; - } - - return getBlockType( transform.blocks[ 0 ] ); -} - -addFilter( - 'editor.BlockEdit', - 'core/edit-post/validate-multiple-use/with-multiple-validation', - withMultipleValidation -); diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 1e0b3fe7d4d6f..f19248fa0d51c 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -19,7 +19,6 @@ import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ -import './hooks'; import Editor from './editor'; /**