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

Use controlled InnerBlocks in reusable blocks #23601

Closed
Closed
Changes from 5 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
321 changes: 150 additions & 171 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,207 +6,186 @@ import { partial } from 'lodash';
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { InnerBlocks } from '@wordpress/block-editor';
import { parse, serialize } from '@wordpress/blocks';
import { Placeholder, Spinner, Disabled } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
BlockEditorProvider,
BlockList,
WritingFlow,
} from '@wordpress/block-editor';
import { compose } from '@wordpress/compose';
import { parse, serialize } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import ReusableBlockEditPanel from './edit-panel';

class ReusableBlockEdit extends Component {
constructor( { reusableBlock } ) {
super( ...arguments );

this.startEditing = this.startEditing.bind( this );
this.stopEditing = this.stopEditing.bind( this );
this.setBlocks = this.setBlocks.bind( this );
this.setTitle = this.setTitle.bind( this );
this.save = this.save.bind( this );

if ( reusableBlock ) {
// Start in edit mode when we're working with a newly created reusable block
this.state = {
isEditing: reusableBlock.isTemporary,
title: reusableBlock.title,
blocks: parse( reusableBlock.content ),
export default function ReusableBlockEdit( {
attributes: { ref },
isSelected,
} ) {
const {
reusableBlock,
isFetching,
isSaving,
blocks,
title,
canUpdateBlock,
} = useSelect(
( select ) => {
const { canUser } = select( 'core' );
const {
__experimentalGetParsedReusableBlock: getParsedReusableBlock,
} = select( 'core/block-editor' );
const {
__experimentalGetReusableBlock: getReusableBlock,
__experimentalIsFetchingReusableBlock: isFetchingReusableBlock,
__experimentalIsSavingReusableBlock: isSavingReusableBlock,
} = select( 'core/editor' );
const _reusableBlock = getReusableBlock( ref );

let _blocks;
if ( _reusableBlock ) {
if ( _reusableBlock.isTemporary ) {
// The getParsedReusableBlock selector won't work for temporary
// reusable blocks.
_blocks = parse( _reusableBlock.content );
} else {
_blocks = getParsedReusableBlock( ref );
}
} else {
_blocks = null;
}

return {
reusableBlock: _reusableBlock,
isFetching: isFetchingReusableBlock( ref ),
isSaving: isSavingReusableBlock( ref ),
blocks: _blocks,
title: _reusableBlock?.title ?? null,
canUpdateBlock:
!! _reusableBlock &&
! _reusableBlock.isTemporary &&
!! canUser( 'update', 'blocks', ref ),
};
} else {
// Start in preview mode when we're working with an existing reusable block
this.state = {
isEditing: false,
title: null,
blocks: [],
};
}
}

componentDidMount() {
if ( ! this.props.reusableBlock ) {
this.props.fetchReusableBlock();
}
}

componentDidUpdate( prevProps ) {
if (
prevProps.reusableBlock !== this.props.reusableBlock &&
this.state.title === null
) {
this.setState( {
title: this.props.reusableBlock.title,
blocks: parse( this.props.reusableBlock.content ),
} );
},
[ ref ]
);

const {
__experimentalFetchReusableBlocks: fetchReusableBlocks,
__experimentalUpdateReusableBlock: updateReusableBlock,
__experimentalSaveReusableBlock: saveReusableBlock,
} = useDispatch( 'core/editor' );

const fetchReusableBlock = partial( fetchReusableBlocks, ref );
const onChange = partial( updateReusableBlock, ref );
const onSave = partial( saveReusableBlock, ref );

// Start in edit mode when working with a newly created reusable block.
// Start in preview mode when we're working with an existing reusable block.
const [ isEditing, setIsEditing ] = useState(
reusableBlock?.isTemporary ?? false
);

// Local state used for temporary (newly-created, unsaved) reusable blocks
// and reusable blocks being edited. This state is used to make changes to
// the block without having to save them.
const [ localTitle, setLocalTitle ] = useState(
reusableBlock && isEditing ? title : null
);
const [ localBlocks, setLocalBlocks ] = useState(
reusableBlock && isEditing ? blocks : null
);

useEffect( () => {
if ( ! reusableBlock ) {
fetchReusableBlock();
}
}
}, [] );

startEditing() {
const { reusableBlock } = this.props;
this.setState( {
isEditing: true,
title: reusableBlock.title,
blocks: parse( reusableBlock.content ),
} );
}
function startEditing() {
// Copy saved reusable block data into local state.
setLocalBlocks( blocks );
setLocalTitle( title );

stopEditing() {
this.setState( {
isEditing: false,
title: null,
blocks: [],
} );
setIsEditing( true );
}

setBlocks( blocks ) {
this.setState( { blocks } );
}
function cancelEditing() {
// Clear local state.
setLocalBlocks( null );
setLocalTitle( null );

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

save() {
const { onChange, onSave } = this.props;
const { blocks, title } = this.state;
const content = serialize( blocks );
onChange( { title, content } );
function saveAndStopEditing() {
onChange( { title: localTitle, content: serialize( localBlocks ) } );
onSave();

this.stopEditing();
// Clear local state.
setLocalBlocks( null );
setLocalTitle( null );

setIsEditing( false );
}

render() {
const {
isSelected,
reusableBlock,
isFetching,
isSaving,
canUpdateBlock,
settings,
} = this.props;
const { isEditing, title, blocks } = this.state;

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

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

let element = (
<BlockEditorProvider
settings={ settings }
value={ blocks }
onChange={ this.setBlocks }
onInput={ this.setBlocks }
>
<WritingFlow>
<BlockList />
</WritingFlow>
</BlockEditorProvider>
return (
<Placeholder>
{ __( 'Block has been deleted or is unavailable.' ) }
</Placeholder>
);
}

if ( ! isEditing ) {
element = <Disabled>{ element }</Disabled>;
function handleModifyBlocks( modifedBlocks ) {
// We shouldn't change local state when the blocks are loading
// from the saved reusable block.
if ( isEditing ) {
setLocalBlocks( modifedBlocks );
}
}

return (
<div className="block-library-block__reusable-block-container">
{ ( isSelected || isEditing ) && (
<ReusableBlockEditPanel
isEditing={ isEditing }
title={ title !== null ? title : reusableBlock.title }
isSaving={ isSaving && ! reusableBlock.isTemporary }
isEditDisabled={ ! canUpdateBlock }
onEdit={ this.startEditing }
onChangeTitle={ this.setTitle }
onSave={ this.save }
onCancel={ this.stopEditing }
/>
) }
{ element }
</div>
);
let content = (
<InnerBlocks
// If editing, use local state; otherwise, load the blocks from the
// saved reusable block.
value={ isEditing ? localBlocks : blocks }
onChange={ handleModifyBlocks }
/>
);

if ( ! isEditing ) {
content = <Disabled>{ content }</Disabled>;
}
}

export default compose( [
withSelect( ( select, ownProps ) => {
const {
__experimentalGetReusableBlock: getReusableBlock,
__experimentalIsFetchingReusableBlock: isFetchingReusableBlock,
__experimentalIsSavingReusableBlock: isSavingReusableBlock,
} = select( 'core/editor' );
const { canUser } = select( 'core' );
const { __experimentalGetParsedReusableBlock, getSettings } = select(
'core/block-editor'
);
const { ref } = ownProps.attributes;
const reusableBlock = getReusableBlock( ref );

return {
reusableBlock,
isFetching: isFetchingReusableBlock( ref ),
isSaving: isSavingReusableBlock( ref ),
blocks: reusableBlock
? __experimentalGetParsedReusableBlock( reusableBlock.id )
: null,
canUpdateBlock:
!! reusableBlock &&
! reusableBlock.isTemporary &&
!! canUser( 'update', 'blocks', ref ),
settings: getSettings(),
};
} ),
withDispatch( ( dispatch, ownProps ) => {
const {
__experimentalFetchReusableBlocks: fetchReusableBlocks,
__experimentalUpdateReusableBlock: updateReusableBlock,
__experimentalSaveReusableBlock: saveReusableBlock,
} = dispatch( 'core/editor' );
const { ref } = ownProps.attributes;

return {
fetchReusableBlock: partial( fetchReusableBlocks, ref ),
onChange: partial( updateReusableBlock, ref ),
onSave: partial( saveReusableBlock, ref ),
};
} ),
] )( ReusableBlockEdit );
return (
<div className="block-library-block__reusable-block-container">
{ ( isSelected || isEditing ) && (
<ReusableBlockEditPanel
isEditing={ isEditing }
title={ isEditing ? localTitle : title }
isSaving={
isSaving && ! ( reusableBlock?.isTemporary ?? false )
}
isEditDisabled={ ! canUpdateBlock }
onEdit={ startEditing }
onChangeTitle={ ( updatedTitle ) => {
if ( isEditing ) {
setLocalTitle( updatedTitle );
}
} }
onSave={ saveAndStopEditing }
onCancel={ cancelEditing }
/>
) }
{ content }
</div>
);
}