diff --git a/blocks/inner-blocks/index.js b/blocks/inner-blocks/index.js index 37ae97e13c71a7..6b1d56c44932db 100644 --- a/blocks/inner-blocks/index.js +++ b/blocks/inner-blocks/index.js @@ -3,8 +3,8 @@ */ import { withContext } from '@wordpress/components'; -function InnerBlocks( { BlockList, layouts } ) { - return ; +function InnerBlocks( { BlockList, layouts, allowedBlocks, template } ) { + return ; } InnerBlocks = withContext( 'BlockList' )()( InnerBlocks ); diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 43d145c4948ad6..39fdd044139063 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -591,7 +591,7 @@ export class BlockListBlock extends Component { { showSideInserter && (
- +
-
@@ -60,7 +60,7 @@ exports[`DefaultBlockAppender should match snapshot 1`] = ` value="Write your story" /> - @@ -84,7 +84,7 @@ exports[`DefaultBlockAppender should optionally show without prompt 1`] = ` value="" /> - diff --git a/editor/components/inserter-with-shortcuts/index.js b/editor/components/inserter-with-shortcuts/index.js index 009c50056759be..2bbea70352c907 100644 --- a/editor/components/inserter-with-shortcuts/index.js +++ b/editor/components/inserter-with-shortcuts/index.js @@ -52,9 +52,13 @@ export default compose( allowedBlockTypes, }; } ), - withSelect( ( select, { allowedBlockTypes } ) => ( { - items: select( 'core/editor' ).getFrecentInserterItems( allowedBlockTypes, 4 ), - } ) ), + withSelect( ( select, { allowedBlockTypes, rootUID } ) => { + const { getFrecentInserterItems, getSupportedBlocks } = select( 'core/editor' ); + const supportedBlocks = getSupportedBlocks( rootUID, allowedBlockTypes ); + return { + items: getFrecentInserterItems( supportedBlocks, 4 ), + }; + } ), withDispatch( ( dispatch, ownProps ) => { const { uid, rootUID, layout } = ownProps; diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js index 6814c79aa598bd..b1ddc21ce0025c 100644 --- a/editor/components/inserter/index.js +++ b/editor/components/inserter/index.js @@ -87,11 +87,32 @@ class Inserter extends Component { } export default compose( [ - withSelect( ( select ) => ( { - title: select( 'core/editor' ).getEditedPostAttribute( 'title' ), - insertionPoint: select( 'core/editor' ).getBlockInsertionPoint(), - selectedBlock: select( 'core/editor' ).getSelectedBlock(), - } ) ), + withEditorSettings( ( settings ) => { + const { allowedBlockTypes, templateLock } = settings; + + return { + allowedBlockTypes, + isLocked: !! templateLock, + }; + } ), + withSelect( ( select, { allowedBlockTypes } ) => { + const { + getEditedPostAttribute, + getBlockInsertionPoint, + getSelectedBlock, + getSupportedBlocks, + } = select( 'core/editor' ); + + const insertionPoint = getBlockInsertionPoint(); + const { rootUID } = insertionPoint; + const supportedBlocks = getSupportedBlocks( rootUID, allowedBlockTypes ); + return { + title: getEditedPostAttribute( 'title' ), + insertionPoint, + selectedBlock: getSelectedBlock(), + hasSupportedBlocks: true === supportedBlocks || ! isEmpty( supportedBlocks ), + }; + } ), withDispatch( ( dispatch, ownProps ) => ( { showInsertionPoint: dispatch( 'core/editor' ).showInsertionPoint, hideInsertionPoint: dispatch( 'core/editor' ).hideInsertionPoint, @@ -106,12 +127,4 @@ export default compose( [ return dispatch( 'core/editor' ).insertBlock( insertedBlock, index, rootUID ); }, } ) ), - withEditorSettings( ( settings ) => { - const { allowedBlockTypes, templateLock } = settings; - - return { - hasSupportedBlocks: true === allowedBlockTypes || ! isEmpty( allowedBlockTypes ), - isLocked: !! templateLock, - }; - } ), ] )( Inserter ); diff --git a/editor/components/inserter/menu.js b/editor/components/inserter/menu.js index f863ad697f3376..1d7c058a1fae28 100644 --- a/editor/components/inserter/menu.js +++ b/editor/components/inserter/menu.js @@ -344,10 +344,17 @@ export default compose( }; } ), withSelect( ( select, { allowedBlockTypes } ) => { - const { getInserterItems, getFrecentInserterItems } = select( 'core/editor' ); + const { + getBlockInsertionPoint, + getInserterItems, + getFrecentInserterItems, + getSupportedBlocks, + } = select( 'core/editor' ); + const { rootUID } = getBlockInsertionPoint(); + const supportedBlocks = getSupportedBlocks( rootUID, allowedBlockTypes ); return { - items: getInserterItems( allowedBlockTypes ), - frecentItems: getFrecentInserterItems( allowedBlockTypes ), + items: getInserterItems( supportedBlocks ), + frecentItems: getFrecentInserterItems( supportedBlocks ), }; } ), withDispatch( ( dispatch ) => ( { diff --git a/editor/store/actions.js b/editor/store/actions.js index 07a22bc3ec3a5f..c78cdc25ec9acc 100644 --- a/editor/store/actions.js +++ b/editor/store/actions.js @@ -666,3 +666,19 @@ export function insertDefaultBlock( attributes, rootUID, index ) { isProvisional: true, }; } + +/** + * Returns an action object that changes the nested settings of a given block. + * + * @param {string} id UID of the block whose nested setting. + * @param {Object} settings Object with the new settings for the nested block. + * + * @return {Object} Action object + */ +export function updateBlockListSettings( id, settings ) { + return { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + id, + settings, + }; +} diff --git a/editor/store/reducer.js b/editor/store/reducer.js index ca558159fa15e6..ee6dfab934abec 100644 --- a/editor/store/reducer.js +++ b/editor/store/reducer.js @@ -1006,6 +1006,42 @@ export const sharedBlocks = combineReducers( { }, } ); +/** + * Reducer that for each block uid stores an object that represents its nested settings. + * E.g: what blocks can be nested inside a block. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export const blockListSettings = ( state = {}, action ) => { + switch ( action.type ) { + // even if the replaced blocks have the same uid our logic should correct the state. + case 'REPLACE_BLOCKS' : + case 'REMOVE_BLOCKS': { + return omit( state, action.uids ); + } + case 'UPDATE_BLOCK_LIST_SETTINGS': { + const { id, settings } = action; + if ( id && ! settings ) { + return omit( state, id ); + } + const blockSettings = state[ id ]; + const updateIsRequired = ! isEqual( blockSettings, settings ); + if ( updateIsRequired ) { + return { + ...state, + [ id ]: { + ...settings, + }, + }; + } + } + } + return state; +}; + export default optimist( combineReducers( { editor, currentPost, @@ -1013,6 +1049,7 @@ export default optimist( combineReducers( { blockSelection, provisionalBlockUID, blocksMode, + blockListSettings, isInsertionPointVisible, preferences, saving, diff --git a/editor/store/selectors.js b/editor/store/selectors.js index 8d58898ce37597..5c7978d7459972 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -6,6 +6,7 @@ import { first, get, has, + intersection, last, reduce, size, @@ -1590,3 +1591,44 @@ export function inSomeHistory( state, predicate ) { beforeState && predicate( beforeState ) ) ); } + +/** + * Returns the Block List settings of a block if any. + * + * @param {Object} state Editor state. + * @param {?string} uid Block UID. + * + * @return {?Object} Block settings of the block if set. + */ +export function getBlockListSettings( state, uid ) { + return state.blockListSettings[ uid ]; +} + +/** + * Determines the blocks that can be nested inside a given block. Or globally if a block is not specified. + * + * @param {Object} state Global application state. + * @param {?string} uid Block UID. + * @param {string[]|boolean} globallyEnabledBlockTypes Globally enabled block types, or true/false to enable/disable all types. + * + * @return {string[]|boolean} Blocks that can be nested inside the block with the specified uid, or true/false to enable/disable all types. + */ +export function getSupportedBlocks( state, uid, globallyEnabledBlockTypes ) { + if ( ! globallyEnabledBlockTypes ) { + return false; + } + + const supportedNestedBlocks = get( getBlockListSettings( state, uid ), [ 'supportedBlocks' ] ); + if ( supportedNestedBlocks === true || supportedNestedBlocks === undefined ) { + return globallyEnabledBlockTypes; + } + + if ( ! supportedNestedBlocks ) { + return false; + } + + if ( globallyEnabledBlockTypes === true ) { + return supportedNestedBlocks; + } + return intersection( globallyEnabledBlockTypes, supportedNestedBlocks ); +} diff --git a/editor/store/test/actions.js b/editor/store/test/actions.js index 97a0291b9f23db..12462f8d91f7e5 100644 --- a/editor/store/test/actions.js +++ b/editor/store/test/actions.js @@ -42,6 +42,7 @@ import { createErrorNotice, createWarningNotice, removeNotice, + updateBlockListSettings, } from '../actions'; describe( 'actions', () => { @@ -530,4 +531,22 @@ describe( 'actions', () => { } ); } ); } ); + + describe( 'updateBlockListSettings', () => { + it( 'should return the UPDATE_BLOCK_LIST_SETTINGS with undefined settings', () => { + expect( updateBlockListSettings( 'chicken' ) ).toEqual( { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + id: 'chicken', + settings: undefined, + } ); + } ); + + it( 'should return the UPDATE_BLOCK_LIST_SETTINGS action with the passed settings', () => { + expect( updateBlockListSettings( 'chicken', { chicken: 'ribs' } ) ).toEqual( { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + id: 'chicken', + settings: { chicken: 'ribs' }, + } ); + } ); + } ); } ); diff --git a/editor/store/test/reducer.js b/editor/store/test/reducer.js index 005173b127b791..26b0354cd02034 100644 --- a/editor/store/test/reducer.js +++ b/editor/store/test/reducer.js @@ -35,6 +35,7 @@ import { isInsertionPointVisible, sharedBlocks, template, + blockListSettings, } from '../reducer'; describe( 'state', () => { @@ -2189,4 +2190,81 @@ describe( 'state', () => { expect( state ).toEqual( { isValid: true, template: [] } ); } ); } ); + + describe( 'blockListSettings', () => { + it( 'should add new settings', () => { + const original = deepFreeze( {} ); + const state = blockListSettings( original, { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + id: 'chicken', + settings: { + chicken: 'ribs', + }, + } ); + expect( state ).toEqual( { + chicken: { + chicken: 'ribs', + }, + } ); + } ); + + it( 'should update the settings of a block', () => { + const original = deepFreeze( { + chicken: { + chicken: 'ribs', + }, + otherBlock: { + setting1: true, + }, + } ); + const state = blockListSettings( original, { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + id: 'chicken', + settings: { + ribs: 'not-chicken', + }, + } ); + expect( state ).toEqual( { + chicken: { + ribs: 'not-chicken', + }, + otherBlock: { + setting1: true, + }, + } ); + } ); + + it( 'should remove the settings of a block when it is replaced', () => { + const original = deepFreeze( { + chicken: { + chicken: 'ribs', + }, + otherBlock: { + setting1: true, + }, + } ); + const state = blockListSettings( original, { + type: 'REPLACE_BLOCKS', + uids: [ 'otherBlock' ], + } ); + expect( state ).toEqual( { + chicken: { + chicken: 'ribs', + }, + } ); + } ); + + it( 'should remove the settings of a block when it is removed', () => { + const original = deepFreeze( { + otherBlock: { + setting1: true, + }, + } ); + const state = blockListSettings( original, { + type: 'REPLACE_BLOCKS', + uids: [ 'otherBlock' ], + } ); + expect( state ).toEqual( {} ); + } ); + } ); } ); diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index df5d72efadb783..5060f28a8111a9 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -84,6 +84,8 @@ const { isValidTemplate, getTemplate, getTemplateLock, + getBlockListSettings, + getSupportedBlocks, POST_UPDATE_TRANSACTION_ID, isPermalinkEditable, getPermalink, @@ -3253,4 +3255,119 @@ describe( 'selectors', () => { expect( getPermalinkParts( state ) ).toEqual( parts ); } ); } ); + + describe( 'getBlockListSettings', () => { + it( 'should return the settings of a block', () => { + const state = { + blockListSettings: { + chicken: { + setting1: false, + }, + ribs: { + setting2: true, + }, + }, + }; + + expect( getBlockListSettings( state, 'chicken' ) ).toEqual( { + setting1: false, + } ); + } ); + + it( 'should return undefined if settings for the block don\'t exist', () => { + const state = { + blockListSettings: {}, + }; + + expect( getBlockListSettings( state, 'chicken' ) ).toBe( undefined ); + } ); + } ); + + describe( 'getSupportedBlocks', () => { + it( 'should return false if all blocks are disabled globally', () => { + const state = { + blockListSettings: { + block1: { + supportedBlocks: [ 'core/block1' ], + }, + }, + }; + + expect( getSupportedBlocks( state, 'block1', false ) ).toBe( false ); + } ); + + it( 'should return the supportedBlocks of root block if all blocks are supported globally', () => { + const state = { + blockListSettings: { + block1: { + supportedBlocks: [ 'core/block1' ], + }, + }, + }; + + expect( getSupportedBlocks( state, 'block1', true ) ).toEqual( [ 'core/block1' ] ); + } ); + + it( 'should return the globally supported blocks if all blocks are enable inside the root block', () => { + const state = { + blockListSettings: { + block1: { + supportedBlocks: true, + }, + }, + }; + + expect( getSupportedBlocks( state, 'block1', [ 'core/block1' ] ) ).toEqual( [ 'core/block1' ] ); + } ); + + it( 'should return the globally supported blocks if the root block does not sets the supported blocks', () => { + const state = { + blockListSettings: { + block1: { + chicken: 'ribs', + }, + }, + }; + + expect( getSupportedBlocks( state, 'block1', [ 'core/block1' ] ) ).toEqual( [ 'core/block1' ] ); + } ); + + it( 'should return the globally supported blocks if there are no settings for the root block', () => { + const state = { + blockListSettings: { + block1: { + supportedBlocks: true, + }, + }, + }; + + expect( getSupportedBlocks( state, 'block2', [ 'core/block1' ] ) ).toEqual( [ 'core/block1' ] ); + } ); + + it( 'should return false if all blocks are disabled inside the root block ', () => { + const state = { + blockListSettings: { + block1: { + supportedBlocks: false, + }, + }, + }; + + expect( getSupportedBlocks( state, 'block1', [ 'core/block1' ] ) ).toBe( false ); + } ); + + it( 'should return the intersection of globally supported blocks with the supported blocks of the root block if both sets are defined', () => { + const state = { + blockListSettings: { + block1: { + supportedBlocks: [ 'core/block1', 'core/block2', 'core/block3' ], + }, + }, + }; + + expect( getSupportedBlocks( state, 'block1', [ 'core/block2', 'core/block4', 'core/block5' ] ) ).toEqual( + [ 'core/block2' ] + ); + } ); + } ); } ); diff --git a/editor/utils/block-list.js b/editor/utils/block-list.js index 0f68869c783d8a..2bbfe34cdc326e 100644 --- a/editor/utils/block-list.js +++ b/editor/utils/block-list.js @@ -1,12 +1,16 @@ /** * External dependencies */ -import { noop } from 'lodash'; +import { isEqual, noop, omit } from 'lodash'; /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { Component, compose } from '@wordpress/element'; +import { + synchronizeBlocksWithTemplate, +} from '@wordpress/blocks'; +import { withSelect, withDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -35,33 +39,87 @@ const INNER_BLOCK_LIST_CACHE = {}; */ export function createInnerBlockList( uid, renderBlockMenu = noop ) { if ( ! INNER_BLOCK_LIST_CACHE[ uid ] ) { - INNER_BLOCK_LIST_CACHE[ uid ] = [ - // The component class: - class extends Component { - componentWillMount() { - INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]++; + const InnerBlockListComponent = class extends Component { + componentWillReceiveProps( nextProps ) { + this.updateNestedSettings( { + supportedBlocks: nextProps.allowedBlocks, + } ); + } + + componentWillUnmount() { + // If, after decrementing the tracking count, there are no + // remaining instances of the component, remove from cache. + if ( ! INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]-- ) { + delete INNER_BLOCK_LIST_CACHE[ uid ]; } + } + + componentDidMount() { + INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]++; + this.updateNestedSettings( { + supportedBlocks: this.props.allowedBlocks, + } ); + this.insertTemplateBlocks( this.props.template ); + } - componentWillUnmount() { - // If, after decrementing the tracking count, there are no - // remaining instances of the component, remove from cache. - if ( ! INNER_BLOCK_LIST_CACHE[ uid ][ 1 ]-- ) { - delete INNER_BLOCK_LIST_CACHE[ uid ]; - } + insertTemplateBlocks( template ) { + const { block, insertBlocks } = this.props; + if ( template && ! block.innerBlocks.length ) { + // synchronizeBlocksWithTemplate( [], template ) parses the template structure, + // and returns/creates the necessary blocks to represent it. + insertBlocks( synchronizeBlocksWithTemplate( [], template ) ); } + } - render() { - return ( - - ); + updateNestedSettings( newSettings ) { + if ( ! isEqual( this.props.blockListSettings, newSettings ) ) { + this.props.updateNestedSettings( newSettings ); } - }, + } - // A counter tracking active mounted instances: - 0, + render() { + return ( + + ); + } + }; + + const InnerBlockListComponentContainer = compose( + withSelect( ( select ) => { + const { getBlock, getBlockListSettings } = select( 'core/editor' ); + return { + block: getBlock( uid ), + blockListSettings: getBlockListSettings( uid ), + }; + } ), + withDispatch( ( dispatch ) => { + const { insertBlocks, updateBlockListSettings } = dispatch( 'core/editor' ); + return { + insertBlocks( blocks ) { + dispatch( insertBlocks( blocks, undefined, uid ) ); + }, + updateNestedSettings( settings ) { + dispatch( updateBlockListSettings( uid, settings ) ); + }, + }; + } ), + )( InnerBlockListComponent ); + + INNER_BLOCK_LIST_CACHE[ uid ] = [ + InnerBlockListComponentContainer, + 0, // A counter tracking active mounted instances: ]; }