-
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
Add allowedBlocksMiddleware #7301
Conversation
InnerBlocks has changed quite a bit since this PR was opened. What's the status here? |
54e9ebf
to
4c8c9f5
Compare
Hi @mcsf, this PR was rebased. The problems this PR address continue to exist. The PR is waiting on a review. |
4c8c9f5
to
3dcb764
Compare
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 makes sense, but I'm also not super familiar with the locking and templates domain. Right now this needs clean up around the inline comments, namely some structuring and maybe rephrasing for clarity.
// on insert we allow the action if at least one of the blocks can be inserted filtering the other blocks | ||
case 'INSERT_BLOCKS': { | ||
const allowedBlocks = | ||
filter( action.blocks, ( block ) => block && canInsertBlockType( store.getState(), block.name, action.rootUID ) ); |
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.
These lines are weird. Consider:
const allowedBlocks = filter( action.blocks, ( block ) =>
block &&
canInsertBlockType( store.getState(), block.name, action.rootUID )
);
*/ | ||
import { canInsertBlockType, getBlock, getBlockName, getBlockRootUID } from '../store/selectors'; | ||
|
||
const allowedBlocksMiddleware = ( store ) => ( next ) => ( action ) => { |
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 should probably have a
middleware
directory somewhere. - Some high-level documentation for this middleware is warranted.
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.
Hi @mcsf, in all the other store artifacts selectors, reducers, etc... We don't have a folder for them. We have all the artifacts inside the same file. So I updated the code an now our middleware is inside the middlewares file.
} | ||
return; | ||
} | ||
default: |
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, but I think we tend to drop default:
and just leave the closing statement (in this case, next( action )
) after the switch
block.
const blockBeingReplaced = uidBlockBeingReplaced && getBlock( store.getState(), uidBlockBeingReplaced ); | ||
return blockBeingReplaced && blockBeingReplaced.name === block.name; | ||
} ); | ||
if ( replaceIsValid ) { |
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.
Spacing before closing bracket.
} | ||
switch ( action.type ) { | ||
// on insert we allow the action if at least one of the blocks can be inserted filtering the other blocks | ||
case 'INSERT_BLOCKS': { |
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.
The braces around cases are something I'd never seen. 😅 I'm surprised the linter didn't complain, but it should be:
switch ( action.type ) {
case 'INSERT_BLOCKS':
foo();
return;
case 'MOVE…':
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.
Yes, they are not very typical but I like to use them in big cases like the ones here. It allows us to collapse case statements in the text editors for example and they make it easier to navigate around the statements.
But we should be consistent and given that we are not using them in other parts I removed them.
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.
allows us to collapse case statements in the text editors
FWIW, editors should be able to use other folding methods (incl. manual and indentation-based), and these preferences shouldn't ever affect the way we write code.
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.
They’re also useful in creating a scope for const
variable definitions inside the case statement. We used them a lot on Mozilla projects, especially in switch case statements for reducers. 🤷🏻♂️
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.
It seems we are also using this 'case VALUE: {' in the blockOrder reducer.
gutenberg/editor/store/reducer.js
Lines 419 to 425 in 6042f35
case 'INSERT_BLOCKS': { | |
const { rootUID = '', blocks } = action; | |
const subState = state[ rootUID ] || []; | |
const mappedBlocks = mapBlockOrder( blocks, rootUID ); | |
const { index = subState.length } = action; | |
return { |
But in this case const scoping is not required, so we can probably continue without the extra {}.
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.
Huh, I hadn't considered the scopes at all. It's true that these cases
all feature local variables. D'oh.
That to me is enough to justify the block braces. I stand corrected; my apologies for the digression.
return true; | ||
} | ||
const uidBlockBeingReplaced = action.uids[ index ]; | ||
const blockBeingReplaced = uidBlockBeingReplaced && getBlock( store.getState(), uidBlockBeingReplaced ); |
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 recommend uidToReplace
and blockToReplace
, respectively.
const rootUID = getBlockRootUID( store.getState(), first( action.uids ) ); | ||
// replace is valid if the new blocks can be inserted in the root block | ||
// or if we had a block of the same type in the position of the block being replaced. | ||
const replaceIsValid = every( action.blocks, ( block, index ) => { |
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'd start the variable name with is
to make its point clearer, and I might even avoid the word replace
, since it's primarely a verb, suggesting a different kind of function. isOperationValid
? isActionValid
?
Without having yet looked closely into this, there's been an idea in my mind lately that I've been wanting to try, though perhaps you can give me your thoughts on whether you think there's merit to it. At one point in the past I expressed a desire to be able to provide an insertion context, which would provide various details including What I considered more recently is: Could we use the pattern of Slot/Fill or even just plain React portals to transport this context to the global inserter? My thinking is that this would be some combination of our Slot/Fill |
Hi @aduth, I think from the technical point of view using slot/fill + context for the inserter may work. But we should decide if we should use the context or we should continue to have this information available globally from our store/data module. In my option, I think the configuration of a nested area is a property of a block just like its attributes, and it should be available using a similar mechanism to the one we provide to query attributes. |
I added |
70d072b
to
9af1365
Compare
Hi @mcsf, thank you for your review. The points raised were addressed. The remaining part of this PR is an extensive list of test cases. I will give some time for us to think/discuss if this approach is something we want, and after I will add the test cases if that's the approach we should follow. |
Yes, I think or API is already simple to use canInsertBlockType( rootUID, blockType ) returns if the block can be inserted inside some specific block. Taking into account all restrictions (allowedBlocks, locking, available blocks etc....). We have many checks to make on the UI e.g: show the option to insert some block or not, show the option to transform move etc... But even with context, if we want an optional UX these checks need to be made in order for us to render an option or not. |
9af1365
to
f1d4502
Compare
@jorgefilipecosta what's the status of this PR, it seems to be very old. Is it something that still needs to be addressed since it didn't make to WordPress 5.0 release? I'm inclined to mark it |
Closing I will soon open a new PR that will address the same problem in a simpler way using controls, in a similar way to what was done in #13924. |
The PR fixes the following list of issues:
This PR adds a middleware to the editor store whose objective is to make sure we never get into a situation where a block should not have been added to an area.
Props to @aduth for referring the middleware as an option.
This PR's aims to simplify some existing PR's, avoid code duplication and reduce the need for allowed blocks checking in the UI. Some checks will still be required thought to provide a better user experience, e.g., don't allow drag to even start in some cases, don't show unallowed transforms, etc...
Instead of middleware, we could have tried to use a top-level editor higher order reducer ( "top level" because we need different branches of the editor state tree). I felt the higher order reducer approach would not be a good idea because to make this verification we use information from blocks store (inside canInsertBlockType selector), so we would make our reducers impure.
This middleware watches 3 actions: INSERT_BLOCKS, MOVE_BLOCK_TO_POSITION, and REPLACE_BLOCKS.
On INSERT_BLOCKS we filter the blocks to insert to make sure all of them are allowed. If at least one of the blocks we are trying to add is permitted the action is dispatched with the filtered allowed blocks if none of the blocks are allowed the action is discarded.
For the actions MOVE_BLOCK_TO_POSITION and REPLACE_BLOCKS we verify if it is possible to contain all the blocks in the root UID if it is not possible the action is discarded.
Supersedes: #7265, #7212
Simplifies: https://github.com/WordPress/gutenberg/pull/7230/files
Todo: Automated tests & extensive docs tests were not yet added, I would like to get some feedback on this approach and if we think it is something we should follow I will add them before the merge.
How has this been tested?
Test block: https://gist.github.com/jorgefilipecosta/b7194daac3ce26827522694fb4252c1c#file-testallowedblocksmiddleware-js
I pasted the test block referred in the browser console.
I added the Product block.
I verified I cannot move using drag & drop the Buy button out of the Product block. (child block restriction.
I verified I cannot move using drag & drop a paragraph created outside of the Product block inside the product block. (allowed blocks restriction).
I verified that if I press enter in the buy button or image inside the product block, a paragraph is not created (allowed blocks restriction).
I added two galleries outside the product block, multi-selected them, and copied them ( ctrl + c ), I pasted them inside the list block inside the Product block and I verified they were not added (allowed blocks restriction).
I verified that when we have a template lock and we press enter on the title we now do not insert unallowed blocks.
Template lock code:
I verified that if we disable the paragraph, e.g., with a filter or inside the test block, pressing enter on the title or in placeholders does not add paragraph blocks. Pressing the sibling inserter also does not insert the paragraph (PR #7226).
Code to disable paragraph:
I added two image blocks, multi-selected them, copied them and tried to paste them on the list inside the test block. I checked the image blocks were not added inside the test block.
All these actions are possible in master.
Todo: If we accept this approach, end 2 end tests will be created before the merge that replicate this manual tests.