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

Blocks: Support reusable nested blocks (reusable blocks refactor) #5228

Merged
merged 17 commits into from
Mar 16, 2018

Conversation

aduth
Copy link
Member

@aduth aduth commented Feb 23, 2018

Supersedes #5010
Cherry-picks 840fbec and ee61e18 from #5010
Cherry-picks dde1787 and 80e03de from #5223
Cherry-picks 66fdda5 from #5220

This pull request seeks to support reusable nested blocks. Unlike #5010, the changes here do not expand the reusable blocks API to support nested blocks, but rather refactor reusable blocks to serve as pointers to the original block references, therefore having better natural support for nested blocks.

A few of the changes have been brought along from #5010:

  1. Move context assignment of inner block list from BlockListBlock to BlockEdit, as the latter is the component used in rendering from core/block's edit
    • Downside is a circular dependency from blocks to editor, as BlockEdit requires access to the BlockList component
  2. Update reusable block UI behaviors to not abort editing when the block becomes deselected, as this is incompatible with nested blocks where selection changes to the inner block

A complication of reusable blocks as pointers it that we need to add support for the blocks state to receive arbitrary blocks after initial load without dirtying state (the loaded result of a reusable block should be merged into but not dirtying to blocks state). New options of ignoreTypes have been added to both the withChangeDetection and withHistory higher-order reducers to accommodate this.

Testing instructions:

Verify there are no regressions in the behavior of reusable blocks.

Ensure that you can save a Columns block, including content, as a reusable block.

Ensure unit tests pass:

npm test

Remaining tasks:

  • Reusable blocks effects tests

@aduth aduth added [Feature] Blocks Overall functionality of blocks [Feature] Synced Patterns Related to synced patterns (formerly reusable blocks) labels Feb 23, 2018
@noisysocks noisysocks self-requested a review February 27, 2018 04:37
@aduth aduth requested a review from a team February 27, 2018 18:21
@aduth aduth modified the milestone: 2.3 Feb 27, 2018
@aduth aduth force-pushed the fix/reusable-blocks-pointers branch 2 times, most recently from 8a970d1 to 5c6cd14 Compare March 7, 2018 02:43
@aduth
Copy link
Member Author

aduth commented Mar 7, 2018

Rebased to resolve conflicts. Also made an update in 5c6cd14 which avoids referencing the wp.blocks.BlockList global and respects contextual toolbar and block menu settings.

For the editable controls removed in 7d0bdde, I think there might be a solution involving withFocusOutside, but it's a bit tricky because focus is lost when transitioning into Edit mode because the Edit button disappears, so we need to find some other way to transition focus back into the block after editing has started. We should be able to just dispatch selectBlock on the block and rely on the selection focusTabbable behavior, but this was not working in my testing (on a Columns reusable block).

Still need to reinstate effects tests.

Copy link
Member

@noisysocks noisysocks left a comment

Choose a reason for hiding this comment

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

This is looking great. Really nice work 😍

Two issues I noticed in my testing:

  1. Reusable blocks are not displaying at all in the inserter. They display correctly on master. I can see that the FETCH happens, so my guess is that it's a regression in the getInserterItems selector.
  2. If you have a reusable block in the editor and you reload the page, there is a moment where the Block has been deleted or is unavailable message appears for about a single frame, right after the loading spinner hides. My guess is that this message appears in between when FETCH_REUSABLE_BLOCKS_SUCCESS is dispatched and when RECEIVE_REUSABLE_BLOCKS is dispatched.

return {
...block,
id: ref,
isTemporary: ! Number.isInteger( ref ),
Copy link
Member

Choose a reason for hiding this comment

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

Should we split this out into a seperate isReusableBlockTemporary selector? This would match isFetchingReusableBlock and isSavingReusableBlock.

{ element }
{ ( isSelected || isEditing ) && (
<ReusableBlockEditPanel
key="panel"
Copy link
Member

Choose a reason for hiding this comment

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

We can omit this now that we're using <Fragment>.

Copy link
Member

Choose a reason for hiding this comment

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

No longer relevant.


// `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.
Copy link
Member

Choose a reason for hiding this comment

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

s/preferencially/preferentially/

Copy link
Member

Choose a reason for hiding this comment

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

Fixed in f4eb391.

} ) ),
withAPIData( ( { postType } ) => ( {
user: `/wp/v2/users/me?post_type=${ postType }&context=edit`,
} ) ),
Copy link
Member

Choose a reason for hiding this comment

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

I've seen this pattern in a few places now. I wonder if it's worth encapsulating this in a new withUserCapabilities HOC?

Copy link
Member

Choose a reason for hiding this comment

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

Probably not necessary for now.

Copy link
Member Author

Choose a reason for hiding this comment

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

With #5219, I might imagine this could be a simple selector on a core data module.

isFetchingReusableBlock,
isSavingReusableBlock,
getBlock,
} = select( 'core/editor' );
Copy link
Member

Choose a reason for hiding this comment

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

This is great ❤️

@@ -355,9 +364,11 @@ export default {

const { id } = action;
const { getState, dispatch } = store;
const state = getState();
Copy link
Member

Choose a reason for hiding this comment

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

nit: Could avoid assigning getState to its own variable now that it's only used once.

const { id } = action;
const { dispatch } = store;
const state = store.getState();

Copy link
Member

Choose a reason for hiding this comment

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

Fixed in 4e1026f.

} )
) );

dispatch( receiveBlocks( [ parsedBlock ] ) );
Copy link
Member

Choose a reason for hiding this comment

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

This confused me for a minute. Let's add a comment explaining that it's necessary to put parsedBlock back into the store because replaceBlock will have removed it.

Copy link
Member

Choose a reason for hiding this comment

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

Fixed in 08333d3.

const parsedBlock = getBlock( getState(), action.uid );
const reusableBlock = {
id: uniqueId( 'reusable' ),
uid: parsedBlock.uid,
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, this id vs uid distinction is extra confusing now they're in the same object 😬

I'm more and more convinced we should rename uid to id throughout the codebase. This can then be:

const reusableBlock = {
    id: uniqueId( 'reusable' ),
    blockId: parsedBlock.id,

No need to do this right now though.

Copy link
Member Author

Choose a reason for hiding this comment

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

Couldn't naming the property blockUID achieve the same effect as what you're describing?

expect( isSaving ).toBe( true );
} );
} );

describe( 'isFetchingReusableBlock', () => {
it( 'should return false when the block is not being saved', () => {
Copy link
Member

Choose a reason for hiding this comment

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

s/saved/fetched/

Copy link
Member

Choose a reason for hiding this comment

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

Fixed in c348a43.

expect( isFetching ).toBe( false );
} );

it( 'should return true when the block is being saved', () => {
Copy link
Member

Choose a reason for hiding this comment

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

s/saved/fetched/

Copy link
Member

Choose a reason for hiding this comment

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

Fixed in c348a43.

@pento
Copy link
Member

pento commented Mar 9, 2018

🎶 Ain't no party like a recursive block party, because a recursive block party don't stop. 🎶

I ran into the same problem with the inserter as @noisysocks, so I did this by manually editing the shared block in the database.

  • Create a Nested Block
  • Save it as a Shared Block
  • Edit the new Shared block manually, to include itself as a column.
  • Reload Gutenberg
  • 🎉

It'd probably be a good idea to implement recursion limits on this. I'd probably start with not allowing a nested shared block to contain itself, then let's see if there's potential for abuse of nested block trees before we worry too much about putting limits on it.

@noisysocks noisysocks force-pushed the fix/reusable-blocks-pointers branch from 5c6cd14 to e34e523 Compare March 9, 2018 20:31
@noisysocks
Copy link
Member

I rebased, addressed my own feedback, fixed those two issues that I described, and fixed an additional issue I discovered where deleting a reusable block would cause blocks that were created via 'Convert to Regular Block' to be removed.

Still to come: update the effects tests that were removed.

@noisysocks noisysocks dismissed their stale review March 13, 2018 01:04

I addressed all of my own feedback

@noisysocks
Copy link
Member

This should be good to go. Care to give it a quick check, @aduth?

Copy link
Member Author

@aduth aduth left a comment

Choose a reason for hiding this comment

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

I'm noticing that the default block appender no longer shows at all when editing nested blocks. Thinking this could perhaps be related to recent refactoring with it (#5478).

We should probably also wait to merge this until after the next release, since it's large enough to risk breakage.

import { createBlock, BlockEdit } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import { createInnerBlockList } from '../block-list/utils';
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 don't love that we reach into the local utilities of a separate component. Seems like if we need it in both places, might be best to extract up a level to a common place.

Copy link
Member

Choose a reason for hiding this comment

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

👌 moved in e99f333aaeb2626b40f4c03c6fb8d5996443d50c.

@@ -1277,7 +1312,7 @@ export function isSavingReusableBlock( state, ref ) {
* @return {Array} An array of all reusable blocks.
*/
export function getReusableBlocks( state ) {
return Object.values( state.reusableBlocks.data );
return Object.keys( state.reusableBlocks.data ).map( ( ref ) => getReusableBlock( state, ref ) );
Copy link
Member Author

Choose a reason for hiding this comment

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

Minor: Lodash's _.map works well on objects for this purpose (also trivially more performant by avoiding a second iteration):

return map(
	state.reusableBlocks.data,
	( data, ref ) => getReusableBlock( state, ref )
);

Copy link
Member

Choose a reason for hiding this comment

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

👌 changed in 7d87636ec2d95a9e678423fa66390ee2649af4ad.

@noisysocks
Copy link
Member

I'm noticing that the default block appender no longer shows at all when editing nested blocks.

It looks like this is broken in master. I ran git bisect and it seems that 530a66e is what caused it.

We should probably also wait to merge this until after the next release, since it's large enough to risk breakage.

👍

In nested context, block selection changes from reusable block to the inner block being edited, but we want to keep the UI shown
e.g. within Disabled component, e.g. within reusable nested block
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
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.
Allow reusable nested blocks to be previewed in the inserter by having
BlockPreview inject a createInnerBlockList function via context.
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.
Making a copy of the referenced block prevents the regular block from
being removed should the reuasble block be later deleted.
Use _.map to avoid iterating through the reusable blocks twice in
`getReusableBlocks`.
@aduth
Copy link
Member Author

aduth commented Mar 16, 2018

This is looking good, and I'd like it to have a few days to sit in master ahead of the next release 👍

The regression with columns default appender should be addressed separately, since it was not introduced here.

@aduth aduth merged commit f27e228 into master Mar 16, 2018
@aduth aduth deleted the fix/reusable-blocks-pointers branch March 16, 2018 13:20
@aduth
Copy link
Member Author

aduth commented Mar 16, 2018

Thanks for your help with this pull request @noisysocks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Blocks Overall functionality of blocks [Feature] Synced Patterns Related to synced patterns (formerly reusable blocks)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants