-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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: move bootstrapped block types to Redux state #53807
Conversation
Size Change: +162 B (0%) Total Size: 1.51 MB
ℹ️ View Unchanged
|
Flaky tests detected in e6dae48. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6024898479
|
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.
@jsnajdr haven't done a thorough review just yet, but in the meantime, I noticed that the failing e2e tests have a consistent kind of "The block "undefined" must have a title." error and it persists after retrying. Seems to be something related to the changes suggested here.
2d9c9f9
to
bee7370
Compare
That was a bug in the |
See #34299 for reference. The experimental action had indirect e2e test coverage added, but it would be great to add more tests to increase confidence in this logic. I think the issue is that the I believe that the modified action gets raw settings now, while in the past it would standardize it with default values first. |
Thank you for opening this PR. It's definitely the area of the codebase that should be improved after we confirmed over time that #34299 resolved the issue with the order of applying filters for the block registration. I need to spend more time processing the changes proposed. I'd like to share some higher-level thoughts for now after I left a previous comment regarding the failing unit test.
An important note is that |
Yes, I added a unit tests that tests the "reapply block filters" logic. Before, it was tested only through e2e tests. The bug was a simple mistake, I was reading the
Yes, I decided to rearrangle the logic in such a way that |
By the way, from my perspective, you can promote |
This would mean that an inline script that does: wp.blocks.unstable__bootstrapServerSideBlockDefinitions({
'core/paragraph': { title: 'Paragraph' }
}); would be replaced with: wp.apiFetch.use(wp.apiFetch.createPreloadingMiddleware({
'/wp/v2/block-types': {
'body': [ { name: 'core/paragraph', title: 'Paragraph' } ]
}
}); and the access to the boostrapped block types array would be a select( coreStore ).getEntityRecord( 'root', 'blockTypes' ); Is that right? That would work, sure, there's just one caveat:
The preloaded REST data are created in the context of the post editor page, just like the |
@jsnajdr, you did a great job explaining how that could work. The REST API call would definitely need to be preloaded and further optimized so only essential fields get returned, mirroring what happens today around
Excellent observation. I didn't consider it before. In that case, it should be pretty straightforward refactoring that we can tackle after this PR lands. |
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.
Excellent refactoring, @jsnajdr. I did a quite in-depth review and it's looking very close to ready. I noticed one or two potential minor issues, and some room for improvement in terms of how this code was documented in the past (that's on me) to better explain all the ways the block can get registered to satisfy different contexts: client only (covers unit tests and React Native, too) but also client + server.
[ name ]: getBlockSettingsFromMetadata( blockNameOrMetadata ), | ||
} ); | ||
} | ||
const metadata = isObject( blockNameOrMetadata ) |
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.
Previously, it wouldn't run metadata processing when the first param passed to the function is the string representing the block's name. I believe it needs to remain as it as was because the settings
object can provide a different set of keys, and some of them can be already translated like title
or description
. In effect, only when the blockNameOrMetadata
is an object it should call addBootstrappedBlock
. Historically, it was always possible to register a block without block.json
by providing it's name and settings object that could be used as is.
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 made this change because it's very useful for async block loading. At some places, like block inserter, my prototype looks at the server bootstrapped data, to avoid fully loading the block:
const { title, parent, supports } = select( blocksStore ).getBootstrappedBlockType( 'core/query-pagination');
But this doesn't work for blocks that are registered only client-side, with no block.json
metadata:
registerBlockType( 'core/query-pagination', {
title: __( 'Pagination' ),
parent: [ 'core/query' ],
supports: { inserter: false },
} );
There is no metadata
, nothing is added to state.bootstrappedBlockTypes
for this block. That's why I'm "reconstructing" the metadata
by extracting selected fields from settings
.
the settings object can provide a different set of keys, and some of them can be already translated like
title
ordescription
.
There are two scenarios that can happen here:
- the
title
anddescription
fields were bootstrapped from the server. Then they are localized (the server did that) and the call toaddBootstrappedBlock
inregisterBlockType
won't overwrite them. It will do at best the polyfilling of.selectors
. - the fields were not bootstrapped from the server, and
addBoostrappedBlock
will add the data extracted fromsettings
tostate.bootstrappedBlockType
. Thetitle
should be localized, because the JS code will calltitle: __( 'Pagination' )
if the block is localized at all.
So, in neither of these cases are correct localized data overwritten with incorrent unlocalized data. We're just adding "fallback" bootstrapped metadata in case they don't exist at all.
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.
If it's not using block.json
, then it should never have textdomain
in the object, so it shouldn't run custom code that applies localization helpers. We are good in that regard.
It still isn't clear to me why it's necessary to fake the block-type bootstrapping in the case where it is registered on the client with:
registerBlockType( 'core/query-pagination', { title: __( 'Pagination' ), parent: [ 'core/query' ], supports: { inserter: false }, } );There is no metadata, nothing is added to state.bootstrappedBlockTypes for this block. That's why I'm "reconstructing" the metadata by extracting selected fields from settings.
In this case, it should be just fine to bootstrap this block type with an empty object to signal it was also registered. Then, you could do the fallback the other way around and read values directly from the processed version of the block type. You essentially need to fully register the block type anyway to be able to expose it in the inserter.
The only controversial issue is whether to extract "fallback" metadata from a registerBlockType( 'name', { ...settings } ) call. It's useful for the async block loading prototype, but it's also possible to remove this change from this PR and merge it separately, carefully tested.
I guess I don't fully understand the requirements for async loading, so I would say we either pass an empty object for now, or we can skip it altogether so we can discuss it separately and unblock this PR. Otherwise, I think everything is clear and I'm happy to move forward with the refactoring.
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 reverted to the old boostrapping behavior in e6dae48. Extracting the metadata
from settings
can be done later, and can be discussed with better context about async block loading.
Having done this, I believe all feedback is addressed 👍
|
||
const settings = applyFilters( | ||
'blocks.registerBlockType', | ||
blockType, |
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.
Filters might modify the blockType
object directly, so a copy { ...blockType }
was passed in the past. If we aren't concerned with the modifications, then maybe it would be better to use settings
instead of the blockType
after this line.
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 don't need to make a copy because blockType
is a new object (created from default, bootstrapped and unprocessed settings) on each call.
Before this PR, processBlockType
was working directly on an object in Redux state, so a copy was needed.
Still, in both the old and new versions, there is a risk that a filter will do something like:
blockType.supports.inserter = false;
mutating the unprocessed settings object in Redux. We'd need to make a deep copy to avoid that.
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.
That is a good point about deep copy. Thinking a bit more about all of these, I think the challenge here is that block deprecations have very specific needs. You essentially want to pass the orginal blockType
without any modifications/mutations applied with blocks.registerBlockType
filter to avoid the situation where the deprecation has its opinions on how to handle the same filter for every deprecation. Although the old implementation is still not resilient to mutation nested objects ...
Well, it doesn't fail any existing unit tests so that might be not an issue at all 🤷🏻
I'm however still skeptical about whether this REST approach is the right way to go. In addition to the fragility caused by REST being async, it also adds a new |
9e7c9b6
to
3949aa1
Compare
Thanks @gziolo for a detailed review. I addressed all your feedback. The only controversial issue is whether to extract "fallback" metadata from a |
We definitely don't want to make the Anyway, it's a separate issue, and we can discuss it later. The main reason I think it would be valuable is that today, we can only consume core blocks in the mobile app, so that would help to bridge the gap as we would use a single approach to bootstrap blocks registered on the server.
See my comment at #53807 (comment). It would be useful to mention two changes in the CHANGELOG file:
|
11322b9
to
e6dae48
Compare
|
||
// The `autoInsert` prop is not yet included in the server provided | ||
// definitions and needs to be polyfilled. This can be removed when the | ||
// minimum supported WordPress is >= 6.4. |
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.
Added this comment, and it implements the suggestion I shared with @ockham in https://github.com/WordPress/gutenberg/pull/52969/files#r1310187507.
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.
Let's go ahead and move forward now that all feedback has been taken care of. Thank you so much for walking me through all the changes proposed. I'm delighted to see all the improvements applied.
This is a spinoff from the async block loading prototype (#51778), and it moves the
serverSideBlockDefinitions
to Redux state of thecore/blocks
store, and adds some private actions and selectors to that store to work with the bootstrapped block types.The
serverSideBlockDefinitions
object is now instate.bootstrappedBlockTypes
, and almost all code fromunstable__bootstrapServerSideBlockDefinitions
has been moved to theboostrappedBlockTypes
reducer. It's the code that adds some missing properties and converts property names to camel case. There is a newaddBootstrappedBlock
private action to add boostrapped blocks, andunstable__bootstrapServerSideBlockDefinitions
calls this action in a loop.The
__experimentalRegisterBlockType
public action has been renamed to a private actionaddUnprocessedBlock
. It's very unlikely that it's used by any plugin. A wpdirectory.net search shows two matches, but on closer inspection both of them just bundle the@wordpress/blocks
package.Similarly, the
__experimentalGetUnprocessedBlockTypes
public selector has been renamed to privategetUnprocessedBlockTypes
.Now block registration works like this:
addBootstrappedBlock
and stored instate.bootstrappedBlockTypes
.wp.blocks.registerBlockTypes
are stored instate.unprocessedBlockTypes
.processBlockType
function (now extracted to a separate module) that takes these two pieces of information ("bootstrapped" metadata and "unprocessed" settings), merges them together, fills default values, and runs the result throughregisterBlockType
filters. The filtered processed result is stored instate.blockTypes
. That's where all the user code, likewp.blocks.getBlockTypes()
, gets the block types from.__experimentalReapplyBlockTypeFilters
action, called after editor initialization, that reruns theprocessBlockType
job and re-createsstate.blockTypes
once again from the raw data. That accounts for filters that are registered late.A good followup for this PR would be a separate PR that renames and stabilizes the
unstable__bootstrapServerSideBlockDefinitions
and__experimentalReapplyBlockTypeFilters
APIs. Both of them are by now established parts of any Gutenberg editor initialization.