Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reusable Blocks: Add reusable blocks UI #3378

Merged
merged 16 commits into from
Dec 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ) && [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the editing variation be visible while the name is being saved? In my testing, it flips back to the static text immediately upon hitting Save.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch—that's a bug that must have slipped in during a merge. Fixed in a820d1cc43dddd97d44e6e756707dfc6ab56c80b.

<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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any ideas at the moment and it may have already been mentioned, but we need to work to find a solution where the block can access state without the implicit dependency on editor, either:

  • Blocks module having its own separate state
  • Merging more of [the non-post-editor-specific-] editor and blocks

Obvious too by the fact that we have equivalent action creators for what we're dispatching here, but unable (unwilling) to import them directly from editor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and I also have no ideas on how to address this right now.

Merging more of [the non-post-editor-specific-] editor and blocks

Could you elaborate on this? I'm not quite caught up on my history here. Did we abandon the effort to merge the two modules (#2795)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea: Does it make more sense for this block to be defined in the editor module instead?

I also even wonder if the blocks module should hold only the blocks API and define all the blocks in the editor module

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea: Does it make more sense for this block to be defined in the editor module instead?

I think there's a lot of merit to that idea.

I also even wonder if the blocks module should hold only the blocks API and define all the blocks in the editor module

That one I'm not too convinced of right now. I mean, I could picture using editor/library, but the current situation of having the library sit next to the API makes sense to me, as the core blocks are the canonical blocks: in that sense, they highly complement the API (both code and docs) by providing numerous well tested use cases.

( 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: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you should need to define this on the client, since it should be bootstrapped from the server definition.

See #2529

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm hesitant to do this because removing the client-side definition means our test fixtures aren't able to test parsing a <!-- wp:block ref="358b59ee-bab3-4d6f-8445-e8c6971a5605" /-->.

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