-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Changes from all commits
142c15a
2ec264c
94df1aa
918042b
0246ecc
01908a0
e5c33e5
c774d55
0c9473c
f504864
b6d7b36
0f47630
490da26
7b57c44
55b9e1c
cdefd75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,5 +14,6 @@ export { | |
getBlockType, | ||
getBlockTypes, | ||
hasBlockSupport, | ||
isReusableBlock, | ||
} from './registration'; | ||
|
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; | ||
|
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; | ||
} | ||
} |
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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)? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I also even wonder if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think there's a lot of merit to that idea.
That one I'm not too convinced of right now. I mean, I could picture using |
||
( 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: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
ref: { | ||
type: 'string', | ||
}, | ||
}, | ||
|
||
edit: ConnectedReusableBlockEdit, | ||
save: () => null, | ||
} ); |
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', | ||
) ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.