-
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
Block settings extensibility #7895
Conversation
d86bbad
to
4c25f62
Compare
See |
From https://wordpress.slack.com/archives/C02QB2JS7/p1531410854000014 instead of passing 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.
I think this is looking good 👍
One thing that may worths discussion is if we should pass the allowedBlocks prop to the exposed slot/fill component and have a simple BlockSettingsItem for UI instead of doing the fill directly in the item component.
); | ||
|
||
BlockSettingsMenuPluginsGroup.Slot = withSelect( ( select, { fillProps: { uids } } ) => ( { | ||
selectedBlocks: map( flatten( [ uids ] ), ( uid ) => select( 'core/editor' ).getBlockName( 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.
map will generate a new array reference every time the store changes which may cause unnecessary rerenders. Maybe we can use elector getBlocksByUID which is cached and just that array down.
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.
dcf20eb
import BlockSettingsMenuPluginsGroup from './block-settings-menu-plugins-group'; | ||
|
||
const shouldRenderItem = ( selectedBlockNames, allowedBlockNames ) => ! Array.isArray( allowedBlockNames ) || | ||
difference( selectedBlockNames, allowedBlockNames ).length === 0; |
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.
difference( selectedBlockNames, allowedBlockNames ).length === 0; seems like a simple way to check if all all items of selectedBlockNames are contained in allowedBlockNames.
But it makes people reading the code having to think a little bit about what the code is doing. Maybe we can have a simple function arrayContainsArray or something similar and use it in shouldRenderItem plus a comment saying that item is rendered if allowedBlocks is not set or if all selectedBlocks are included in allowedBlocks.
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.
const shouldRenderItem = ( selectedBlockNames, allowedBlockNames ) => ! Array.isArray( allowedBlockNames ) || | ||
difference( selectedBlockNames, allowedBlockNames ).length === 0; | ||
|
||
const BlockSettingsMenuPluginsItem = ( { allowedBlocks, icon, label, onClick, small, role } ) => ( |
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.
Plugins may want to have multiple menu items and even have their own separators to separate their items. In that case, they would need to be repeating the allowedBlocks prop on each item plus use the BlockSettingsMenuPluginsGroup to pass the separator.
Maybe we can consider exposing the slot in a way that does not render anything and receives the allowed block props. And exposes a simple component called SettingMenuItem that just uses IconButton and handles the mobile label logic.
E.g:
<PluginBlockSettingsMenu allowedBlocks={ [...] }>
<BlockSettingMenuItem
icon={}
label="Item of the first group"
onClick={}
/>
...
<div className="editor-block-settings-menu__separator" />
<BlockSettingMenuItem
icon={}
label="Item of the second group"
onClick={}
/>
...
</PluginBlockSettingsMenu>
This would also allow plugins more options to customize their UI.
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 like the idea of enabling plugins to introduce more than one item on the block settings menu.
How about this API?
<BlockSettingsMenuPluginsItem
allowedBlockNames=[ 'core/paragraph' ]
secondaryMenu=[
{
label: 'Secondary 1',
onClick: { secondary1OnClick }
},
{
label: 'Secondary 2',
onClick: { secondary2OnClick }
},
],
icon='dashicon-name'
label='Plugin name'
onClick={ primaryOnClick } />
The idea would be to enable plugins to add either one or multiple items, in an browsers-like fashion (compare LastPass
VS Save to Pocket
items):
If BlockSettingsMenuPluginsItem
receives a secondaryMenu
property it'll introduce a secondary menu and an arrow in the item to communicate there is another level. If secondaryMenu
is not present it'll render a normal item. On mobile, we may want to use a different pattern (for ex: render the two items consecutively in the primary menu).
This doesn't address allowing plugins to add separators. I lean towards thinking separators should be UI only core can add - by staying the same no matter the plugins an user has installed Gutenberg communicates clearly how the menu is organized.
What do you think?
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 @nosolosw, I like the idea of submenus so plugins can group their actions if they have multiple operations.
This proposal introduces a new type of menus that we don't have in Gutenberg yet cc: @karmatosed .
Regarding the technical point of view:
What if we have a component that allows the rendering of multiple items like:
<MultipleItemsMenu
icon='dashicon-name'
label='Plugin name'"
>
<MenuItem
label="Secondary 1"
onClick={}
/>
<MenuItem
label="Secondary 2"
onClick={}
/>
</MultipleItemsMenu>
It would be a simple UI component that even core could use.
If we allow plugins to render what they want to the slot, then they could use this component as:
<PluginBlockSettingsMenu allowedBlocks={ [...] }>
<MultipleItemsMenu
icon='dashicon-name'
label='Plugin name'"
>
...
</PluginBlockSettingsMenu>
Both approaches are similar both if we use react components we have some things simplified.
Using secondaryMenu=[ { label: 'Secondary 1', onClick: { secondary1OnClicl } ...,
Will create a new array reference and new object references per item inside the array. For plugins to avoid rerenders they will need to have some logic to cache the references and only update the references when onClick or labels change. If we represent the same info as React components that logic is not needed.
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.
As per private conversations with Tammie, she suggested that plugins shouldn't be able to have the flexibility to customize the slot at this point (adding separators, etc).
There are a couple of extra considerations we haven't considered yet and we should take into account:
- the API surface of this should be similar to the one of
PluginSidebarMenuItem
, as to create a coherent experience for developers. - plugins may want to have a different set of
allowedBlocks
per item.
All things considered, I think we should go with the API as it is in the PR, as it meets all requirements (plugins can have more than one item, items can have different allowedBlocks
sets, developers can't add their own separators/components, and the API shape is the same than the PlugiSidebarMenuItem
). I still think a component for secondary menus could be useful, but that can be added later if it proves necessary/useful.
Thanks for your time, Jorge! This conversation has helped me to understand better the Gutenberg APIs.
selectedBlocks: map( flatten( [ uids ] ), ( uid ) => select( 'core/editor' ).getBlockName( uid ) ), | ||
} ) )( BlockSettingsMenuPluginsGroupSlot ); | ||
|
||
export default BlockSettingsMenuPluginsGroup; |
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.
Normally we start the extensibility slot names with Plugin e.g: PluginPostPublishPanel, PluginPostStatusInfo etc... So it may make sense to call this component something like PluginBlockSettingsMenu.
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.
1237f0b
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.
As per #7895 (comment) reverted this change: the external API lives now in edit-post/block-settings-menu
and can be used as PluginBlockSettingsMenuItem
.
} | ||
return ( <IconButton | ||
className="editor-block-settings-menu__control" | ||
onClick={ compose( onClick, onClose ) } |
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.
compose will call: onClick( onClose( ...arg ) ); I think what we want here is:
( ...args ) => {
onClick( ...args );
onClose();
}
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.
Note that the onClick
API isn't passed any props, so
onClick( onClose() );
VS
( ) => {
onClick( );
onClose();
}
is essentially the same (omiting the sequence they're called, which doesn't matter as closing and executing the action are parallel processes).
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 thought we passed arguments to onClick, if we don't pass them it is exactly the same.
303efce
to
1237f0b
Compare
editor/components/index.js
Outdated
@@ -83,6 +83,8 @@ export { default as BlockMover } from './block-mover'; | |||
export { default as BlockSelectionClearer } from './block-selection-clearer'; | |||
export { default as BlockSettingsMenu } from './block-settings-menu'; | |||
export { default as _BlockSettingsMenuFirstItem } from './block-settings-menu/block-settings-menu-first-item'; | |||
export { default as PluginBlockSettingsMenuGroup } from './block-settings-menu/plugin-block-settings-menu-group'; |
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.
All plugin specific code should live outside of editor
folder. This is purely design decision. The idea is that editor
is meant to provide all the pieces to required to build your own editor interface. edit-post
is one of the examples of this implementation. You can check how this was done for publish panels in here:
https://github.com/WordPress/gutenberg/blob/master/editor/components/post-publish-panel/index.js#L88-L99
Related PR and discussion: #6798.
@@ -99,6 +100,7 @@ export class BlockSettingsMenu extends Component { | |||
{ count === 1 && <BlockHTMLConvertButton uid={ firstBlockUID } role="menuitem" /> } | |||
<BlockDuplicateButton uids={ uids } rootUID={ rootUID } role="menuitem" /> | |||
{ count === 1 && <SharedBlockConvertButton uid={ firstBlockUID } onToggle={ onClose } itemsRole="menuitem" /> } | |||
<PluginBlockSettingsMenuGroup.Slot fillProps={ { uids, onClose } } /> |
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.
See also related discussion related to using slots in here:
#7199 (comment)
There are a few important point discussed and you can also learn why:
<_BlockSettingsMenuFirstItem.Slot fillProps={ { onClose } } />
exists in the first place and why it is considered as internal implementation.
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.
01b045b takes this and #7895 (comment) into account and exposes the API as PluginBlockSettingsMenuItem
.
@@ -0,0 +1,4 @@ | |||
import { _BlockSettingsMenuPluginsItem } from '@wordpress/editor'; | |||
|
|||
const PluginBlockSettingsMenuItem = _BlockSettingsMenuPluginsItem; |
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.
PluginPrePublishPanel
which I referenced as an example is fully defined inside edit-post
module, and passed down to the component imported from editor
module as a prop. Is there any reason we shouldn't be doing the same in here?
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 think I've addressed this concern with the latest changes, editor and edit-post are decoupled now.
<Slot fillProps={ { ...fillProps, selectedBlocks } } > | ||
{ ( fills ) => ! isEmpty( fills ) && ( | ||
<Fragment> | ||
<div className="editor-block-settings-menu__separator" /> |
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.
Wouldn't it be more flexible if produce the following code:
<div className="class-name...">
{ fills }
</div>
In More Menu we don't use explicit DOM elements as separators, we rather use CSS styles to achieve a similar result. In this scenario, the placement of separator is fully dependent on the sibiling componets which is not optimal.
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.
Note that this is how the BlockSettingsMenu works at the moment, which is different from the MoreMenu. I'd like to leave this out of this PR as it's a different concern.
9cec012
to
566a1b8
Compare
@jorgefilipecosta @gziolo I think I've addressed all feedback provided. Can this be merged? |
566a1b8
to
abd5385
Compare
This has been rebased from master (there was some conflicts because |
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 @nosolosw, things worked correctly in my tests and the code looks good to me.
There is a design detail that I noticed. When we have just one plugin I think we may be missing a separator between plugins and remove:
If we add two or more plugins the separator appears:
We should also add some docs to this API not certain if here or in a followup PR.
Docs are essential for any public API. They should be added to the matching section in https://github.com/WordPress/gutenberg/blob/master/docs/extensibility/extending-blocks.md. |
abd5385
to
d51b5ee
Compare
Added docs in d51b5ee. @jorgefilipecosta Tried with Chrome and Firefox (Ubuntu OS) but I can't repro the issue. I can't figure out why that would happen: that separator element is static, not dependant on any other item. Would it be too much to ask to clean up the branch and try again? |
I'm sorry for the trouble cause @nosolosw it looks like something was wrong in my browser state in a private window and in other browsers the design was right. |
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 code looks good to me and it tested great 👍 Thank you for iterating on this @nosolosw!
I can't thank you enough for helping me through this review @jorgefilipecosta and @gziolo My Gutenknowledge grew by an order of magnitude! ❤️ |
editor exposes the _BlockSettingsMenuPluginsExtension component that specific implementations can use. edit-post chooses to fill that slot with a component of their own (PluginBlockSettingsMenuGroup) which happens to be a slot as well. This way, third-party authors can hook into the edit-post specific implementation by using the PluginsBlockSettingsMenuItem fill.
d51b5ee
to
740f68e
Compare
}; | ||
|
||
PluginBlockSettingsMenuGroup.Slot = withSelect( ( select, { fillProps: { clientIds } } ) => ( { | ||
selectedBlocks: select( 'core/editor' ).getBlocksByUID( clientIds ), |
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.
getBlocksByUID
was deprecated in Gutenberg v3.3 and is slated to be removed in the next release. A warning would have been logged to your console on its use.
* WordPress dependencies | ||
*/ | ||
import { IconButton } from '@wordpress/components'; | ||
import { compose } from '@wordpress/element'; |
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.
Similarly, wp.element.compose
was deprecated in v3.3 and is slated to be removed in the next release. See #7948
Worth pointing out that the removal of the deprecated features caused no test failures, meaning test coverage is non-existent for this functionality, and was only caught by pure luck in project search. |
@aduth Good catch. I haven't noticed the warnings, otherwise, I'd have addressed them. Now that I'm aware that's logged, will look into that thoroughly next time, thanks. |
In fairness, it's not entirely on you that a deprecation was overlooked, as it's a requirement of the deprecation process (and common point of conversation) that it be communicated effectively. Console logging is decent, but obviously not the best solution. It would be nice if we could surface this through the UI, plugin update interface, and/or automated tests (I've been considering introducing |
See #8721 |
@aduth @jorgefilipecosta Seems to be working but with different styling from the rest. Am I missing something? Thanks!
|
@nosolosw Wow! That's fast :) I'll wait for the merge. Thanks! |
Add a Slot in the BlockSettingsMenu for third-parties to hook into.
onClick
will be called when the user clicks the menu item.allowedBlocks
should be a whitelist of block names. If not present the item will be shown for every block. If multiple blocks are selected, the item will be shown if all of them are in the whitelist.An example on how a plugin would use this API:
Open questionsshould the API allow whitelist/blacklist the block types in which this item should be shown? If so, what would be the best way to declare this? I was thinking about adding a new propMimic theblockList
to theBlockSettingsMenuPluginsItem
that would contain the block names.allowedBlocks
property of InnerBlocks.is the block UID enough info for the callback or do we anticipate the plugin would need anything else?Plugin authors should use the selectors in core data package to get access to the selected blocks.Testing
screenoptions
. On clicking,Block clicked
is logged to the console.allowedBlocks
property (no prop, different block names, multiple selections, etc) and test that results are expected.