Skip to content

Commit

Permalink
Refactor Reusable Blocks controller to use WP_REST_Posts_Controller
Browse files Browse the repository at this point in the history
We can remove a lot of code by subclassing WP_REST_Posts_Controller,
which gives us methods for creating, reading, updating and deleting
posts (in this case, blocks) for free.

This also adds support for DELETEing a block to the API, and renames the
'name' field to 'title'.
  • Loading branch information
noisysocks committed Dec 22, 2017
1 parent 5aabd70 commit 2bc1329
Show file tree
Hide file tree
Showing 15 changed files with 373 additions and 631 deletions.
2 changes: 1 addition & 1 deletion blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export function createReusableBlock( type, attributes ) {
return {
id: +uniqueId(), // Temorary id replaced when the block is saved server side
isTemporary: true,
name: __( 'Untitled block' ),
title: __( 'Untitled block' ),
type,
attributes,
};
Expand Down
2 changes: 1 addition & 1 deletion blocks/api/test/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ describe( 'block factory', () => {

expect( createReusableBlock( type, attributes ) ).toMatchObject( {
id: expect.any( Number ),
name: 'Untitled block',
title: 'Untitled block',
type,
attributes,
} );
Expand Down
14 changes: 7 additions & 7 deletions blocks/library/block/edit-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { __ } from '@wordpress/i18n';
import './style.scss';

function ReusableBlockEditPanel( props ) {
const { isEditing, name, isSaving, onEdit, onDetach, onChangeName, onSave, onCancel } = props;
const { isEditing, title, isSaving, onEdit, onDetach, onChangeTitle, onSave, onCancel } = props;

return (
<div className="reusable-block-edit-panel">
{ ! isEditing && ! isSaving && [
<span key="info" className="reusable-block-edit-panel__info">
<b>{ name }</b>
<b>{ title }</b>
</span>,
<Button
key="edit"
Expand All @@ -35,18 +35,18 @@ function ReusableBlockEditPanel( props ) {
] }
{ ( isEditing || isSaving ) && [
<input
key="name"
key="title"
type="text"
disabled={ isSaving }
className="reusable-block-edit-panel__name"
value={ name }
onChange={ ( event ) => onChangeName( event.target.value ) } />,
className="reusable-block-edit-panel__title"
value={ title }
onChange={ ( event ) => onChangeTitle( event.target.value ) } />,
<Button
key="save"
isPrimary
isLarge
isBusy={ isSaving }
disabled={ ! name || isSaving }
disabled={ ! title || isSaving }
className="reusable-block-edit-panel__button"
onClick={ onSave }>
{ __( 'Save' ) }
Expand Down
2 changes: 1 addition & 1 deletion blocks/library/block/edit-panel/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
margin-right: auto;
}

.reusable-block-edit-panel__name {
.reusable-block-edit-panel__title {
flex-grow: 1;
font-size: 14px;
height: 30px;
Expand Down
20 changes: 10 additions & 10 deletions blocks/library/block/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ class ReusableBlockEdit extends Component {
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.setTitle = this.setTitle.bind( this );
this.updateReusableBlock = this.updateReusableBlock.bind( this );

this.state = {
isEditing: false,
name: null,
title: null,
attributes: null,
};
}
Expand All @@ -48,7 +48,7 @@ class ReusableBlockEdit extends Component {
stopEditing() {
this.setState( {
isEditing: false,
name: null,
title: null,
attributes: null,
} );
}
Expand All @@ -59,16 +59,16 @@ class ReusableBlockEdit extends Component {
} ) );
}

setName( name ) {
this.setState( { name } );
setTitle( title ) {
this.setState( { title } );
}

updateReusableBlock() {
const { name, attributes } = this.state;
const { title, attributes } = this.state;

// Use pickBy to include only changed (assigned) values in payload
const payload = pickBy( {
name,
title,
attributes,
} );

Expand All @@ -79,7 +79,7 @@ class ReusableBlockEdit extends Component {

render() {
const { focus, reusableBlock, isSaving, convertBlockToStatic } = this.props;
const { isEditing, name, attributes } = this.state;
const { isEditing, title, attributes } = this.state;

if ( ! reusableBlock ) {
return <Placeholder><Spinner /></Placeholder>;
Expand All @@ -102,11 +102,11 @@ class ReusableBlockEdit extends Component {
<ReusableBlockEditPanel
key="panel"
isEditing={ isEditing }
name={ name !== null ? name : reusableBlock.name }
title={ title !== null ? title : reusableBlock.title }
isSaving={ isSaving }
onEdit={ this.startEditing }
onDetach={ convertBlockToStatic }
onChangeName={ this.setName }
onChangeTitle={ this.setTitle }
onSave={ this.updateReusableBlock }
onCancel={ this.stopEditing }
/>
Expand Down
2 changes: 1 addition & 1 deletion editor/components/inserter/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class InserterMenu extends Component {
initialAttributes: {
ref: reusableBlock.id,
},
title: reusableBlock.name,
title: reusableBlock.title,
icon: 'layout',
category: 'reusable-blocks',
} ) );
Expand Down
14 changes: 7 additions & 7 deletions editor/store/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,18 +321,18 @@ export default {

let result;
if ( id ) {
result = new wp.api.models.ReusableBlocks( { id } ).fetch();
result = new wp.api.models.Blocks( { id } ).fetch();
} else {
result = new wp.api.collections.ReusableBlocks().fetch();
result = new wp.api.collections.Blocks().fetch();
}

result.then(
( reusableBlockOrBlocks ) => {
dispatch( {
type: 'FETCH_REUSABLE_BLOCKS_SUCCESS',
reusableBlocks: castArray( reusableBlockOrBlocks ).map( ( { id: itemId, name, content } ) => {
reusableBlocks: castArray( reusableBlockOrBlocks ).map( ( { id: itemId, title, content } ) => {
const [ { name: type, attributes } ] = parse( content );
return { id: itemId, name, type, attributes };
return { id: itemId, title, type, attributes };
} ),
} );
},
Expand All @@ -351,10 +351,10 @@ export default {
const { id } = action;
const { getState, dispatch } = store;

const { name, type, attributes, isTemporary } = getReusableBlock( getState(), id );
const { title, type, attributes, isTemporary } = getReusableBlock( getState(), id );
const content = serialize( createBlock( type, attributes ) );
const requestData = isTemporary ? { name, content } : { id, name, content };
new wp.api.models.ReusableBlocks( requestData ).save().then(
const requestData = isTemporary ? { title, content } : { id, title, content };
new wp.api.models.Blocks( requestData ).save().then(
( updatedReusableBlock ) => {
dispatch( {
type: 'SAVE_REUSABLE_BLOCK_SUCCESS',
Expand Down
24 changes: 12 additions & 12 deletions editor/store/test/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,12 +501,12 @@ describe( 'effects', () => {
const promise = Promise.resolve( [
{
id: 'a9691cf9-ecaa-42bd-a9ca-49587e817647',
name: 'My cool block',
title: 'My cool block',
content: '<!-- wp:core/test-block {"name":"Big Bird"} /-->',
},
] );

set( global, 'wp.api.collections.ReusableBlocks', class {
set( global, 'wp.api.collections.Blocks', class {
fetch() {
return promise;
}
Expand All @@ -523,7 +523,7 @@ describe( 'effects', () => {
reusableBlocks: [
{
id: 'a9691cf9-ecaa-42bd-a9ca-49587e817647',
name: 'My cool block',
title: 'My cool block',
type: 'core/test-block',
attributes: {
name: 'Big Bird',
Expand All @@ -540,11 +540,11 @@ describe( 'effects', () => {
let modelAttributes;
const promise = Promise.resolve( {
id,
name: 'My cool block',
title: 'My cool block',
content: '<!-- wp:core/test-block {"name":"Big Bird"} /-->',
} );

set( global, 'wp.api.models.ReusableBlocks', class {
set( global, 'wp.api.models.Blocks', class {
constructor( attributes ) {
modelAttributes = attributes;
}
Expand All @@ -566,7 +566,7 @@ describe( 'effects', () => {
reusableBlocks: [
{
id: 'a9691cf9-ecaa-42bd-a9ca-49587e817647',
name: 'My cool block',
title: 'My cool block',
type: 'core/test-block',
attributes: {
name: 'Big Bird',
Expand All @@ -580,7 +580,7 @@ describe( 'effects', () => {
it( 'should handle an API error', () => {
const promise = Promise.reject( {} );

set( global, 'wp.api.collections.ReusableBlocks', class {
set( global, 'wp.api.collections.Blocks', class {
fetch() {
return promise;
}
Expand Down Expand Up @@ -610,7 +610,7 @@ describe( 'effects', () => {
let modelAttributes;
const promise = Promise.resolve( { id: 3 } );

set( global, 'wp.api.models.ReusableBlocks', class {
set( global, 'wp.api.models.Blocks', class {
constructor( attributes ) {
modelAttributes = attributes;
}
Expand All @@ -622,7 +622,7 @@ describe( 'effects', () => {

const reusableBlock = {
id: '69f00b2b-ea09-4d5c-a98f-0b1112d7d400',
name: 'My cool block',
title: 'My cool block',
type: 'core/test-block',
attributes: {
name: 'Big Bird',
Expand All @@ -640,7 +640,7 @@ describe( 'effects', () => {

expect( modelAttributes ).toEqual( {
id: '69f00b2b-ea09-4d5c-a98f-0b1112d7d400',
name: 'My cool block',
title: 'My cool block',
content: '<!-- wp:test-block {\"name\":\"Big Bird\"} /-->',
} );
return promise.then( () => {
Expand All @@ -655,7 +655,7 @@ describe( 'effects', () => {
it( 'should handle an API error', () => {
const promise = Promise.reject( {} );

set( global, 'wp.api.models.ReusableBlocks', class {
set( global, 'wp.api.models.Blocks', class {
save() {
return promise;
}
Expand Down Expand Up @@ -753,7 +753,7 @@ describe( 'effects', () => {
updateReusableBlock( 1, {
id: 1,
isTemporary: true,
name: 'Untitled block',
title: 'Untitled block',
type: staticBlock.name,
attributes: staticBlock.attributes,
} )
Expand Down
116 changes: 116 additions & 0 deletions lib/class-wp-rest-blocks-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php
/**
* Reusable Blocks REST API: WP_REST_Blocks_Controller class
*
* @package gutenberg
* @since 0.10.0
*/

/**
* Controller which provides a REST endpoint for Gutenberg to read, create,
* edit and delete reusable blocks. Blocks are stored as posts with the
* wp_block post type.
*
* @since 0.10.0
*
* @see WP_REST_Controller
*/
class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller {
/**
* Given an update or create request, build the post object that is saved to
* the database.
*
* @since 1.10.0
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Post object or WP_Error.
*/
protected function prepare_item_for_database( $request ) {
$prepared_post = new stdClass;

if ( isset( $request['id'] ) ) {
$existing_post = $this->get_post( $request['id'] );
if ( is_wp_error( $existing_post ) ) {
return $existing_post;
}

$prepared_post->ID = $existing_post->ID;
}

$prepared_post->post_title = $request['title'];
$prepared_post->post_content = $request['content'];
$prepared_post->post_type = $this->post_type;
$prepared_post->post_status = 'publish';

return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
}

/**
* Given a post from the database, build the array that is returned from an
* API response.
*
* @since 1.10.0
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $post, $request ) {
$data = array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
);

$response = rest_ensure_response( $data );

return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
}

/**
* Handle a DELETE request.
*
* @since 1.10.0
*
* @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 delete_item( $request ) {
// Always hard-delete a block.
$request->set_param( 'force', true );

return parent::delete_item( $request );
}

/**
* Builds the block's schema, conforming to JSON Schema.
*
* @since 1.10.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
return array(
'$schema' => 'http://json-schema.org/schema#',
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
'id' => array(
'description' => __( 'Unique identifier for the block.', 'gutenberg' ),
'type' => 'integer',
'readonly' => true,
),
'title' => array(
'description' => __( 'The block\'s title.', 'gutenberg' ),
'type' => 'string',
'required' => true,
),
'content' => array(
'description' => __( 'The block\'s HTML content.', 'gutenberg' ),
'type' => 'string',
'required' => true,
),
),
);
}
}
Loading

0 comments on commit 2bc1329

Please sign in to comment.