diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index 6931c7b1d7f65..85b2af41e2e06 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -8,9 +8,7 @@ import '@wordpress/viewport'; /** * Internal dependencies */ -import './store'; import './hooks'; - export * from './components'; export * from './utils'; export { storeConfig } from './store'; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 9bafe5b832844..4ab5d663f551b 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -14,6 +14,7 @@ import { orderBy, reduce, some, + find, } from 'lodash'; import createSelector from 'rememo'; @@ -24,7 +25,9 @@ import { getBlockType, getBlockTypes, hasBlockSupport, + parse, } from '@wordpress/blocks'; +import { SVG, Rect, G, Path } from '@wordpress/components'; // Module constants @@ -51,6 +54,7 @@ export const INSERTER_UTILITY_NONE = 0; const MILLISECONDS_PER_HOUR = 3600 * 1000; const MILLISECONDS_PER_DAY = 24 * 3600 * 1000; const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; +const templateIcon = ; /** * Shared reference to an empty array for cases where it is important to avoid @@ -645,29 +649,6 @@ export function getLastMultiSelectedBlockClientId( state ) { return last( getMultiSelectedBlockClientIds( state ) ) || null; } -/** - * Checks if possibleAncestorId is an ancestor of possibleDescendentId. - * - * @param {Object} state Editor state. - * @param {string} possibleAncestorId Possible ancestor client ID. - * @param {string} possibleDescendentId Possible descent client ID. - * - * @return {boolean} True if possibleAncestorId is an ancestor - * of possibleDescendentId, and false otherwise. - */ -const isAncestorOf = createSelector( - ( state, possibleAncestorId, possibleDescendentId ) => { - let idToCheck = possibleDescendentId; - while ( possibleAncestorId !== idToCheck && idToCheck ) { - idToCheck = getBlockRootClientId( state, idToCheck ); - } - return possibleAncestorId === idToCheck; - }, - ( state ) => [ - state.blocks.order, - ], -); - /** * Returns true if a multi-selection exists, and the block corresponding to the * specified client ID is the first block of the multi-selection set, or false @@ -1111,41 +1092,6 @@ const canIncludeBlockTypeInInserter = ( state, blockType, rootClientId ) => { return canInsertBlockTypeUnmemoized( state, blockType.name, rootClientId ); }; -/** - * Returns whether we can show a reusable block in the inserter - * - * @param {Object} state Global State - * @param {Object} reusableBlock Reusable block object - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {boolean} Whether the given block type is allowed to be shown in the inserter. - */ -const canIncludeReusableBlockInInserter = ( state, reusableBlock, rootClientId ) => { - if ( ! canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) ) { - return false; - } - - const referencedBlockName = getBlockName( state, reusableBlock.clientId ); - if ( ! referencedBlockName ) { - return false; - } - - const referencedBlockType = getBlockType( referencedBlockName ); - if ( ! referencedBlockType ) { - return false; - } - - if ( ! canInsertBlockTypeUnmemoized( state, referencedBlockName, rootClientId ) ) { - return false; - } - - if ( isAncestorOf( state, reusableBlock.clientId, rootClientId ) ) { - return false; - } - - return true; -}; - /** * Determines the items that appear in the inserter. Includes both static * items (e.g. a regular block type) and dynamic items (e.g. a reusable block). @@ -1246,8 +1192,11 @@ export const getInserterItems = createSelector( const buildReusableBlockInserterItem = ( reusableBlock ) => { const id = `core/block/${ reusableBlock.id }`; - const referencedBlockName = getBlockName( state, reusableBlock.clientId ); - const referencedBlockType = getBlockType( referencedBlockName ); + const referencedBlocks = __experimentalGetParsedReusableBlock( state, reusableBlock.id ); + let referencedBlockType; + if ( referencedBlocks.length === 1 ) { + referencedBlockType = getBlockType( referencedBlocks[ 0 ].name ); + } const { time, count = 0 } = getInsertUsage( state, id ) || {}; const utility = calculateUtility( 'reusable', count, false ); @@ -1258,7 +1207,7 @@ export const getInserterItems = createSelector( name: 'core/block', initialAttributes: { ref: reusableBlock.id }, title: reusableBlock.title, - icon: referencedBlockType.icon, + icon: referencedBlockType ? referencedBlockType.icon : templateIcon, category: 'reusable', keywords: [], isDisabled: false, @@ -1271,9 +1220,9 @@ export const getInserterItems = createSelector( .filter( ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) ) .map( buildBlockTypeInserterItem ); - const reusableBlockInserterItems = getReusableBlocks( state ) - .filter( ( block ) => canIncludeReusableBlockInInserter( state, block, rootClientId ) ) - .map( buildReusableBlockInserterItem ); + const reusableBlockInserterItems = canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) ? + getReusableBlocks( state ).map( buildReusableBlockInserterItem ) : + []; return orderBy( [ ...blockTypeInserterItems, ...reusableBlockInserterItems ], @@ -1310,9 +1259,9 @@ export const hasInserterItems = createSelector( if ( hasBlockType ) { return true; } - const hasReusableBlock = some( - getReusableBlocks( state ), - ( block ) => canIncludeReusableBlockInInserter( state, block, rootClientId ) + const hasReusableBlock = ( + canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) && + getReusableBlocks( state ).length > 0 ); return hasReusableBlock; @@ -1363,6 +1312,31 @@ export function isLastBlockChangePersistent( state ) { return state.blocks.isPersistentChange; } +/** + * Returns the parsed block saved as shared block with the given ID. + * + * @param {Object} state Global application state. + * @param {number|string} ref The shared block's ID. + * + * @return {Object} The parsed block. + */ +export const __experimentalGetParsedReusableBlock = createSelector( + ( state, ref ) => { + const reusableBlock = find( + getReusableBlocks( state ), + ( block ) => block.id === ref + ); + if ( ! reusableBlock ) { + return null; + } + + return parse( reusableBlock.content ); + }, + ( state ) => [ + getReusableBlocks( state ), + ], +); + /** * Returns true if the most recent block change is be considered ignored, or * false otherwise. An ignored change is one not to be committed by diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 1e7dfe7f2b6f4..22ee0ed1adef3 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -1934,19 +1934,21 @@ describe( 'selectors', () => { it( 'should properly list block type and reusable block items', () => { const state = { blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - }, - attributes: { - block1: {}, - }, + byClientId: {}, + attributes: {}, order: {}, parents: {}, cache: {}, }, settings: { __experimentalReusableBlocks: [ - { id: 1, isTemporary: false, clientId: 'block1', title: 'Reusable Block 1' }, + { + id: 1, + isTemporary: false, + clientId: 'block1', + title: 'Reusable Block 1', + content: '', + }, ], }, // Intentionally include a test case which considers @@ -1989,155 +1991,31 @@ describe( 'selectors', () => { } ); } ); - it( 'should not list a reusable block item if it is being inserted inside it self', () => { - const state = { - blocks: { - byClientId: { - block1ref: { - name: 'core/block', - clientId: 'block1ref', - }, - itselfBlock1: { name: 'core/test-block-a' }, - itselfBlock2: { name: 'core/test-block-b' }, - }, - attributes: { - block1ref: { - attributes: { - ref: 1, - }, - }, - itselfBlock1: {}, - itselfBlock2: {}, - }, - order: { - '': [ 'block1ref' ], - }, - parents: { - block1ref: '', - }, - cache: { - block1ref: {}, - }, - }, - settings: { - __experimentalReusableBlocks: [ - { id: 1, isTemporary: false, clientId: 'itselfBlock1', title: 'Reusable Block 1' }, - { id: 2, isTemporary: false, clientId: 'itselfBlock2', title: 'Reusable Block 2' }, - ], - }, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - }; - const items = getInserterItems( state, 'itselfBlock1' ); - const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); - expect( reusableBlockItems ).toHaveLength( 1 ); - expect( reusableBlockItems[ 0 ] ).toEqual( { - id: 'core/block/2', - name: 'core/block', - initialAttributes: { ref: 2 }, - title: 'Reusable Block 2', - icon: { - src: 'test', - }, - category: 'reusable', - keywords: [], - isDisabled: false, - utility: 0, - frecency: 0, - } ); - } ); - - it( 'should not list a reusable block item if it is being inserted inside a descendent', () => { - const state = { - blocks: { - byClientId: { - block2ref: { - name: 'core/block', - clientId: 'block1ref', - }, - referredBlock1: { name: 'core/test-block-a' }, - referredBlock2: { name: 'core/test-block-b' }, - childReferredBlock2: { name: 'core/test-block-a' }, - grandchildReferredBlock2: { name: 'core/test-block-b' }, - }, - attributes: { - block2ref: { - attributes: { - ref: 2, - }, - }, - referredBlock1: {}, - referredBlock2: {}, - childReferredBlock2: {}, - grandchildReferredBlock2: {}, - }, - order: { - '': [ 'block2ref' ], - referredBlock2: [ 'childReferredBlock2' ], - childReferredBlock2: [ 'grandchildReferredBlock2' ], - }, - parents: { - block2ref: '', - childReferredBlock2: 'referredBlock2', - grandchildReferredBlock2: 'childReferredBlock2', - }, - cache: { - block2ref: {}, - childReferredBlock2: {}, - grandchildReferredBlock2: {}, - }, - }, - - settings: { - __experimentalReusableBlocks: [ - { id: 1, isTemporary: false, clientId: 'referredBlock1', title: 'Reusable Block 1' }, - { id: 2, isTemporary: false, clientId: 'referredBlock2', title: 'Reusable Block 2' }, - ], - }, - preferences: { - insertUsage: {}, - }, - blockListSettings: {}, - }; - const items = getInserterItems( state, 'grandchildReferredBlock2' ); - const reusableBlockItems = filter( items, [ 'name', 'core/block' ] ); - expect( reusableBlockItems ).toHaveLength( 1 ); - expect( reusableBlockItems[ 0 ] ).toEqual( { - id: 'core/block/1', - name: 'core/block', - initialAttributes: { ref: 1 }, - title: 'Reusable Block 1', - icon: { - src: 'test', - }, - category: 'reusable', - keywords: [], - isDisabled: false, - utility: 0, - frecency: 0, - } ); - } ); it( 'should order items by descending utility and frecency', () => { const state = { blocks: { - byClientId: { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-a' }, - }, - attributes: { - block1: {}, - block2: {}, - }, + byClientId: {}, + attributes: {}, order: {}, parents: {}, cache: {}, }, settings: { __experimentalReusableBlocks: [ - { id: 1, isTemporary: false, clientId: 'block1', title: 'Reusable Block 1' }, - { id: 2, isTemporary: false, clientId: 'block2', title: 'Reusable Block 2' }, + { + id: 1, + isTemporary: false, + clientId: 'block1', + title: 'Reusable Block 1', + content: '', + }, + { + id: 2, + isTemporary: false, + clientId: 'block2', + title: 'Reusable Block 2', + content: '', + }, ], }, preferences: { @@ -2162,14 +2040,10 @@ describe( 'selectors', () => { const state = { blocks: { byClientId: { - block1: { name: 'core/test-block-a' }, - block2: { name: 'core/test-block-a' }, block3: { name: 'core/test-block-a' }, block4: { name: 'core/test-block-a' }, }, attributes: { - block1: {}, - block2: {}, block3: {}, block4: {}, }, @@ -2181,16 +2055,26 @@ describe( 'selectors', () => { block4: '', }, cache: { - block1: {}, - block2: {}, block3: {}, block4: {}, }, }, settings: { __experimentalReusableBlocks: [ - { id: 1, isTemporary: false, clientId: 'block1', title: 'Reusable Block 1' }, - { id: 2, isTemporary: false, clientId: 'block2', title: 'Reusable Block 2' }, + { + id: 1, + isTemporary: false, + clientId: 'block1', + title: 'Reusable Block 1', + content: '', + }, + { + id: 2, + isTemporary: false, + clientId: 'block2', + title: 'Reusable Block 2', + content: '', + }, ], }, preferences: { diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 785f5a762c01f..7e450327085df 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -1,17 +1,25 @@ /** * External dependencies */ -import { noop, partial } from 'lodash'; +import { partial } from 'lodash'; /** * WordPress dependencies */ import { Component } from '@wordpress/element'; import { Placeholder, Spinner, Disabled } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { + withSelect, + withDispatch, +} from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { BlockEdit } from '@wordpress/block-editor'; +import { + BlockEditorProvider, + BlockList, + WritingFlow, +} from '@wordpress/block-editor'; import { compose } from '@wordpress/compose'; +import { parse, serialize } from '@wordpress/blocks'; /** * Internal dependencies @@ -25,23 +33,23 @@ class ReusableBlockEdit extends Component { this.startEditing = this.startEditing.bind( this ); this.stopEditing = this.stopEditing.bind( this ); - this.setAttributes = this.setAttributes.bind( this ); + this.setBlocks = this.setBlocks.bind( this ); this.setTitle = this.setTitle.bind( this ); this.save = this.save.bind( this ); - if ( reusableBlock && reusableBlock.isTemporary ) { + if ( reusableBlock ) { // Start in edit mode when we're working with a newly created reusable block this.state = { - isEditing: true, + isEditing: reusableBlock.isTemporary, title: reusableBlock.title, - changedAttributes: {}, + blocks: parse( reusableBlock.content ), }; } else { // Start in preview mode when we're working with an existing reusable block this.state = { isEditing: false, title: null, - changedAttributes: null, + blocks: [], }; } } @@ -52,13 +60,21 @@ class ReusableBlockEdit extends Component { } } + componentDidUpdate( prevProps ) { + if ( prevProps.reusableBlock !== this.props.reusableBlock && this.state.title === null ) { + this.setState( { + title: this.props.reusableBlock.title, + blocks: parse( this.props.reusableBlock.content ), + } ); + } + } + startEditing() { const { reusableBlock } = this.props; - this.setState( { isEditing: true, title: reusableBlock.title, - changedAttributes: {}, + blocks: parse( reusableBlock.content ), } ); } @@ -66,16 +82,12 @@ class ReusableBlockEdit extends Component { this.setState( { isEditing: false, title: null, - changedAttributes: null, + blocks: [], } ); } - setAttributes( attributes ) { - this.setState( ( prevState ) => { - if ( prevState.changedAttributes !== null ) { - return { changedAttributes: { ...prevState.changedAttributes, ...attributes } }; - } - } ); + setBlocks( blocks ) { + this.setState( { blocks } ); } setTitle( title ) { @@ -83,40 +95,38 @@ class ReusableBlockEdit extends Component { } save() { - const { reusableBlock, onUpdateTitle, updateAttributes, block, onSave } = this.props; - const { title, changedAttributes } = this.state; - - if ( title !== reusableBlock.title ) { - onUpdateTitle( title ); - } - - updateAttributes( block.clientId, changedAttributes ); + const { onChange, onSave } = this.props; + const { blocks, title } = this.state; + const content = serialize( blocks ); + onChange( { title, content } ); onSave(); this.stopEditing(); } render() { - const { isSelected, reusableBlock, block, isFetching, isSaving, canUpdateBlock } = this.props; - const { isEditing, title, changedAttributes } = this.state; + const { isSelected, reusableBlock, isFetching, isSaving, canUpdateBlock, settings } = this.props; + const { isEditing, title, blocks } = this.state; if ( ! reusableBlock && isFetching ) { return ; } - if ( ! reusableBlock || ! block ) { + if ( ! reusableBlock ) { return { __( 'Block has been deleted or is unavailable.' ) }; } let element = ( - + + + + + ); if ( ! isEditing ) { @@ -124,7 +134,7 @@ class ReusableBlockEdit extends Component { } return ( - <> +
{ ( isSelected || isEditing ) && ( } { element } - +
); } } @@ -153,7 +163,8 @@ export default compose( [ } = select( 'core/editor' ); const { canUser } = select( 'core' ); const { - getBlock, + __experimentalGetParsedReusableBlock, + getSettings, } = select( 'core/block-editor' ); const { ref } = ownProps.attributes; const reusableBlock = getReusableBlock( ref ); @@ -162,25 +173,22 @@ export default compose( [ reusableBlock, isFetching: isFetchingReusableBlock( ref ), isSaving: isSavingReusableBlock( ref ), - block: reusableBlock ? getBlock( reusableBlock.clientId ) : null, + blocks: reusableBlock ? __experimentalGetParsedReusableBlock( reusableBlock.id ) : null, canUpdateBlock: !! reusableBlock && ! reusableBlock.isTemporary && !! canUser( 'update', 'blocks', ref ), + settings: getSettings(), }; } ), withDispatch( ( dispatch, ownProps ) => { const { __experimentalFetchReusableBlocks: fetchReusableBlocks, - __experimentalUpdateReusableBlockTitle: updateReusableBlockTitle, + __experimentalUpdateReusableBlock: updateReusableBlock, __experimentalSaveReusableBlock: saveReusableBlock, } = dispatch( 'core/editor' ); - const { - updateBlockAttributes, - } = dispatch( 'core/block-editor' ); const { ref } = ownProps.attributes; return { fetchReusableBlock: partial( fetchReusableBlocks, ref ), - updateAttributes: updateBlockAttributes, - onUpdateTitle: partial( updateReusableBlockTitle, ref ), + onChange: partial( updateReusableBlock, ref ), onSave: partial( saveReusableBlock, ref ), }; } ), diff --git a/packages/block-library/src/block/editor.scss b/packages/block-library/src/block/editor.scss new file mode 100644 index 0000000000000..cdeaf9ad0ef6a --- /dev/null +++ b/packages/block-library/src/block/editor.scss @@ -0,0 +1,3 @@ +.edit-post-visual-editor .block-library-block__reusable-block-container .block-editor-writing-flow__click-redirect { + height: auto; +} diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 5c40e56925d8f..3b38a4e136640 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -1,5 +1,6 @@ @import "./archives/editor.scss"; @import "./audio/editor.scss"; +@import "./block/editor.scss"; @import "./button/editor.scss"; @import "./categories/editor.scss"; @import "./code/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index bcb177db239ef..e05057a05ed10 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -54,7 +54,6 @@ import * as shortcode from './shortcode'; import * as spacer from './spacer'; import * as subhead from './subhead'; import * as table from './table'; -import * as template from './template'; import * as textColumns from './text-columns'; import * as verse from './verse'; import * as video from './video'; @@ -137,7 +136,6 @@ export const registerCoreBlocks = () => { subhead, table, tagCloud, - template, textColumns, verse, video, diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index b3411411ee75f..8d5eab4182e50 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -44,7 +44,6 @@ import * as shortcode from './shortcode'; import * as spacer from './spacer'; import * as subhead from './subhead'; import * as table from './table'; -import * as template from './template'; import * as textColumns from './text-columns'; import * as verse from './verse'; import * as video from './video'; @@ -92,7 +91,6 @@ export const coreBlocks = [ subhead, table, tagCloud, - template, textColumns, verse, video, diff --git a/packages/block-library/src/template/block.json b/packages/block-library/src/template/block.json deleted file mode 100644 index fc5600a48cc3a..0000000000000 --- a/packages/block-library/src/template/block.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "core/template", - "category": "reusable" -} diff --git a/packages/block-library/src/template/edit.js b/packages/block-library/src/template/edit.js deleted file mode 100644 index 7a78461fc6ff2..0000000000000 --- a/packages/block-library/src/template/edit.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * WordPress dependencies - */ -import { InnerBlocks } from '@wordpress/block-editor'; - -export default function TemplateEdit() { - return ; -} diff --git a/packages/block-library/src/template/icon.js b/packages/block-library/src/template/icon.js deleted file mode 100644 index e62335ee654bb..0000000000000 --- a/packages/block-library/src/template/icon.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * WordPress dependencies - */ -import { G, Path, Rect, SVG } from '@wordpress/components'; - -export default ( - -); diff --git a/packages/block-library/src/template/index.js b/packages/block-library/src/template/index.js deleted file mode 100644 index 7abddbd6552bf..0000000000000 --- a/packages/block-library/src/template/index.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import edit from './edit'; -import icon from './icon'; -import metadata from './block.json'; -import save from './save'; - -const { name } = metadata; - -export { metadata, name }; - -export const settings = { - title: __( 'Reusable Template' ), - description: __( 'Template block used as a container.' ), - icon, - supports: { - customClassName: false, - html: false, - inserter: false, - }, - edit, - save, -}; diff --git a/packages/block-library/src/template/save.js b/packages/block-library/src/template/save.js deleted file mode 100644 index 17571d8f30d2d..0000000000000 --- a/packages/block-library/src/template/save.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * WordPress dependencies - */ -import { InnerBlocks } from '@wordpress/block-editor'; - -export default function save() { - return ; -} diff --git a/packages/e2e-tests/specs/reusable-blocks.test.js b/packages/e2e-tests/specs/reusable-blocks.test.js index 81f8db73d4862..0cc0232acd6b2 100644 --- a/packages/e2e-tests/specs/reusable-blocks.test.js +++ b/packages/e2e-tests/specs/reusable-blocks.test.js @@ -118,6 +118,7 @@ describe( 'Reusable Blocks', () => { // Tab three times to navigate to the block's content await page.keyboard.press( 'Tab' ); await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); // Enter edit mode // Change the block's content await page.keyboard.type( 'Oh! ' ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 78b58f919dc16..a20b0624fa3ea 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -628,19 +628,19 @@ export function __experimentalDeleteReusableBlock( id ) { } /** - * Returns an action object used in signalling that a reusable block's title is + * Returns an action object used in signalling that a reusable block is * to be updated. * - * @param {number} id The ID of the reusable block to update. - * @param {string} title The new title. + * @param {number} id The ID of the reusable block to update. + * @param {Object} changes The changes to apply. * * @return {Object} Action object. */ -export function __experimentalUpdateReusableBlockTitle( id, title ) { +export function __experimentalUpdateReusableBlock( id, changes ) { return { - type: 'UPDATE_REUSABLE_BLOCK_TITLE', + type: 'UPDATE_REUSABLE_BLOCK', id, - title, + changes, }; } diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index 0893f8315cbf1..9e831b7a5a4b7 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -7,7 +7,6 @@ import { deleteReusableBlocks, convertBlockToReusable, convertBlockToStatic, - receiveReusableBlocks, } from './effects/reusable-blocks'; export default { @@ -20,7 +19,6 @@ export default { DELETE_REUSABLE_BLOCK: ( action, store ) => { deleteReusableBlocks( action, store ); }, - RECEIVE_REUSABLE_BLOCKS: receiveReusableBlocks, CONVERT_BLOCK_TO_STATIC: convertBlockToStatic, CONVERT_BLOCK_TO_REUSABLE: convertBlockToReusable, }; diff --git a/packages/editor/src/store/effects/reusable-blocks.js b/packages/editor/src/store/effects/reusable-blocks.js index ebd4caee7fbed..bdc0ebaa5a7d9 100644 --- a/packages/editor/src/store/effects/reusable-blocks.js +++ b/packages/editor/src/store/effects/reusable-blocks.js @@ -13,7 +13,6 @@ import { serialize, createBlock, isReusableBlock, - cloneBlock, } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; // TODO: Ideally this would be the only dispatch in scope. This requires either @@ -31,7 +30,6 @@ import { import { __experimentalGetReusableBlock as getReusableBlock, } from '../selectors'; -import { getPostRawValue } from '../reducer'; /** * Module Constants @@ -69,15 +67,10 @@ export const fetchReusableBlocks = async ( action, store ) => { return null; } - const parsedBlocks = parse( post.content.raw ); return { - reusableBlock: { - id: post.id, - title: getPostRawValue( post.title ), - }, - parsedBlock: parsedBlocks.length === 1 ? - parsedBlocks[ 0 ] : - createBlock( 'core/template', {}, parsedBlocks ), + ...post, + content: post.content.raw, + title: post.title.raw, }; } ) ); @@ -115,9 +108,7 @@ export const saveReusableBlocks = async ( action, store ) => { const { id } = action; const { dispatch } = store; const state = store.getState(); - const { clientId, title, isTemporary } = getReusableBlock( state, id ); - const reusableBlock = select( 'core/block-editor' ).getBlock( clientId ); - const content = serialize( reusableBlock.name === 'core/template' ? reusableBlock.innerBlocks : reusableBlock ); + const { title, content, isTemporary } = getReusableBlock( state, id ); const data = isTemporary ? { title, content, status: 'publish' } : { id, title, content, status: 'publish' }; const path = isTemporary ? `/wp/v2/${ postType.rest_base }` : `/wp/v2/${ postType.rest_base }/${ id }`; @@ -167,7 +158,6 @@ export const deleteReusableBlocks = async ( action, store ) => { if ( ! reusableBlock || reusableBlock.isTemporary ) { return; } - // Remove any other blocks that reference this reusable block const allBlocks = select( 'core/block-editor' ).getBlocks(); const associatedBlocks = allBlocks.filter( ( block ) => isReusableBlock( block ) && block.attributes.ref === id ); @@ -182,10 +172,9 @@ export const deleteReusableBlocks = async ( action, store ) => { } ); // Remove the parsed block. - dataDispatch( 'core/block-editor' ).removeBlocks( [ - ...associatedBlockClientIds, - reusableBlock.clientId, - ] ); + if ( associatedBlockClientIds.length ) { + dataDispatch( 'core/block-editor' ).removeBlocks( associatedBlockClientIds ); + } try { await apiFetch( { @@ -214,15 +203,6 @@ export const deleteReusableBlocks = async ( action, store ) => { } }; -/** - * Receive Reusable Blocks Effect Handler. - * - * @param {Object} action action object. - */ -export const receiveReusableBlocks = ( action ) => { - dataDispatch( 'core/block-editor' ).receiveBlocks( map( action.results, 'parsedBlock' ) ); -}; - /** * Convert a reusable block to a static block effect handler * @@ -233,13 +213,7 @@ export const convertBlockToStatic = ( action, store ) => { const state = store.getState(); const oldBlock = select( 'core/block-editor' ).getBlock( action.clientId ); const reusableBlock = getReusableBlock( state, oldBlock.attributes.ref ); - const referencedBlock = select( 'core/block-editor' ).getBlock( reusableBlock.clientId ); - let newBlocks; - if ( referencedBlock.name === 'core/template' ) { - newBlocks = referencedBlock.innerBlocks.map( ( innerBlock ) => cloneBlock( innerBlock ) ); - } else { - newBlocks = [ cloneBlock( referencedBlock ) ]; - } + const newBlocks = parse( reusableBlock.content ); dataDispatch( 'core/block-editor' ).replaceBlocks( oldBlock.clientId, newBlocks ); }; @@ -251,32 +225,15 @@ export const convertBlockToStatic = ( action, store ) => { */ export const convertBlockToReusable = ( action, store ) => { const { dispatch } = store; - let parsedBlock; - if ( action.clientIds.length === 1 ) { - parsedBlock = select( 'core/block-editor' ).getBlock( action.clientIds[ 0 ] ); - } else { - parsedBlock = createBlock( - 'core/template', - {}, - select( 'core/block-editor' ).getBlocksByClientId( action.clientIds ) - ); - - // This shouldn't be necessary but at the moment - // we expect the content of the shared blocks to live in the blocks state. - dataDispatch( 'core/block-editor' ).receiveBlocks( [ parsedBlock ] ); - } - const reusableBlock = { id: uniqueId( 'reusable' ), - clientId: parsedBlock.clientId, title: __( 'Untitled Reusable Block' ), + content: serialize( select( 'core/block-editor' ).getBlocksByClientId( action.clientIds ) ), }; - dispatch( receiveReusableBlocksAction( [ { + dispatch( receiveReusableBlocksAction( [ reusableBlock, - parsedBlock, - } ] ) ); - + ] ) ); dispatch( saveReusableBlock( reusableBlock.id ) ); dataDispatch( 'core/block-editor' ).replaceBlocks( @@ -285,7 +242,4 @@ export const convertBlockToReusable = ( action, store ) => { ref: reusableBlock.id, } ) ); - - // Re-add the original block to the store, since replaceBlock() will have removed it - dataDispatch( 'core/block-editor' ).receiveBlocks( [ parsedBlock ] ); }; diff --git a/packages/editor/src/store/effects/test/reusable-blocks.js b/packages/editor/src/store/effects/test/reusable-blocks.js index 1d783b1dd9fe1..97ae30feaf949 100644 --- a/packages/editor/src/store/effects/test/reusable-blocks.js +++ b/packages/editor/src/store/effects/test/reusable-blocks.js @@ -20,7 +20,6 @@ import { dispatch as dataDispatch, select as dataSelect } from '@wordpress/data' import { fetchReusableBlocks, saveReusableBlocks, - receiveReusableBlocks, deleteReusableBlocks, convertBlockToStatic, convertBlockToReusable, @@ -99,14 +98,10 @@ describe( 'reusable blocks effects', () => { expect( dispatch ).toHaveBeenCalledWith( receiveReusableBlocksAction( [ { - reusableBlock: { - id: 123, - title: 'My cool block', - }, - parsedBlock: expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), + id: 123, + title: 'My cool block', + content: '', + status: 'publish', }, ] ) ); @@ -146,18 +141,12 @@ describe( 'reusable blocks effects', () => { await fetchReusableBlocks( fetchReusableBlocksAction( 123 ), store ); expect( dispatch ).toHaveBeenCalledWith( - receiveReusableBlocksAction( [ - { - reusableBlock: { - id: 123, - title: 'My cool block', - }, - parsedBlock: expect.objectContaining( { - name: 'core/test-block', - attributes: { name: 'Big Bird' }, - } ), - }, - ] ) + receiveReusableBlocksAction( [ { + id: 123, + title: 'My cool block', + content: '', + status: 'publish', + } ] ) ); expect( dispatch ).toHaveBeenCalledWith( { type: 'FETCH_REUSABLE_BLOCKS_SUCCESS', @@ -248,11 +237,8 @@ describe( 'reusable blocks effects', () => { return savePromise; } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( () => parsedBlock ); + const reusableBlock = { id: 123, title: 'My cool block', content: '' }; + const state = reducer( undefined, receiveReusableBlocksAction( [ reusableBlock ] ) ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; @@ -264,8 +250,6 @@ describe( 'reusable blocks effects', () => { id: 123, updatedId: 456, } ); - - dataSelect( 'core/block-editor' ).getBlock.mockReset(); } ); it( 'should handle an API error', async () => { @@ -282,11 +266,8 @@ describe( 'reusable blocks effects', () => { return savePromise; } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( () => parsedBlock ); + const reusableBlock = { id: 123, title: 'My cool block', content: '' }; + const state = reducer( undefined, receiveReusableBlocksAction( [ reusableBlock ] ) ); const dispatch = jest.fn(); const store = { getState: () => state, dispatch }; @@ -296,26 +277,6 @@ describe( 'reusable blocks effects', () => { type: 'SAVE_REUSABLE_BLOCK_FAILURE', id: 123, } ); - - dataSelect( 'core/block-editor' ).getBlock.mockReset(); - } ); - } ); - - describe( 'receiveReusableBlocks', () => { - it( 'should receive parsed blocks', () => { - const action = receiveReusableBlocksAction( [ - { - parsedBlock: { clientId: 'broccoli' }, - }, - ] ); - - jest.spyOn( dataDispatch( 'core/block-editor' ), 'receiveBlocks' ).mockImplementation( () => {} ); - receiveReusableBlocks( action ); - expect( dataDispatch( 'core/block-editor' ).receiveBlocks ).toHaveBeenCalledWith( [ - { clientId: 'broccoli' }, - ] ); - - dataDispatch( 'core/block-editor' ).receiveBlocks.mockReset(); } ); } ); @@ -334,14 +295,11 @@ describe( 'reusable blocks effects', () => { return deletePromise; } ); + const reusableBlock = { id: 123, title: 'My cool block', content: '' }; + const state = reducer( undefined, receiveReusableBlocksAction( [ reusableBlock ] ) ); const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ associatedBlock, - parsedBlock, ] ); jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); @@ -357,7 +315,7 @@ describe( 'reusable blocks effects', () => { } ); expect( dataDispatch( 'core/block-editor' ).removeBlocks ).toHaveBeenCalledWith( - [ associatedBlock.clientId, parsedBlock.clientId ] + [ associatedBlock.clientId ] ); expect( dispatch ).toHaveBeenCalledWith( { @@ -384,12 +342,9 @@ describe( 'reusable blocks effects', () => { return deletePromise; } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ - parsedBlock, - ] ); + const reusableBlock = { id: 123, title: 'My cool block', content: '' }; + const state = reducer( undefined, receiveReusableBlocksAction( [ reusableBlock ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [] ); jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); @@ -408,12 +363,8 @@ describe( 'reusable blocks effects', () => { it( 'should not save reusable blocks with temporary IDs', async () => { const reusableBlock = { id: 'reusable1', title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [ - parsedBlock, - ] ); + const state = reducer( undefined, receiveReusableBlocksAction( [ reusableBlock ] ) ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocks' ).mockImplementation( () => [] ); jest.spyOn( dataDispatch( 'core/block-editor' ), 'removeBlocks' ).mockImplementation( () => {} ); const dispatch = jest.fn(); @@ -430,12 +381,10 @@ describe( 'reusable blocks effects', () => { describe( 'convertBlockToStatic', () => { it( 'should convert a reusable block into a static block', () => { const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' } ); - - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + const reusableBlock = { id: 123, title: 'My cool block', content: '' }; + const state = reducer( undefined, receiveReusableBlocksAction( [ reusableBlock ] ) ); jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( id ) => - associatedBlock.clientId === id ? associatedBlock : parsedBlock + associatedBlock.clientId === id ? associatedBlock : null ); jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); @@ -460,14 +409,14 @@ describe( 'reusable blocks effects', () => { it( 'should convert a reusable block with nested blocks into a static block', () => { const associatedBlock = createBlock( 'core/block', { ref: 123 } ); - const reusableBlock = { id: 123, title: 'My cool block' }; - const parsedBlock = createBlock( 'core/test-block', { name: 'Big Bird' }, [ - createBlock( 'core/test-block', { name: 'Oscar the Grouch' } ), - createBlock( 'core/test-block', { name: 'Cookie Monster' } ), - ] ); - const state = reducer( undefined, receiveReusableBlocksAction( [ { reusableBlock, parsedBlock } ] ) ); + const reusableBlock = { + id: 123, + title: 'My cool block', + content: '', + }; + const state = reducer( undefined, receiveReusableBlocksAction( [ reusableBlock ] ) ); jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( id ) => - associatedBlock.clientId === id ? associatedBlock : parsedBlock + associatedBlock.clientId === id ? associatedBlock : null ); jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); @@ -501,9 +450,9 @@ describe( 'reusable blocks effects', () => { describe( 'convertBlockToReusable', () => { it( 'should convert a static block into a reusable block', () => { - const staticBlock = createBlock( 'core/block', { ref: 123 } ); - jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlock' ).mockImplementation( ( ) => - staticBlock + const staticBlock = createBlock( 'core/test-block' ); + jest.spyOn( dataSelect( 'core/block-editor' ), 'getBlocksByClientId' ).mockImplementation( ( ) => + [ staticBlock ] ); jest.spyOn( dataDispatch( 'core/block-editor' ), 'replaceBlocks' ).mockImplementation( () => {} ); jest.spyOn( dataDispatch( 'core/block-editor' ), 'receiveBlocks' ).mockImplementation( () => {} ); @@ -515,12 +464,9 @@ describe( 'reusable blocks effects', () => { expect( dispatch ).toHaveBeenCalledWith( receiveReusableBlocksAction( [ { - reusableBlock: { - id: expect.stringMatching( /^reusable/ ), - clientId: staticBlock.clientId, - title: 'Untitled Reusable Block', - }, - parsedBlock: staticBlock, + id: expect.stringMatching( /^reusable/ ), + title: 'Untitled Reusable Block', + content: '', } ] ) ); @@ -536,10 +482,6 @@ describe( 'reusable blocks effects', () => { } ), ); - expect( dataDispatch( 'core/block-editor' ).receiveBlocks ).toHaveBeenCalledWith( - [ staticBlock ] - ); - dataDispatch( 'core/block-editor' ).replaceBlocks.mockReset(); dataDispatch( 'core/block-editor' ).receiveBlocks.mockReset(); dataSelect( 'core/block-editor' ).getBlock.mockReset(); diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 69ab09f7bf0c8..7014d8a3b1fe5 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -2,7 +2,12 @@ * External dependencies */ import optimist from 'redux-optimist'; -import { reduce, omit, keys, isEqual } from 'lodash'; +import { + omit, + keys, + isEqual, + keyBy, +} from 'lodash'; /** * WordPress dependencies @@ -30,23 +35,6 @@ export function getPostRawValue( value ) { return value; } -/** - * Returns an object against which it is safe to perform mutating operations, - * given the original object and its current working copy. - * - * @param {Object} original Original object. - * @param {Object} working Working object. - * - * @return {Object} Mutation-safe object. - */ -function getMutateSafeObject( original, working ) { - if ( original === working ) { - return { ...original }; - } - - return working; -} - /** * Returns true if the two object arguments have the same keys, or false * otherwise. @@ -263,33 +251,19 @@ export const reusableBlocks = combineReducers( { data( state = {}, action ) { switch ( action.type ) { case 'RECEIVE_REUSABLE_BLOCKS': { - return reduce( action.results, ( nextState, result ) => { - const { id, title } = result.reusableBlock; - const { clientId } = result.parsedBlock; - - const value = { clientId, title }; - - if ( ! isEqual( nextState[ id ], value ) ) { - nextState = getMutateSafeObject( state, nextState ); - nextState[ id ] = value; - } - - return nextState; - }, state ); + return { + ...state, + ...keyBy( action.results, 'id' ), + }; } - case 'UPDATE_REUSABLE_BLOCK_TITLE': { - const { id, title } = action; - - if ( ! state[ id ] || state[ id ].title === title ) { - return state; - } - + case 'UPDATE_REUSABLE_BLOCK': { + const { id, changes } = action; return { ...state, [ id ]: { ...state[ id ], - title, + ...changes, }, }; } @@ -305,7 +279,10 @@ export const reusableBlocks = combineReducers( { const value = state[ id ]; return { ...omit( state, id ), - [ updatedId ]: value, + [ updatedId ]: { + ...value, + id: updatedId, + }, }; } diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 156ce5b9b3602..56428d44855b9 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -207,19 +207,14 @@ describe( 'state', () => { const state = reusableBlocks( {}, { type: 'RECEIVE_REUSABLE_BLOCKS', results: [ { - reusableBlock: { - id: 123, - title: 'My cool block', - }, - parsedBlock: { - clientId: 'foo', - }, + id: 123, + title: 'My cool block', } ], } ); expect( state ).toEqual( { data: { - 123: { clientId: 'foo', title: 'My cool block' }, + 123: { id: 123, title: 'My cool block' }, }, isFetching: {}, isSaving: {}, @@ -236,9 +231,11 @@ describe( 'state', () => { }; const state = reusableBlocks( initialState, { - type: 'UPDATE_REUSABLE_BLOCK_TITLE', + type: 'UPDATE_REUSABLE_BLOCK', id: 123, - title: 'My block', + changes: { + title: 'My block', + }, } ); expect( state ).toEqual( { @@ -253,7 +250,7 @@ describe( 'state', () => { it( "should update the reusable block's id if it was temporary", () => { const initialState = { data: { - reusable1: { clientId: '', title: '' }, + reusable1: { id: 'reusable1', title: '' }, }, isSaving: {}, }; @@ -266,7 +263,7 @@ describe( 'state', () => { expect( state ).toEqual( { data: { - 123: { clientId: '', title: '' }, + 123: { id: 123, title: '' }, }, isFetching: {}, isSaving: {},