diff --git a/packages/editor/src/components/inner-blocks/index.js b/packages/editor/src/components/inner-blocks/index.js index c4cb0ac5d4099..c6e16eef6d98b 100644 --- a/packages/editor/src/components/inner-blocks/index.js +++ b/packages/editor/src/components/inner-blocks/index.js @@ -154,9 +154,9 @@ InnerBlocks = compose( [ replaceInnerBlocks( blocks ) { const clientIds = map( block.innerBlocks, 'clientId' ); if ( clientIds.length ) { - replaceBlocks( clientIds, blocks ); + replaceBlocks( clientIds, blocks, true ); } else { - insertBlocks( blocks, undefined, clientId, templateInsertUpdatesSelection ); + insertBlocks( blocks, undefined, clientId, templateInsertUpdatesSelection, true ); } }, updateNestedSettings( settings ) { diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 864fcfdd518e5..af88fe1dd60f0 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -217,17 +217,19 @@ export function toggleSelection( isSelectionEnabled = true ) { * Returns an action object signalling that a blocks should be replaced with * one or more replacement blocks. * - * @param {(string|string[])} clientIds Block client ID(s) to replace. - * @param {(Object|Object[])} blocks Replacement block(s). + * @param {(string|string[])} clientIds Block client ID(s) to replace. + * @param {(Object|Object[])} blocks Replacement block(s). + * @param {?boolean} ignoreAllowedBlocksValidation If true the replacement will occur even if some of the new blocks were not allowed e.g: because of allowed blocks restrictions. * * @return {Object} Action object. */ -export function replaceBlocks( clientIds, blocks ) { +export function replaceBlocks( clientIds, blocks, ignoreAllowedBlocksValidation = false ) { return { type: 'REPLACE_BLOCKS', clientIds: castArray( clientIds ), blocks: castArray( blocks ), time: Date.now(), + ignoreAllowedBlocksValidation, }; } @@ -305,14 +307,21 @@ export function insertBlock( block, index, rootClientId, updateSelection = true * Returns an action object used in signalling that an array of blocks should * be inserted, optionally at a specific index respective a root block list. * - * @param {Object[]} blocks Block objects to insert. - * @param {?number} index Index at which block should be inserted. - * @param {?string} rootClientId Optional root cliente ID of block list on which to insert. - * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. + * @param {Object[]} blocks Block objects to insert. + * @param {?number} index Index at which block should be inserted. + * @param {?string} rootClientId Optional root client ID of block list on which to insert. + * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. + * @param {?boolean} ignoreAllowedBlocksValidation If true the block will be inserted even if the insertion was not allowed e.g: because of allowed blocks restrictions. * * @return {Object} Action object. */ -export function insertBlocks( blocks, index, rootClientId, updateSelection = true ) { +export function insertBlocks( + blocks, + index, + rootClientId, + updateSelection = true, + ignoreAllowedBlocksValidation = false +) { return { type: 'INSERT_BLOCKS', blocks: castArray( blocks ), @@ -320,6 +329,7 @@ export function insertBlocks( blocks, index, rootClientId, updateSelection = tru rootClientId, time: Date.now(), updateSelection, + ignoreAllowedBlocksValidation, }; } diff --git a/packages/editor/src/store/middlewares.js b/packages/editor/src/store/middlewares.js index 6381132bb81e0..fed65d3b97a76 100644 --- a/packages/editor/src/store/middlewares.js +++ b/packages/editor/src/store/middlewares.js @@ -3,12 +3,82 @@ */ import refx from 'refx'; import multi from 'redux-multi'; -import { flowRight } from 'lodash'; +import { every, filter, first, flowRight } from 'lodash'; /** * Internal dependencies */ import effects from './effects'; +import { canInsertBlockType, getBlockName, getBlockRootClientId, getTemplateLock } from './selectors'; + +/** + * The allowedBlocksMiddleware middleware makes sure we never add a block when that addition is not possible. + * In order to accomplish this validation allowedBlocksMiddleware makes use of canInsertBlockType selector + * and custom logic for replace, move and multi-block insertion. + * The primary objective of middleware is to make sure the store never gets in an inconsistent state with a block + * added inside in a forbidden area. So for example, if an external plugin tries to insert blocks when a locking exists + * the action will be discarded. + * + * @param {Object} store Middleware Store Object. + * @return {Function} Redux Middleware. + */ +const allowedBlocksMiddleware = ( store ) => ( next ) => ( action ) => { + if ( action.ignoreAllowedBlocksValidation ) { + next( action ); + return; + } + switch ( action.type ) { + // When inserting we allow the action if at least one of the blocks can be inserted. + // Blocks that can not be inserted are removed from the action. + case 'INSERT_BLOCKS': { + const allowedBlocks = filter( action.blocks, ( block ) => + block && + canInsertBlockType( store.getState(), block.name, action.rootClientId ) + ); + if ( allowedBlocks.length ) { + next( { + ...action, + blocks: allowedBlocks, + } ); + } + return; + } + case 'MOVE_BLOCK_TO_POSITION': { + const { fromRootClientId, toRootClientId, clientId } = action; + const state = store.getState(); + const blockName = getBlockName( state, clientId ); + + // If locking is equal to all on the original clientId (fromRootClientId) it is not possible to move the block to any other position. + // In the other cases (locking !== all ), if moving inside the same block the move is always possible + // if moving to other parent block, the move is possible if we can insert a block of the same type inside the new parent block. + if ( + getTemplateLock( state, fromRootClientId ) !== 'all' && + ( fromRootClientId === toRootClientId || canInsertBlockType( store.getState(), blockName, toRootClientId ) ) + ) { + next( action ); + } + return; + } + case 'REPLACE_BLOCKS': { + const clientId = getBlockRootClientId( store.getState(), first( action.clientIds ) ); + // Replace is valid if the new blocks can be inserted in the root block + // or if we had a block of the same type in the position of the block being replaced. + const isOperationValid = every( action.blocks, ( block, index ) => { + if ( canInsertBlockType( store.getState(), block.name, clientId ) ) { + return true; + } + const clientIdToReplace = action.clientIds[ index ]; + const nameOfBlockToReplace = clientIdToReplace && getBlockName( store.getState(), clientIdToReplace ); + return nameOfBlockToReplace && nameOfBlockToReplace === block.name; + } ); + if ( isOperationValid ) { + next( action ); + } + return; + } + } + next( action ); +}; /** * Applies the custom middlewares used specifically in the editor module. @@ -21,6 +91,7 @@ function applyMiddlewares( store ) { const middlewares = [ refx( effects ), multi, + allowedBlocksMiddleware, ]; let enhancedDispatch = () => {