Skip to content

Commit

Permalink
Blocks: Support reusable nested blocks (reusable blocks refactor) (#5228
Browse files Browse the repository at this point in the history
)

* Blocks: Avoid dismissing editable controls

In nested context, block selection changes from reusable block to the inner block being edited, but we want to keep the UI shown

* Block: Move block context provides into BlockEdit

* Revert "Blocks: Disable Convert to Reusable for nested blocks"

This reverts commit 8872058.

* State: Enhance history reducer utils to support ignores

* Block List: Hide disabled default block appender

e.g. within Disabled component, e.g. within reusable nested block

* State: Update reusable blocks as pointers to state blocks

* Block List: Inject inner block list creator as function

Generate BlockList from block's own utils, which has advantage of:

- Not having circular dependency from blocks to editor
- Respecting block menu and contextual toolbar props

* Typo: saved -> fetched

* Typo: preferencially -> preferentially

* Explain why dispatching RECEIVE_BLOCKS is necessary

* Remove unnecessary getState variable

* Update getInserterItems to respect new reusable block data layout

Makes `getInserterItems` and `getFrecentInserterItems` respect that
reusable blocks now point to a block that is elsewhere in the editor
state. This makes reusable blocks again appear in the inserter.

* Provide createInnerBlockList context in BlockPreview

Allow reusable nested blocks to be previewed in the inserter by having
BlockPreview inject a createInnerBlockList function via context.

* Fix flash of 'Not found' error message when fetching a reusable block

Re-order the FETCH_REUSABLE_BLOCKS effect so that the reusable block is
added to the store before it is marked as no longer being fetched. This
prevents core/block from briefly flashing the 'Not found' error message
in between dispatches.

* When converting reusable -> regular, replace the block with a copy

Making a copy of the referenced block prevents the regular block from
being removed should the reuasble block be later deleted.

* Avoid iterating through reusable blocks twice

Use _.map to avoid iterating through the reusable blocks twice in
`getReusableBlocks`.

* Move createInnerBlockList into `editor/utils`
  • Loading branch information
aduth committed Mar 16, 2018
1 parent 8e63479 commit f27e228
Show file tree
Hide file tree
Showing 22 changed files with 704 additions and 557 deletions.
22 changes: 0 additions & 22 deletions blocks/api/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ import {
find,
first,
flatMap,
uniqueId,
} from 'lodash';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { applyFilters } from '@wordpress/hooks';

/**
Expand Down Expand Up @@ -257,23 +255,3 @@ export function switchToBlockType( blocks, name ) {
return applyFilters( 'blocks.switchToBlockType.transformedBlock', transformedBlock, blocks );
} );
}

/**
* Creates a new reusable block.
*
* @param {string} type The type of the block referenced by the reusable
* block.
* @param {Object} attributes The attributes of the block referenced by the
* reusable block.
*
* @return {Object} A reusable block object.
*/
export function createReusableBlock( type, attributes ) {
return {
id: -uniqueId(), // Temporary id replaced when the block is saved server side
isTemporary: true,
title: __( 'Untitled block' ),
type,
attributes,
};
}
15 changes: 0 additions & 15 deletions blocks/api/test/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
cloneBlock,
getPossibleBlockTransformations,
switchToBlockType,
createReusableBlock,
} from '../factory';
import { getBlockTypes, unregisterBlockType, setUnknownTypeHandlerName, registerBlockType } from '../registration';

Expand Down Expand Up @@ -700,18 +699,4 @@ describe( 'block factory', () => {
} );
} );
} );

describe( 'createReusableBlock', () => {
it( 'should create a reusable block', () => {
const type = 'core/test-block';
const attributes = { name: 'Big Bird' };

expect( createReusableBlock( type, attributes ) ).toMatchObject( {
id: expect.any( Number ),
title: 'Untitled block',
type,
attributes,
} );
} );
} );
} );
92 changes: 63 additions & 29 deletions blocks/block-edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
* External dependencies
*/
import classnames from 'classnames';
import { noop } from 'lodash';
import { noop, get } from 'lodash';

/**
* WordPress dependencies
*/
import { withFilters } from '@wordpress/components';
import { withSelect } from '@wordpress/data';
import { Component, compose } from '@wordpress/element';
import { withContext, withFilters, withAPIData } from '@wordpress/components';

/**
* Internal dependencies
Expand All @@ -18,35 +20,67 @@ import {
hasBlockSupport,
} from '../api';

export function BlockEdit( props ) {
const { name, attributes = {} } = props;
const blockType = getBlockType( name );
export class BlockEdit extends Component {
getChildContext() {
const {
id: uid,
user,
createInnerBlockList,
} = this.props;

if ( ! blockType ) {
return null;
return {
BlockList: createInnerBlockList( uid ),
canUserUseUnfilteredHTML: get( user.data, [
'capabilities',
'unfiltered_html',
], false ),
};
}

// Generate a class name for the block's editable form
const generatedClassName = hasBlockSupport( blockType, 'className', true ) ?
getBlockDefaultClassname( name ) :
null;
const className = classnames( generatedClassName, attributes.className );

// `edit` and `save` are functions or components describing the markup
// with which a block is displayed. If `blockType` is valid, assign
// them preferencially as the render value for the block.
const Edit = blockType.edit || blockType.save;

// For backwards compatibility concerns adds a focus and setFocus prop
// These should be removed after some time (maybe when merging to Core)
return (
<Edit
{ ...props }
className={ className }
focus={ props.isSelected ? {} : false }
setFocus={ noop }
/>
);
render() {
const { name, attributes = {}, isSelected } = this.props;
const blockType = getBlockType( name );

if ( ! blockType ) {
return null;
}

// Generate a class name for the block's editable form
const generatedClassName = hasBlockSupport( blockType, 'className', true ) ?
getBlockDefaultClassname( name ) :
null;
const className = classnames( generatedClassName, attributes.className );

// `edit` and `save` are functions or components describing the markup
// with which a block is displayed. If `blockType` is valid, assign
// them preferentially as the render value for the block.
const Edit = blockType.edit || blockType.save;

// For backwards compatibility concerns adds a focus and setFocus prop
// These should be removed after some time (maybe when merging to Core)
return (
<Edit
{ ...this.props }
className={ className }
focus={ isSelected ? {} : false }
setFocus={ noop }
/>
);
}
}

export default withFilters( 'blocks.BlockEdit' )( BlockEdit );
BlockEdit.childContextTypes = {
BlockList: noop,
canUserUseUnfilteredHTML: noop,
};

export default compose( [
withFilters( 'blocks.BlockEdit' ),
withSelect( ( select ) => ( {
postType: select( 'core/editor' ).getEditedPostAttribute( 'type' ),
} ) ),
withAPIData( ( { postType } ) => ( {
user: `/wp/v2/users/me?post_type=${ postType }&context=edit`,
} ) ),
withContext( 'createInnerBlockList' )(),
] )( BlockEdit );
125 changes: 63 additions & 62 deletions blocks/library/block/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* External dependencies
*/
import { pickBy, noop } from 'lodash';
import { connect } from 'react-redux';
import { noop, partial } from 'lodash';

/**
* WordPress dependencies
*/
import { Component, Fragment } from '@wordpress/element';
import { Component, Fragment, compose } from '@wordpress/element';
import { Placeholder, Spinner, Disabled } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';

/**
Expand All @@ -25,12 +25,11 @@ class ReusableBlockEdit extends Component {
this.stopEditing = this.stopEditing.bind( this );
this.setAttributes = this.setAttributes.bind( this );
this.setTitle = this.setTitle.bind( this );
this.updateReusableBlock = this.updateReusableBlock.bind( this );
this.save = this.save.bind( this );

this.state = {
isEditing: !! ( reusableBlock && reusableBlock.isTemporary ),
title: null,
attributes: null,
};
}

Expand All @@ -40,68 +39,63 @@ class ReusableBlockEdit extends Component {
}
}

componentWillReceiveProps( nextProps ) {
if ( this.props.focus && ! nextProps.focus ) {
this.stopEditing();
}
}

startEditing() {
this.setState( { isEditing: true } );
const { reusableBlock } = this.props;

this.setState( {
isEditing: true,
title: reusableBlock.title,
} );
}

stopEditing() {
this.setState( {
isEditing: false,
title: null,
attributes: null,
} );
}

setAttributes( attributes ) {
this.setState( ( prevState ) => ( {
attributes: { ...prevState.attributes, ...attributes },
} ) );
const { updateAttributes, block } = this.props;
updateAttributes( block.uid, attributes );
}

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

updateReusableBlock() {
const { title, attributes } = this.state;
save() {
const { reusableBlock, onUpdateTitle, onSave } = this.props;

// Use pickBy to include only changed (assigned) values in payload
const payload = pickBy( {
title,
attributes,
} );
const { title } = this.state;
if ( title !== reusableBlock.title ) {
onUpdateTitle( title );
}

onSave();

this.props.updateReusableBlock( payload );
this.props.saveReusableBlock();
this.stopEditing();
}

render() {
const { isSelected, reusableBlock, isFetching, isSaving } = this.props;
const { isEditing, title, attributes } = this.state;
const { isSelected, reusableBlock, block, isFetching, isSaving } = this.props;
const { isEditing, title } = this.state;

if ( ! reusableBlock && isFetching ) {
return <Placeholder><Spinner /></Placeholder>;
}

if ( ! reusableBlock ) {
if ( ! reusableBlock || ! block ) {
return <Placeholder>{ __( 'Block has been deleted or is unavailable.' ) }</Placeholder>;
}

const reusableBlockAttributes = { ...reusableBlock.attributes, ...attributes };

let element = (
<BlockEdit
{ ...this.props }
name={ reusableBlock.type }
isSelected={ isEditing && isSelected }
attributes={ reusableBlockAttributes }
id={ block.uid }
name={ block.name }
attributes={ block.attributes }
setAttributes={ isEditing ? this.setAttributes : noop }
/>
);
Expand All @@ -113,14 +107,14 @@ class ReusableBlockEdit extends Component {
return (
<Fragment>
{ element }
{ isSelected && (
{ ( isSelected || isEditing ) && (
<ReusableBlockEditPanel
isEditing={ isEditing }
title={ title !== null ? title : reusableBlock.title }
isSaving={ isSaving && ! reusableBlock.isTemporary }
onEdit={ this.startEditing }
onChangeTitle={ this.setTitle }
onSave={ this.updateReusableBlock }
onSave={ this.save }
onCancel={ this.stopEditing }
/>
) }
Expand All @@ -129,34 +123,41 @@ class ReusableBlockEdit extends Component {
}
}

const ConnectedReusableBlockEdit = connect(
( state, ownProps ) => ( {
reusableBlock: state.reusableBlocks.data[ ownProps.attributes.ref ],
isFetching: state.reusableBlocks.isFetching[ ownProps.attributes.ref ],
isSaving: state.reusableBlocks.isSaving[ ownProps.attributes.ref ],
const EnhancedReusableBlockEdit = compose( [
withSelect( ( select, ownProps ) => {
const {
getReusableBlock,
isFetchingReusableBlock,
isSavingReusableBlock,
getBlock,
} = select( 'core/editor' );
const { ref } = ownProps.attributes;
const reusableBlock = getReusableBlock( ref );

return {
reusableBlock,
isFetching: isFetchingReusableBlock( ref ),
isSaving: isSavingReusableBlock( ref ),
block: reusableBlock ? getBlock( reusableBlock.uid ) : null,
};
} ),
( 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,
} );
},
} )
)( ReusableBlockEdit );
withDispatch( ( dispatch, ownProps ) => {
const {
fetchReusableBlocks,
updateBlockAttributes,
updateReusableBlockTitle,
saveReusableBlock,
} = dispatch( 'core/editor' );
const { ref } = ownProps.attributes;

return {
fetchReusableBlock: partial( fetchReusableBlocks, ref ),
updateAttributes: updateBlockAttributes,
onUpdateTitle: partial( updateReusableBlockTitle, ref ),
onSave: partial( saveReusableBlock, ref ),
};
} ),
] )( ReusableBlockEdit );

export const name = 'core/block';

Expand All @@ -176,6 +177,6 @@ export const settings = {
html: false,
},

edit: ConnectedReusableBlockEdit,
edit: EnhancedReusableBlockEdit,
save: () => null,
};
Loading

0 comments on commit f27e228

Please sign in to comment.