From e437939903921c0d38aee1cad87855c24bc3af5f Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Sat, 9 Dec 2017 02:26:23 +1100 Subject: [PATCH] Reusable Blocks: Add reusable blocks UI (#3378) * Add UI for editing a reusable block Adds the UI which displays when a reusable block is asked for its `edit` component. This includes a panel which includes UI that indicates whether a reusable block is loading and lets a user change the name of the block. * Allow a reusable block to be rendered as HTML Adds the necessary PHP code for rendering a reusable block as HTML on the blog's front-end. * Present reusable blocks with a dashed outline Changes BlockListBlock to display selected reusable blocks with a dashed outline. * Fix reusable blocks not loading Fix reusable blocks not loading by looking at the correct path in the state tree (state.reusableBlocks.data) and by adding a trailing slash to the /gutenberg/v1/ API versionString. * Add UI for inserting a reusable block Patches the Inserter to display all reusable blocks in a seperate group, and to insert a reusable block when one is clicked. * Add UI for converting blocks to/from a reusable block Add a block settings menu control which allows users to convert a regular block to a reusable block and convert a reusable block to a regular block. * Resolve ambiguity around `onSelect( attributes )` Distinguish between initialAttributes which are passed into createBlock and attributes which define which attributes a block type supports. * Change copy of 'Convert to Reusable Block' * Mark gutenberg_render_block as being available @since 1.9.0 * Fix inability to search for reusable blocks Make the Inserter search both static blocks and reusable blocks. * Display reusable blocks in a seperate Inserter tab Moves reusbale blocks from the Blocks tab to a new Saved tab in the Inserter. * Reusable Blocks: Fix reusable blocks classname * Rename core/reusable-block to core/block This way, just as inserts an image, inserts a wp_block. * Fix Reusable Block saving state Ensure that the Save button indicates that the block is being saved. * DRY up block.name === 'core/block' logic Move block.name === 'core/block' logic to a new isReusableBlock function. * Reusable Blocks: Avoiding using a generated uid and rely on the post_id instead --- blocks/api/factory.js | 3 +- blocks/api/index.js | 1 + blocks/api/registration.js | 12 ++ blocks/api/test/registration.js | 13 ++ blocks/library/block/edit-panel/index.js | 68 +++++++ blocks/library/block/edit-panel/style.scss | 32 ++++ blocks/library/block/index.js | 172 ++++++++++++++++++ blocks/library/block/index.php | 39 ++++ blocks/library/index.js | 2 +- blocks/library/reusable-block/index.js | 24 --- blocks/test/fixtures/core__block.html | 1 + ...__reusable-block.json => core__block.json} | 2 +- ...ck.parsed.json => core__block.parsed.json} | 2 +- .../test/fixtures/core__block.serialized.html | 1 + .../test/fixtures/core__reusable-block.html | 1 - .../core__reusable-block.serialized.html | 1 - editor/components/block-list/block.js | 10 +- editor/components/block-list/style.scss | 5 + .../components/block-settings-menu/index.js | 2 + .../reusable-block-toggle.js | 49 +++++ .../test/reusable-block-toggle.js | 29 +++ editor/components/inserter/group.js | 2 +- editor/components/inserter/index.js | 7 +- editor/components/inserter/menu.js | 52 +++++- editor/components/inserter/test/menu.js | 18 ++ editor/effects.js | 16 +- editor/reducer.js | 39 ++++ editor/test/effects.js | 14 +- editor/test/reducer.js | 78 +++++++- lib/blocks.php | 47 +++-- ...ass-wp-rest-reusable-blocks-controller.php | 79 ++------ lib/client-assets.php | 2 +- ...s-rest-reusable-blocks-controller-test.php | 45 ++--- 33 files changed, 698 insertions(+), 170 deletions(-) create mode 100644 blocks/library/block/edit-panel/index.js create mode 100644 blocks/library/block/edit-panel/style.scss create mode 100644 blocks/library/block/index.js create mode 100644 blocks/library/block/index.php delete mode 100644 blocks/library/reusable-block/index.js create mode 100644 blocks/test/fixtures/core__block.html rename blocks/test/fixtures/{core__reusable-block.json => core__block.json} (82%) rename blocks/test/fixtures/{core__reusable-block.parsed.json => core__block.parsed.json} (83%) create mode 100644 blocks/test/fixtures/core__block.serialized.html delete mode 100644 blocks/test/fixtures/core__reusable-block.html delete mode 100644 blocks/test/fixtures/core__reusable-block.serialized.html create mode 100644 editor/components/block-settings-menu/reusable-block-toggle.js create mode 100644 editor/components/block-settings-menu/test/reusable-block-toggle.js diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 310fcae6c0708..eb72d1c040399 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -142,7 +142,8 @@ export function switchToBlockType( blocks, name ) { */ export function createReusableBlock( type, attributes ) { return { - id: uuid(), + id: uuid(), // Temorary id replaced when the block is saved server side + isTemporary: true, name: __( 'Untitled block' ), type, attributes, diff --git a/blocks/api/index.js b/blocks/api/index.js index 86bbf662d9c23..0cf3c8c1c4108 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -14,5 +14,6 @@ export { getBlockType, getBlockTypes, hasBlockSupport, + isReusableBlock, } from './registration'; diff --git a/blocks/api/registration.js b/blocks/api/registration.js index c123a7ea4de1d..6d44f13c83dd8 100644 --- a/blocks/api/registration.js +++ b/blocks/api/registration.js @@ -214,3 +214,15 @@ export function hasBlockSupport( nameOrType, feature, defaultSupports ) { feature, ], defaultSupports ); } + +/** + * Determines whether or not the given block is a reusable block. This is a + * special block type that is used to point to a global block stored via the + * API. + * + * @param {Object} blockOrType Block or Block Type to test + * @return {Boolean} Whether the given block is a reusable block + */ +export function isReusableBlock( blockOrType ) { + return blockOrType.name === 'core/block'; +} diff --git a/blocks/api/test/registration.js b/blocks/api/test/registration.js index da09563a400f4..af724294a06e2 100644 --- a/blocks/api/test/registration.js +++ b/blocks/api/test/registration.js @@ -18,6 +18,7 @@ import { getBlockType, getBlockTypes, hasBlockSupport, + isReusableBlock, } from '../registration'; describe( 'blocks', () => { @@ -412,4 +413,16 @@ describe( 'blocks', () => { expect( hasBlockSupport( settings, 'foo' ) ).toBe( true ); } ); } ); + + describe( 'isReusableBlock', () => { + it( 'should return true for a reusable block', () => { + const block = { name: 'core/block' }; + expect( isReusableBlock( block ) ).toBe( true ); + } ); + + it( 'should return false for other blocks', () => { + const block = { name: 'core/paragraph' }; + expect( isReusableBlock( block ) ).toBe( false ); + } ); + } ); } ); diff --git a/blocks/library/block/edit-panel/index.js b/blocks/library/block/edit-panel/index.js new file mode 100644 index 0000000000000..793ef1f932ca0 --- /dev/null +++ b/blocks/library/block/edit-panel/index.js @@ -0,0 +1,68 @@ +/** + * WordPress dependencies + */ +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import './style.scss'; + +function ReusableBlockEditPanel( props ) { + const { isEditing, name, isSaving, onEdit, onDetach, onChangeName, onSave, onCancel } = props; + + return ( +
+ { ! isEditing && ! isSaving && [ + + { name } + , + , + , + ] } + { ( isEditing || isSaving ) && [ + onChangeName( event.target.value ) } />, + , + , + ] } +
+ ); +} + +export default ReusableBlockEditPanel; + diff --git a/blocks/library/block/edit-panel/style.scss b/blocks/library/block/edit-panel/style.scss new file mode 100644 index 0000000000000..4baef7e219d37 --- /dev/null +++ b/blocks/library/block/edit-panel/style.scss @@ -0,0 +1,32 @@ +.reusable-block-edit-panel { + align-items: center; + background: $light-gray-100; + color: $dark-gray-500; + display: flex; + font-family: $default-font; + font-size: $default-font-size; + justify-content: flex-end; + margin: $block-padding (-$block-padding) (-$block-padding); + padding: 10px $block-padding; + + .reusable-block-edit-panel__spinner { + margin: 0 5px; + } + + .reusable-block-edit-panel__info { + margin-right: auto; + } + + .reusable-block-edit-panel__name { + flex-grow: 1; + font-size: 14px; + height: 30px; + margin: 0 auto 0 0; + max-width: 230px; + } + + // Needs specificity to override the margin-bottom set by .button + .wp-core-ui & .reusable-block-edit-panel__button { + margin: 0 0 0 5px; + } +} diff --git a/blocks/library/block/index.js b/blocks/library/block/index.js new file mode 100644 index 0000000000000..2a6bb8dce4667 --- /dev/null +++ b/blocks/library/block/index.js @@ -0,0 +1,172 @@ +/** + * External dependencies + */ +import { pickBy, noop } from 'lodash'; +import { connect } from 'react-redux'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { Placeholder, Spinner } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { getBlockType, registerBlockType, hasBlockSupport, getBlockDefaultClassname } from '../../api'; +import ReusableBlockEditPanel from './edit-panel'; + +class ReusableBlockEdit extends Component { + constructor() { + super( ...arguments ); + + this.startEditing = this.startEditing.bind( this ); + this.stopEditing = this.stopEditing.bind( this ); + this.setAttributes = this.setAttributes.bind( this ); + this.setName = this.setName.bind( this ); + this.updateReusableBlock = this.updateReusableBlock.bind( this ); + + this.state = { + isEditing: false, + name: null, + attributes: null, + }; + } + + componentDidMount() { + if ( ! this.props.reusableBlock ) { + this.props.fetchReusableBlock(); + } + } + + startEditing() { + this.setState( { isEditing: true } ); + } + + stopEditing() { + this.setState( { + isEditing: false, + name: null, + attributes: null, + } ); + } + + setAttributes( attributes ) { + this.setState( ( prevState ) => ( { + attributes: { ...prevState.attributes, ...attributes }, + } ) ); + } + + setName( name ) { + this.setState( { name } ); + } + + updateReusableBlock() { + const { name, attributes } = this.state; + + // Use pickBy to include only changed (assigned) values in payload + const payload = pickBy( { + name, + attributes, + } ); + + this.props.updateReusableBlock( payload ); + this.props.saveReusableBlock(); + this.stopEditing(); + } + + render() { + const { focus, reusableBlock, isSaving, convertBlockToStatic } = this.props; + const { isEditing, name, attributes } = this.state; + + if ( ! reusableBlock ) { + return ; + } + + const reusableBlockAttributes = { ...reusableBlock.attributes, ...attributes }; + const blockType = getBlockType( reusableBlock.type ); + const BlockEdit = blockType.edit || blockType.save; + + // Generate a class name for the block's editable form + const generatedClassName = hasBlockSupport( blockType, 'className', true ) ? + getBlockDefaultClassname( reusableBlock.type ) : + null; + const className = classnames( generatedClassName, reusableBlockAttributes.className ); + return [ + // We fake the block being read-only by wrapping it with an element that has pointer-events: none +
+ +
, + focus && ( + + ), + ]; + } +} + +const ConnectedReusableBlockEdit = connect( + ( state, ownProps ) => ( { + reusableBlock: state.reusableBlocks.data[ ownProps.attributes.ref ], + isSaving: state.reusableBlocks.isSaving[ ownProps.attributes.ref ], + } ), + ( dispatch, ownProps ) => ( { + fetchReusableBlock() { + dispatch( { + type: 'FETCH_REUSABLE_BLOCKS', + id: ownProps.attributes.ref, + } ); + }, + updateReusableBlock( reusableBlock ) { + dispatch( { + type: 'UPDATE_REUSABLE_BLOCK', + id: ownProps.attributes.ref, + reusableBlock, + } ); + }, + saveReusableBlock() { + dispatch( { + type: 'SAVE_REUSABLE_BLOCK', + id: ownProps.attributes.ref, + } ); + }, + convertBlockToStatic() { + dispatch( { + type: 'CONVERT_BLOCK_TO_STATIC', + uid: ownProps.id, + } ); + }, + } ) +)( ReusableBlockEdit ); + +registerBlockType( 'core/block', { + title: __( 'Reusable Block' ), + category: 'reusable-blocks', + isPrivate: true, + + attributes: { + ref: { + type: 'string', + }, + }, + + edit: ConnectedReusableBlockEdit, + save: () => null, +} ); diff --git a/blocks/library/block/index.php b/blocks/library/block/index.php new file mode 100644 index 0000000000000..ecafb9d2d2146 --- /dev/null +++ b/blocks/library/block/index.php @@ -0,0 +1,39 @@ +post_content ); + + $block = array_shift( $blocks ); + if ( ! $block ) { + return ''; + } + + return gutenberg_render_block( $block ); +} + +register_block_type( 'core/block', array( + 'attributes' => array( + 'ref' => array( + 'type' => 'string', + ), + ), + + 'render_callback' => 'gutenberg_render_block_core_reusable_block', +) ); diff --git a/blocks/library/index.js b/blocks/library/index.js index d12af4f1efafe..2076db1f9dc5a 100644 --- a/blocks/library/index.js +++ b/blocks/library/index.js @@ -21,5 +21,5 @@ import './text-columns'; import './verse'; import './video'; import './audio'; -import './reusable-block'; +import './block'; import './paragraph'; diff --git a/blocks/library/reusable-block/index.js b/blocks/library/reusable-block/index.js deleted file mode 100644 index 800a5cc4cd3b2..0000000000000 --- a/blocks/library/reusable-block/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { registerBlockType } from '../../api'; - -registerBlockType( 'core/reusable-block', { - title: __( 'Reusable Block' ), - category: 'reusable-blocks', - isPrivate: true, - - attributes: { - ref: { - type: 'string', - }, - }, - - edit: () =>
{ __( 'Reusable Blocks are coming soon!' ) }
, - save: () => null, -} ); diff --git a/blocks/test/fixtures/core__block.html b/blocks/test/fixtures/core__block.html new file mode 100644 index 0000000000000..a0693a60e7f95 --- /dev/null +++ b/blocks/test/fixtures/core__block.html @@ -0,0 +1 @@ + diff --git a/blocks/test/fixtures/core__reusable-block.json b/blocks/test/fixtures/core__block.json similarity index 82% rename from blocks/test/fixtures/core__reusable-block.json rename to blocks/test/fixtures/core__block.json index 366a117a9e5f1..e27c8fe772b1b 100644 --- a/blocks/test/fixtures/core__reusable-block.json +++ b/blocks/test/fixtures/core__block.json @@ -1,7 +1,7 @@ [ { "uid": "_uid_0", - "name": "core/reusable-block", + "name": "core/block", "isValid": true, "attributes": { "ref": "358b59ee-bab3-4d6f-8445-e8c6971a5605" diff --git a/blocks/test/fixtures/core__reusable-block.parsed.json b/blocks/test/fixtures/core__block.parsed.json similarity index 83% rename from blocks/test/fixtures/core__reusable-block.parsed.json rename to blocks/test/fixtures/core__block.parsed.json index aaaac11724462..4644ce602201b 100644 --- a/blocks/test/fixtures/core__reusable-block.parsed.json +++ b/blocks/test/fixtures/core__block.parsed.json @@ -1,6 +1,6 @@ [ { - "blockName": "core/reusable-block", + "blockName": "core/block", "attrs": { "ref": "358b59ee-bab3-4d6f-8445-e8c6971a5605" }, diff --git a/blocks/test/fixtures/core__block.serialized.html b/blocks/test/fixtures/core__block.serialized.html new file mode 100644 index 0000000000000..db861d912a356 --- /dev/null +++ b/blocks/test/fixtures/core__block.serialized.html @@ -0,0 +1 @@ + diff --git a/blocks/test/fixtures/core__reusable-block.html b/blocks/test/fixtures/core__reusable-block.html deleted file mode 100644 index 1d4d5f0c09b92..0000000000000 --- a/blocks/test/fixtures/core__reusable-block.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/blocks/test/fixtures/core__reusable-block.serialized.html b/blocks/test/fixtures/core__reusable-block.serialized.html deleted file mode 100644 index 2b3a42824ef3e..0000000000000 --- a/blocks/test/fixtures/core__reusable-block.serialized.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js index 429a79440c563..4d1e4b550dbc0 100644 --- a/editor/components/block-list/block.js +++ b/editor/components/block-list/block.js @@ -10,7 +10,14 @@ import { get, partial, reduce, size } from 'lodash'; */ import { Component, compose, createElement } from '@wordpress/element'; import { keycodes } from '@wordpress/utils'; -import { getBlockType, BlockEdit, getBlockDefaultClassname, createBlock, hasBlockSupport } from '@wordpress/blocks'; +import { + getBlockType, + BlockEdit, + getBlockDefaultClassname, + createBlock, + hasBlockSupport, + isReusableBlock, +} from '@wordpress/blocks'; import { withFilters, withContext } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -359,6 +366,7 @@ class BlockListBlock extends Component { 'is-selected': showUI, 'is-multi-selected': isMultiSelected, 'is-hovered': isHovered, + 'is-reusable': isReusableBlock( blockType ), } ); const { onMouseLeave, onFocus, onReplace } = this.props; diff --git a/editor/components/block-list/style.scss b/editor/components/block-list/style.scss index e93e79581a534..d73257f2240ee 100644 --- a/editor/components/block-list/style.scss +++ b/editor/components/block-list/style.scss @@ -72,6 +72,11 @@ outline: 1px solid $light-gray-500; } + // give reusable blocks a dashed outline + &.is-reusable.is-selected:before { + outline: 1px dashed $light-gray-500; + } + // selection style for textarea ::-moz-selection { background: $blue-medium-highlight; diff --git a/editor/components/block-settings-menu/index.js b/editor/components/block-settings-menu/index.js index c8d3b5a72c337..cdeff64d345b7 100644 --- a/editor/components/block-settings-menu/index.js +++ b/editor/components/block-settings-menu/index.js @@ -17,6 +17,7 @@ import './style.scss'; import BlockInspectorButton from './block-inspector-button'; import BlockModeToggle from './block-mode-toggle'; import BlockDeleteButton from './block-delete-button'; +import ReusableBlockToggle from './reusable-block-toggle'; import UnknownConverter from './unknown-converter'; import { selectBlock } from '../../actions'; @@ -56,6 +57,7 @@ function BlockSettingsMenu( { uids, onSelect, focus } ) { { count === 1 && } { count === 1 && } + { count === 1 && } ) } /> diff --git a/editor/components/block-settings-menu/reusable-block-toggle.js b/editor/components/block-settings-menu/reusable-block-toggle.js new file mode 100644 index 0000000000000..aa007031eca06 --- /dev/null +++ b/editor/components/block-settings-menu/reusable-block-toggle.js @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; +import { isReusableBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { getBlock } from '../../selectors'; +import { convertBlockToStatic, convertBlockToReusable } from '../../actions'; + +export function ReusableBlockToggle( { isReusable, convertToStatic, convertToReusable } ) { + return ( + + { isReusable ? __( 'Detach from Reusable Block' ) : __( 'Convert to Reusable Block' ) } + + ); +} + +export default connect( + ( state, { uid } ) => { + const block = getBlock( state, uid ); + return { + isReusable: isReusableBlock( block ), + }; + }, + ( dispatch, { uid, onToggle = noop } ) => ( { + convertToStatic() { + dispatch( convertBlockToStatic( uid ) ); + onToggle(); + }, + convertToReusable() { + dispatch( convertBlockToReusable( uid ) ); + onToggle(); + }, + } ) +)( ReusableBlockToggle ); diff --git a/editor/components/block-settings-menu/test/reusable-block-toggle.js b/editor/components/block-settings-menu/test/reusable-block-toggle.js new file mode 100644 index 0000000000000..4c6258d49f194 --- /dev/null +++ b/editor/components/block-settings-menu/test/reusable-block-toggle.js @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { ReusableBlockToggle } from '../reusable-block-toggle'; + +describe( 'ReusableBlockToggle', () => { + it( 'should allow converting a reusable block to static', () => { + const wrapper = shallow( + + ); + const text = wrapper.find( 'IconButton' ).first().prop( 'children' ); + + expect( text ).toEqual( 'Detach from Reusable Block' ); + } ); + + it( 'should allow converting a static block to reusable', () => { + const wrapper = shallow( + + ); + const text = wrapper.find( 'IconButton' ).first().prop( 'children' ); + + expect( text ).toEqual( 'Convert to Reusable Block' ); + } ); +} ); diff --git a/editor/components/inserter/group.js b/editor/components/inserter/group.js index 50939aa1c61ed..c32f801338f1b 100644 --- a/editor/components/inserter/group.js +++ b/editor/components/inserter/group.js @@ -48,7 +48,7 @@ export default class InserterGroup extends Component { role="menuitem" key={ block.name } className="editor-inserter__block" - onClick={ selectBlock( block.name ) } + onClick={ selectBlock( block ) } ref={ bindReferenceNode( block.name ) } tabIndex={ current === block.name || disabled ? null : '-1' } onMouseEnter={ ! disabled ? this.props.showInsertionPoint : null } diff --git a/editor/components/inserter/index.js b/editor/components/inserter/index.js index 4976b4ed03ad0..c00d83388c0c8 100644 --- a/editor/components/inserter/index.js +++ b/editor/components/inserter/index.js @@ -89,9 +89,10 @@ class Inserter extends Component { ) } renderContent={ ( { onClose } ) => { - const onInsert = ( name ) => { + const onInsert = ( name, initialAttributes ) => { onInsertBlock( name, + initialAttributes, insertionPoint ); @@ -114,10 +115,10 @@ export default flowRight( [ }; }, ( dispatch ) => ( { - onInsertBlock( name, position ) { + onInsertBlock( name, initialAttributes, position ) { dispatch( hideInsertionPoint() ); dispatch( insertBlock( - createBlock( name ), + createBlock( name, initialAttributes ), position ) ); }, diff --git a/editor/components/inserter/menu.js b/editor/components/inserter/menu.js index 4c2f49f3c4cef..30f55b047cbdf 100644 --- a/editor/components/inserter/menu.js +++ b/editor/components/inserter/menu.js @@ -34,8 +34,8 @@ import { keycodes } from '@wordpress/utils'; */ import './style.scss'; -import { getBlocks, getRecentlyUsedBlocks } from '../../selectors'; -import { showInsertionPoint, hideInsertionPoint } from '../../actions'; +import { getBlocks, getRecentlyUsedBlocks, getReusableBlocks } from '../../selectors'; +import { showInsertionPoint, hideInsertionPoint, fetchReusableBlocks } from '../../actions'; import { default as InserterGroup } from './group'; export const searchBlocks = ( blocks, searchTerm ) => { @@ -71,6 +71,10 @@ export class InserterMenu extends Component { this.switchTab = this.switchTab.bind( this ); } + componentDidMount() { + this.props.fetchReusableBlocks(); + } + componentDidUpdate( prevProps, prevState ) { const searchResults = this.searchBlocks( this.getBlockTypes() ); // Announce the blocks search results to screen readers. @@ -103,16 +107,16 @@ export class InserterMenu extends Component { } ); } - selectBlock( name ) { + selectBlock( block ) { return () => { - this.props.onSelect( name ); + this.props.onSelect( block.name, block.initialAttributes ); this.setState( { filterValue: '', } ); }; } - getBlockTypes() { + getStaticBlockTypes() { const { blockTypes } = this.props; // If all block types disabled, return empty set @@ -136,6 +140,28 @@ export class InserterMenu extends Component { } ); } + getReusableBlockTypes() { + const { reusableBlocks } = this.props; + + // Display reusable blocks that we've fetched in the inserter + return reusableBlocks.map( ( reusableBlock ) => ( { + name: 'core/block', + initialAttributes: { + ref: reusableBlock.id, + }, + title: reusableBlock.name, + icon: 'layout', + category: 'reusable-blocks', + } ) ); + } + + getBlockTypes() { + return [ + ...this.getStaticBlockTypes(), + ...this.getReusableBlockTypes(), + ]; + } + searchBlocks( blockTypes ) { return searchBlocks( blockTypes, this.state.filterValue ); } @@ -154,12 +180,16 @@ export class InserterMenu extends Component { ( { name } ) => find( blockTypes, { name } ) ); case 'blocks': - predicate = ( block ) => block.category !== 'embed'; + predicate = ( block ) => block.category !== 'embed' && block.category !== 'reusable-blocks'; break; case 'embeds': predicate = ( block ) => block.category === 'embed'; break; + + case 'saved': + predicate = ( block ) => block.category === 'reusable-blocks'; + break; } return filter( blockTypes, predicate ); @@ -300,6 +330,11 @@ export class InserterMenu extends Component { title: __( 'Embeds' ), className: 'editor-inserter__tab', }, + { + name: 'saved', + title: __( 'Saved' ), + className: 'editor-inserter__tab', + }, ] } > { ( tabKey ) => ( @@ -311,7 +346,7 @@ export class InserterMenu extends Component { } { isSearching &&
- { this.renderCategories( this.getVisibleBlocksByCategory( getBlockTypes() ) ) } + { this.renderCategories( this.getVisibleBlocksByCategory( this.getBlockTypes() ) ) }
} @@ -324,9 +359,10 @@ const connectComponent = connect( return { recentlyUsedBlocks: getRecentlyUsedBlocks( state ), blocks: getBlocks( state ), + reusableBlocks: getReusableBlocks( state ), }; }, - { showInsertionPoint, hideInsertionPoint } + { showInsertionPoint, hideInsertionPoint, fetchReusableBlocks } ); export default flow( diff --git a/editor/components/inserter/test/menu.js b/editor/components/inserter/test/menu.js index 2e2132f8f1ed1..77c47aa21a1c9 100644 --- a/editor/components/inserter/test/menu.js +++ b/editor/components/inserter/test/menu.js @@ -95,8 +95,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes /> ); @@ -114,8 +116,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [ advancedTextBlock ] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes={ false } /> ); @@ -130,8 +134,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [ textBlock, advancedTextBlock ] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes={ [ textBlock.name ] } /> ); @@ -147,6 +153,7 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [ // Actually recently used by user, thus present at the top. advancedTextBlock, @@ -157,6 +164,7 @@ describe( 'InserterMenu', () => { someOtherBlock, ] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes /> ); @@ -173,8 +181,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes /> ); @@ -197,8 +207,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes /> ); @@ -223,8 +235,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [ { name: moreBlock.name } ] } + reusableBlocks={ [] } recentlyUsedBlocks={ [] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes /> ); @@ -244,8 +258,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes /> ); @@ -267,8 +283,10 @@ describe( 'InserterMenu', () => { position={ 'top center' } instanceId={ 1 } blocks={ [] } + reusableBlocks={ [] } recentlyUsedBlocks={ [] } debouncedSpeak={ noop } + fetchReusableBlocks={ noop } blockTypes /> ); diff --git a/editor/effects.js b/editor/effects.js index 8dd5000ba7746..9cc3def391708 100644 --- a/editor/effects.js +++ b/editor/effects.js @@ -346,12 +346,16 @@ export default { const { id } = action; const { getState, dispatch } = store; - const { name, type, attributes } = getReusableBlock( getState(), id ); + const { name, type, attributes, isTemporary } = getReusableBlock( getState(), id ); const content = serialize( createBlock( type, attributes ) ); - - new wp.api.models.ReusableBlocks( { id, name, content } ).save().then( - () => { - dispatch( { type: 'SAVE_REUSABLE_BLOCK_SUCCESS', id } ); + const requestData = isTemporary ? { name, content } : { id, name, content }; + new wp.api.models.ReusableBlocks( requestData ).save().then( + ( updatedReusableBlock ) => { + dispatch( { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + updatedId: updatedReusableBlock.id, + id, + } ); dispatch( createSuccessNotice( __( 'Reusable block updated' ), { id: SAVE_REUSABLE_BLOCK_NOTICE_ID } @@ -379,7 +383,7 @@ export default { const oldBlock = getBlock( getState(), action.uid ); const reusableBlock = createReusableBlock( oldBlock.name, oldBlock.attributes ); - const newBlock = createBlock( 'core/reusable-block', { ref: reusableBlock.id } ); + const newBlock = createBlock( 'core/block', { ref: reusableBlock.id } ); dispatch( updateReusableBlock( reusableBlock.id, reusableBlock ) ); dispatch( saveReusableBlock( reusableBlock.id ) ); dispatch( replaceBlocks( [ oldBlock.uid ], [ newBlock ] ) ); diff --git a/editor/reducer.js b/editor/reducer.js index c7597db556b34..8f62d685fe5b7 100644 --- a/editor/reducer.js +++ b/editor/reducer.js @@ -203,6 +203,29 @@ export const editor = flow( [ case 'REMOVE_BLOCKS': return omit( state, action.uids ); + + case 'SAVE_REUSABLE_BLOCK_SUCCESS': { + const { id, updatedId } = action; + + // If a temporary reusable block is saved, we swap the temporary id with the final one + if ( id === updatedId ) { + return state; + } + + return mapValues( state, ( block ) => { + if ( block.name === 'core/block' && block.attributes.ref === id ) { + return { + ...block, + attributes: { + ...block.attributes, + ref: updatedId, + }, + }; + } + + return block; + } ); + } } return state; @@ -731,6 +754,22 @@ export const reusableBlocks = combineReducers( { }, }; } + + case 'SAVE_REUSABLE_BLOCK_SUCCESS': { + const { id, updatedId } = action; + + // If a temporary reusable block is saved, we swap the temporary id with the final one + if ( id === updatedId ) { + return state; + } + return { + ...omit( state, id ), + [ updatedId ]: { + ...omit( state[ id ], [ 'id', 'isTemporary' ] ), + id: updatedId, + }, + }; + } } return state; diff --git a/editor/test/effects.js b/editor/test/effects.js index bf2ab9ce4fb5e..b72cdced692b5 100644 --- a/editor/test/effects.js +++ b/editor/test/effects.js @@ -402,7 +402,7 @@ describe( 'effects', () => { name: { type: 'string' }, }, } ); - registerBlockType( 'core/reusable-block', { + registerBlockType( 'core/block', { title: 'Reusable Block', category: 'common', save: () => null, @@ -414,7 +414,7 @@ describe( 'effects', () => { afterAll( () => { unregisterBlockType( 'core/test-block' ); - unregisterBlockType( 'core/reusable-block' ); + unregisterBlockType( 'core/block' ); } ); describe( '.FETCH_REUSABLE_BLOCKS', () => { @@ -529,9 +529,9 @@ describe( 'effects', () => { describe( '.SAVE_REUSABLE_BLOCK', () => { const handler = effects.SAVE_REUSABLE_BLOCK; - it( 'should save a reusable block', () => { + it( 'should save a reusable block and swaps its id', () => { let modelAttributes; - const promise = Promise.resolve(); + const promise = Promise.resolve( { id: 3 } ); set( global, 'wp.api.models.ReusableBlocks', class { constructor( attributes ) { @@ -570,6 +570,7 @@ describe( 'effects', () => { expect( dispatch ).toHaveBeenCalledWith( { type: 'SAVE_REUSABLE_BLOCK_SUCCESS', id: reusableBlock.id, + updatedId: 3, } ); } ); } ); @@ -624,7 +625,7 @@ describe( 'effects', () => { }; const staticBlock = { uid: 'd6b55aa9-16b5-4123-9675-749d75a7f14d', - name: 'core/reusable-block', + name: 'core/block', attributes: { ref: reusableBlock.id, }, @@ -674,6 +675,7 @@ describe( 'effects', () => { expect( dispatch ).toHaveBeenCalledWith( updateReusableBlock( 'this-is-a-mock-uuid', { id: 'this-is-a-mock-uuid', + isTemporary: true, name: 'Untitled block', type: staticBlock.name, attributes: staticBlock.attributes, @@ -685,7 +687,7 @@ describe( 'effects', () => { expect( dispatch ).toHaveBeenCalledWith( replaceBlocks( [ staticBlock.uid ], - [ createBlock( 'core/reusable-block', { ref: 'this-is-a-mock-uuid' } ) ] + [ createBlock( 'core/block', { ref: 'this-is-a-mock-uuid' } ) ] ) ); } ); diff --git a/editor/test/reducer.js b/editor/test/reducer.js index fdf0875f53d09..08426a96d4d4f 100644 --- a/editor/test/reducer.js +++ b/editor/test/reducer.js @@ -153,6 +153,35 @@ describe( 'state', () => { } ); } ); + it( 'should update the reusable block reference if the temporary id is swapped', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/block', + attributes: { + ref: 'random-uid', + }, + isValid: false, + } ], + } ); + + const state = editor( deepFreeze( original ), { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + id: 'random-uid', + updatedId: 3, + } ); + + expect( state.present.blocksByUid.chicken ).toEqual( { + uid: 'chicken', + name: 'core/block', + attributes: { + ref: 3, + }, + isValid: false, + } ); + } ); + it( 'should move the block up', () => { const original = editor( undefined, { type: 'RESET_BLOCKS', @@ -1384,6 +1413,46 @@ describe( 'state', () => { } ); } ); + it( 'should update the reusable block\'s id if it was temporary', () => { + const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; + const initialState = { + data: { + [ id ]: { + id, + isTemporary: true, + name: 'My cool block', + type: 'core/paragraph', + attributes: { + content: 'Hello!', + dropCap: true, + }, + }, + }, + isSaving: {}, + }; + + const state = reusableBlocks( initialState, { + type: 'SAVE_REUSABLE_BLOCK_SUCCESS', + id, + updatedId: 3, + } ); + + expect( state ).toEqual( { + data: { + 3: { + id: 3, + name: 'My cool block', + type: 'core/paragraph', + attributes: { + content: 'Hello!', + dropCap: true, + }, + }, + }, + isSaving: {}, + } ); + } ); + it( 'should indicate that a reusable block is saving', () => { const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; const initialState = { @@ -1407,7 +1476,9 @@ describe( 'state', () => { it( 'should stop indicating that a reusable block is saving when the save succeeded', () => { const id = '358b59ee-bab3-4d6f-8445-e8c6971a5605'; const initialState = { - data: {}, + data: { + [ id ]: { id }, + }, isSaving: { [ id ]: true, }, @@ -1416,10 +1487,13 @@ describe( 'state', () => { const state = reusableBlocks( initialState, { type: 'SAVE_REUSABLE_BLOCK_SUCCESS', id, + updatedId: id, } ); expect( state ).toEqual( { - data: {}, + data: { + [ id ]: { id }, + }, isSaving: {}, } ); } ); diff --git a/lib/blocks.php b/lib/blocks.php index 801bbf326ee4b..d5e4091b09cd0 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -131,6 +131,33 @@ function gutenberg_serialize_block( $block ) { return $content; } +/** + * Renders a single block into a HTML string. + * + * @since 1.9.0 + * + * @param array $block A single parsed block object. + * @return string String of rendered HTML. + */ +function gutenberg_render_block( $block ) { + $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null; + $attributes = is_array( $block['attrs'] ) ? $block['attrs'] : array(); + $raw_content = isset( $block['innerHTML'] ) ? $block['innerHTML'] : null; + + if ( $block_name ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); + if ( null !== $block_type ) { + return $block_type->render( $attributes, $raw_content ); + } + } + + if ( $raw_content ) { + return $raw_content; + } + + return ''; +} + /** * Parses dynamic blocks out of `post_content` and re-renders them. * @@ -140,30 +167,12 @@ function gutenberg_serialize_block( $block ) { * @return string Updated post content. */ function do_blocks( $content ) { - $registry = WP_Block_Type_Registry::get_instance(); - $blocks = gutenberg_parse_blocks( $content ); $content_after_blocks = ''; - foreach ( $blocks as $block ) { - $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null; - $attributes = is_array( $block['attrs'] ) ? $block['attrs'] : array(); - $raw_content = isset( $block['innerHTML'] ) ? $block['innerHTML'] : null; - - if ( $block_name ) { - $block_type = $registry->get_registered( $block_name ); - if ( null !== $block_type ) { - $content_after_blocks .= $block_type->render( $attributes, $raw_content ); - continue; - } - } - - if ( $raw_content ) { - $content_after_blocks .= $raw_content; - } + $content_after_blocks .= gutenberg_render_block( $block ); } - return $content_after_blocks; } add_filter( 'the_content', 'do_blocks', 9 ); // BEFORE do_shortcode(). diff --git a/lib/class-wp-rest-reusable-blocks-controller.php b/lib/class-wp-rest-reusable-blocks-controller.php index 6d8f00a13d359..17b6d80054d73 100644 --- a/lib/class-wp-rest-reusable-blocks-controller.php +++ b/lib/class-wp-rest-reusable-blocks-controller.php @@ -43,6 +43,11 @@ public function register_routes() { 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'save_item' ), + 'permission_callback' => array( $this, 'save_item_permissions_check' ), + ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -54,8 +59,8 @@ public function register_routes() { ), array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'callback' => array( $this, 'save_item' ), + 'permission_callback' => array( $this, 'save_item_permissions_check' ), ), 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -133,14 +138,8 @@ public function get_item_permissions_check( $request ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { - $uuid = $request['id']; - if ( ! $this->is_valid_uuid4( $uuid ) ) { - return new WP_Error( 'gutenberg_reusable_block_invalid_id', __( 'ID is not a valid UUID v4.', 'gutenberg' ), array( - 'status' => 404, - ) ); - } - - $reusable_block = $this->get_reusable_block( $uuid ); + $id = $request['id']; + $reusable_block = get_post( $id ); if ( ! $reusable_block ) { return new WP_Error( 'gutenberg_reusable_block_not_found', __( 'No reusable block with that ID found.', 'gutenberg' ), array( 'status' => 404, @@ -151,7 +150,7 @@ public function get_item( $request ) { } /** - * Checks if a given request has access to update a reusable block. + * Checks if a given request has access to update/create a reusable block. * * @since 0.10.0 * @access public @@ -159,7 +158,7 @@ public function get_item( $request ) { * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. */ - public function update_item_permissions_check( $request ) { + public function save_item_permissions_check( $request ) { if ( ! current_user_can( 'edit_posts' ) ) { return new WP_Error( 'gutenberg_reusable_block_cannot_edit', __( 'Sorry, you are not allowed to edit reusable blocks as this user.', 'gutenberg' ), array( 'status' => rest_authorization_required_code(), @@ -170,7 +169,7 @@ public function update_item_permissions_check( $request ) { } /** - * Updates a single reusable block. + * Updates a single reusable block or creates a new one if no id provided. * * @since 0.10.0 * @access public @@ -178,14 +177,7 @@ public function update_item_permissions_check( $request ) { * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ - public function update_item( $request ) { - $uuid = $request['id']; - if ( ! $this->is_valid_uuid4( $uuid ) ) { - return new WP_Error( 'gutenberg_reusable_block_invalid_id', __( 'ID is not a valid UUID v4.', 'gutenberg' ), array( - 'status' => 404, - ) ); - } - + public function save_item( $request ) { $reusable_block = $this->prepare_item_for_database( $request ); if ( is_wp_error( $reusable_block ) ) { return $reusable_block; @@ -214,7 +206,7 @@ public function update_item( $request ) { protected function prepare_item_for_database( $request ) { $prepared_reusable_block = new stdClass(); - $existing_reusable_block = $this->get_reusable_block( $request['id'] ); + $existing_reusable_block = get_post( $request['id'] ); if ( $existing_reusable_block ) { $prepared_reusable_block->ID = $existing_reusable_block->ID; } @@ -222,9 +214,6 @@ protected function prepare_item_for_database( $request ) { $prepared_reusable_block->post_type = 'wp_block'; $prepared_reusable_block->post_status = 'publish'; - // ID. We already validated this in self::update_item(). - $prepared_reusable_block->post_name = $request['id']; - // Name. if ( isset( $request['name'] ) && is_string( $request['name'] ) ) { $prepared_reusable_block->post_title = $request['name']; @@ -258,7 +247,7 @@ protected function prepare_item_for_database( $request ) { */ public function prepare_item_for_response( $reusable_block, $request ) { $data = array( - 'id' => $reusable_block->post_name, + 'id' => $reusable_block->ID, 'name' => $reusable_block->post_title, 'content' => $reusable_block->post_content, ); @@ -294,7 +283,7 @@ public function get_item_schema() { 'type' => 'object', 'properties' => array( 'id' => array( - 'description' => __( 'UUID that identifies this reusable block.', 'gutenberg' ), + 'description' => __( 'ID that identifies this reusable block.', 'gutenberg' ), 'type' => 'string', 'context' => array( 'view', 'edit' ), 'readonly' => true, @@ -314,40 +303,4 @@ public function get_item_schema() { ), ); } - - /** - * Fetches a reusable block by its UUID ID. Reusable blocks are stored as posts with a custom post type. - * - * @since 0.10.0 - * @access private - * - * @param string $uuid A UUID string that uniquely identifies the reusable block. - * - * @return WP_Post|null The block (a WP_Post), or null if none was found. - */ - private function get_reusable_block( $uuid ) { - $reusable_blocks = get_posts( array( - 'post_type' => 'wp_block', - 'name' => $uuid, - ) ); - - return array_shift( $reusable_blocks ); - } - - /** - * Checks if the given value is a valid UUID v4 string. - * - * @since 0.10.0 - * @access private - * - * @param mixed $uuid The value to validate. - * @return bool Whether or not the string is a valid UUID v4 string. - */ - private function is_valid_uuid4( $uuid ) { - if ( ! is_string( $uuid ) ) { - return false; - } - - return (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid ); - } } diff --git a/lib/client-assets.php b/lib/client-assets.php index a38f7c6045d39..de8b32efbde63 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -802,7 +802,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { $script = '( function() {'; $script .= sprintf( 'var editorSettings = %s;', wp_json_encode( $editor_settings ) ); $script .= << 'wp_block', 'post_status' => 'publish', - 'post_name' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', 'post_title' => 'My cool block', 'post_content' => '

Hello!

', ) ); @@ -70,7 +69,7 @@ public function test_register_routes() { $routes = $this->server->get_routes(); $this->assertArrayHasKey( '/gutenberg/v1/reusable-blocks', $routes ); - $this->assertCount( 1, $routes['/gutenberg/v1/reusable-blocks'] ); + $this->assertCount( 2, $routes['/gutenberg/v1/reusable-blocks'] ); $this->assertArrayHasKey( '/gutenberg/v1/reusable-blocks/(?P[\w-]+)', $routes ); $this->assertCount( 2, $routes['/gutenberg/v1/reusable-blocks/(?P[\w-]+)'] ); } @@ -87,7 +86,7 @@ public function test_get_items() { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( array( array( - 'id' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', + 'id' => self::$reusable_block_post_id, 'name' => 'My cool block', 'content' => '

Hello!

', ), @@ -114,12 +113,12 @@ public function test_get_items_when_not_allowed() { public function test_get_item() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/' . self::$reusable_block_post_id ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( array( - 'id' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', + 'id' => self::$reusable_block_post_id, 'name' => 'My cool block', 'content' => '

Hello!

', ), $response->get_data() ); @@ -131,7 +130,7 @@ public function test_get_item() { public function test_get_item_when_not_allowed() { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/' . self::$reusable_block_post_id ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -139,27 +138,13 @@ public function test_get_item_when_not_allowed() { $this->assertEquals( 'gutenberg_reusable_block_cannot_read', $data['code'] ); } - /** - * Check that invalid UUIDs 404. - */ - public function test_get_item_invalid_id() { - wp_set_current_user( self::$editor_id ); - - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/invalid-uuid' ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); - - $this->assertEquals( 404, $response->get_status() ); - $this->assertEquals( 'gutenberg_reusable_block_invalid_id', $data['code'] ); - } - /** * Check that we get a 404 when we GET a non-existent reusable block. */ public function test_get_item_not_found() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/6e614ced-e80d-4e10-bd04-1e890b5f7f83' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/unknownid' ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -173,7 +158,7 @@ public function test_get_item_not_found() { public function test_update_item() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/75236553-f4ba-4f12-aa25-4ba402044bd5' ); + $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/' . self::$reusable_block_post_id ); $request->set_body_params( array( 'name' => 'Another cool block', 'content' => '
An image
', @@ -183,7 +168,7 @@ public function test_update_item() { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( array( - 'id' => '75236553-f4ba-4f12-aa25-4ba402044bd5', + 'id' => self::$reusable_block_post_id, 'name' => 'Another cool block', 'content' => '
An image
', ), $response->get_data() ); @@ -192,10 +177,10 @@ public function test_update_item() { /** * Check that users without permission can't PUT a single reusable block. */ - public function test_update_item_when_not_allowed() { + public function test_save_item_when_not_allowed() { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); + $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/' . self::$reusable_block_post_id ); $response = $this->server->dispatch( $request ); $data = $response->get_data(); @@ -204,11 +189,11 @@ public function test_update_item_when_not_allowed() { } /** - * Test cases for test_update_item_with_invalid_fields(). + * Test cases for test_save_item_with_invalid_fields(). * * @return array */ - public function data_update_item_with_invalid_fields() { + public function data_save_item_with_invalid_fields() { return array( array( array(), @@ -239,12 +224,12 @@ public function data_update_item_with_invalid_fields() { /** * Check that attributes are validated correctly when we PUT a single reusable block. * - * @dataProvider data_update_item_with_invalid_fields + * @dataProvider data_save_item_with_invalid_fields */ - public function test_update_item_with_invalid_fields( $body_params, $expected_message ) { + public function test_save_item_with_invalid_fields( $body_params, $expected_message ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/75236553-f4ba-4f12-aa25-4ba402044bd5' ); + $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/' . self::$reusable_block_post_id ); $request->set_body_params( $body_params ); $response = $this->server->dispatch( $request );