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: {},