Skip to content

Commit

Permalink
Reusable Blocks: Add reusable blocks UI (#3378)
Browse files Browse the repository at this point in the history
* 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 <!-- wp:image --> inserts an image, <!-- wp:block --> 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
  • Loading branch information
noisysocks authored and youknowriad committed Dec 8, 2017
1 parent 76060d4 commit e437939
Show file tree
Hide file tree
Showing 33 changed files with 698 additions and 170 deletions.
3 changes: 2 additions & 1 deletion blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions blocks/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export {
getBlockType,
getBlockTypes,
hasBlockSupport,
isReusableBlock,
} from './registration';

12 changes: 12 additions & 0 deletions blocks/api/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
13 changes: 13 additions & 0 deletions blocks/api/test/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getBlockType,
getBlockTypes,
hasBlockSupport,
isReusableBlock,
} from '../registration';

describe( 'blocks', () => {
Expand Down Expand Up @@ -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 );
} );
} );
} );
68 changes: 68 additions & 0 deletions blocks/library/block/edit-panel/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="reusable-block-edit-panel">
{ ! isEditing && ! isSaving && [
<span key="info" className="reusable-block-edit-panel__info">
<b>{ name }</b>
</span>,
<Button
key="edit"
isLarge
className="reusable-block-edit-panel__button"
onClick={ onEdit }>
{ __( 'Edit' ) }
</Button>,
<Button
key="detach"
isLarge
className="reusable-block-edit-panel__button"
onClick={ onDetach }>
{ __( 'Detach' ) }
</Button>,
] }
{ ( isEditing || isSaving ) && [
<input
key="name"
type="text"
disabled={ isSaving }
className="reusable-block-edit-panel__name"
value={ name }
onChange={ ( event ) => onChangeName( event.target.value ) } />,
<Button
key="save"
isPrimary
isLarge
isBusy={ isSaving }
disabled={ ! name || isSaving }
className="reusable-block-edit-panel__button"
onClick={ onSave }>
{ __( 'Save' ) }
</Button>,
<Button
key="cancel"
isLarge
disabled={ isSaving }
className="reusable-block-edit-panel__button"
onClick={ onCancel }>
{ __( 'Cancel' ) }
</Button>,
] }
</div>
);
}

export default ReusableBlockEditPanel;

32 changes: 32 additions & 0 deletions blocks/library/block/edit-panel/style.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
172 changes: 172 additions & 0 deletions blocks/library/block/index.js
Original file line number Diff line number Diff line change
@@ -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 <Placeholder><Spinner /></Placeholder>;
}

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
<div key="edit" style={ { pointerEvents: isEditing ? 'auto' : 'none' } }>
<BlockEdit
{ ...this.props }
focus={ isEditing ? focus : null }
attributes={ reusableBlockAttributes }
setAttributes={ isEditing ? this.setAttributes : noop }
className={ className }
/>
</div>,
focus && (
<ReusableBlockEditPanel
key="panel"
isEditing={ isEditing }
name={ name !== null ? name : reusableBlock.name }
isSaving={ isSaving }
onEdit={ this.startEditing }
onDetach={ convertBlockToStatic }
onChangeName={ this.setName }
onSave={ this.updateReusableBlock }
onCancel={ this.stopEditing }
/>
),
];
}
}

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,
} );
39 changes: 39 additions & 0 deletions blocks/library/block/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
/**
* Server-side rendering of the `core/block` block.
*
* @package gutenberg
*/

/**
* Renders the `core/block` block on server.
*
* @param array $attributes The block attributes.
*
* @return string Rendered HTML of the referenced block.
*/
function gutenberg_render_block_core_reusable_block( $attributes ) {
$reusable_block = get_post( $attributes['ref'] );
if ( ! $reusable_block ) {
return '';
}

$blocks = gutenberg_parse_blocks( $reusable_block->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',
) );
2 changes: 1 addition & 1 deletion blocks/library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ import './text-columns';
import './verse';
import './video';
import './audio';
import './reusable-block';
import './block';
import './paragraph';
Loading

0 comments on commit e437939

Please sign in to comment.