-
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
Blocks: Support reusable nested blocks (reusable blocks refactor) #5228
Conversation
8a970d1
to
5c6cd14
Compare
Rebased to resolve conflicts. Also made an update in 5c6cd14 which avoids referencing the For the editable controls removed in 7d0bdde, I think there might be a solution involving Still need to reinstate effects tests. |
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.
This is looking great. Really nice work 😍
Two issues I noticed in my testing:
- Reusable blocks are not displaying at all in the inserter. They display correctly on
master
. I can see that theFETCH
happens, so my guess is that it's a regression in thegetInserterItems
selector. - 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 whenRECEIVE_REUSABLE_BLOCKS
is dispatched.
editor/store/selectors.js
Outdated
return { | ||
...block, | ||
id: ref, | ||
isTemporary: ! Number.isInteger( ref ), |
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 we split this out into a seperate isReusableBlockTemporary
selector? This would match isFetchingReusableBlock
and isSavingReusableBlock
.
blocks/library/block/index.js
Outdated
{ element } | ||
{ ( isSelected || isEditing ) && ( | ||
<ReusableBlockEditPanel | ||
key="panel" |
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.
We can omit this now that we're using <Fragment>
.
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.
No longer relevant.
blocks/block-edit/index.js
Outdated
|
||
// `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. |
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.
s/preferencially/preferentially/
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.
Fixed in f4eb391.
} ) ), | ||
withAPIData( ( { postType } ) => ( { | ||
user: `/wp/v2/users/me?post_type=${ postType }&context=edit`, | ||
} ) ), |
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.
I've seen this pattern in a few places now. I wonder if it's worth encapsulating this in a new withUserCapabilities
HOC?
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.
Probably not necessary for now.
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.
With #5219, I might imagine this could be a simple selector on a core data module.
isFetchingReusableBlock, | ||
isSavingReusableBlock, | ||
getBlock, | ||
} = select( 'core/editor' ); |
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.
This is great ❤️
editor/store/effects.js
Outdated
@@ -355,9 +364,11 @@ export default { | |||
|
|||
const { id } = action; | |||
const { getState, dispatch } = store; | |||
const state = getState(); |
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.
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();
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.
Fixed in 4e1026f.
editor/store/effects.js
Outdated
} ) | ||
) ); | ||
|
||
dispatch( receiveBlocks( [ parsedBlock ] ) ); |
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.
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.
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.
Fixed in 08333d3.
const parsedBlock = getBlock( getState(), action.uid ); | ||
const reusableBlock = { | ||
id: uniqueId( 'reusable' ), | ||
uid: parsedBlock.uid, |
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.
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.
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.
Couldn't naming the property blockUID
achieve the same effect as what you're describing?
editor/store/test/selectors.js
Outdated
expect( isSaving ).toBe( true ); | ||
} ); | ||
} ); | ||
|
||
describe( 'isFetchingReusableBlock', () => { | ||
it( 'should return false when the block is not being saved', () => { |
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.
s/saved/fetched/
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.
Fixed in c348a43.
editor/store/test/selectors.js
Outdated
expect( isFetching ).toBe( false ); | ||
} ); | ||
|
||
it( 'should return true when the block is being saved', () => { |
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.
s/saved/fetched/
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.
Fixed in c348a43.
🎶 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.
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. |
5c6cd14
to
e34e523
Compare
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. |
a33325c
to
ba90cbc
Compare
This should be good to go. Care to give it a quick check, @aduth? |
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.
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'; |
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.
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.
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.
👌 moved in e99f333aaeb2626b40f4c03c6fb8d5996443d50c.
editor/store/selectors.js
Outdated
@@ -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 ) ); |
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.
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 )
);
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.
👌 changed in 7d87636ec2d95a9e678423fa66390ee2649af4ad.
It looks like this is broken in
👍 |
ba90cbc
to
e99f333
Compare
This reverts commit 8872058.
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`.
e99f333
to
3f0180c
Compare
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. |
Thanks for your help with this pull request @noisysocks |
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:
BlockListBlock
toBlockEdit
, as the latter is the component used in rendering fromcore/block
'sedit
blocks
toeditor
, asBlockEdit
requires access to theBlockList
componentA 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 thewithChangeDetection
andwithHistory
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:
Remaining tasks: