-
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
Add More Menu Item Api #4484
Add More Menu Item Api #4484
Changes from all commits
d971eb5
cec1e66
54e979d
230c82a
2a2224f
011544a
8ca0ac2
0b16b6c
399d526
2abf2e5
b6e9d92
8d0de21
050e377
7fa0a42
b68614f
0e03a43
f7e4ac4
a9f7c8f
95c816d
438015d
b762428
31d50a8
1fdf22c
8997346
f61a32b
8bcb6fb
ffc6038
9779e93
2ae47c6
4e4dee1
07b98e5
daa0605
b1ab904
74c12d8
4178f8c
4a84ec2
27675de
c1c9a9a
31a3815
37590b2
020e191
d19a282
e29e15e
cceb1ad
7496fd3
b2f342a
79fe168
2579564
54f2417
dcab4c1
1c01707
1803d1a
c0be60b
da572d8
b95e43d
4d544ec
9acbe10
e55bdcc
2679098
e70c88d
ebafa1c
d8afc30
2ff0b8e
e71388b
5ece508
46642fa
fdef65d
0a57601
8e97836
b9631b7
3910b3f
8ceca16
a48baf8
ff5d2df
dca2e0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* eslint no-console: [ 'error', { allow: [ 'error' ] } ] */ | ||
|
||
/** | ||
* Wordpress dependencies | ||
*/ | ||
import { applyFilters } from '@wordpress/hooks'; | ||
import { validateNamespacedId } from '@wordpress/utils'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { isString } from 'lodash'; | ||
import { activateSidebar } from './sidebar'; | ||
|
||
const menuItems = {}; | ||
|
||
/** | ||
* Registers a plugin under the more menu. | ||
* | ||
* @param {string} menuItemId The unique identifier of the plugin. Should be in | ||
* `[namespace]/[name]` format. | ||
* @param {Object} settings The settings for this menu item. | ||
* @param {string} settings.title The name to show in the settings menu. | ||
* @param {func} settings.target The registered plugin that should be activated. | ||
* @param {ReactElement} [settings.icon] SVG React Element. | ||
* | ||
* @return {Object} The final sidebar settings object. | ||
*/ | ||
export function registerMoreMenuItem( menuItemId, settings ) { | ||
settings = { | ||
menuItemId, | ||
...settings, | ||
}; | ||
|
||
settings = applyFilters( 'editor.registerMoreMenuItem', settings, menuItemId ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be prefixed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is passing |
||
|
||
if ( ! validateNamespacedId( menuItemId ) ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All this common validation makes me wonder at what point we should consider reaching for a full-fledge solution built for this type of thing, such as Yup (Joi) or JSON Schema. |
||
return null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the discrepancy between this registration's returning of |
||
} | ||
if ( menuItems[ menuItemId ] ) { | ||
console.error( | ||
`Menu item "${ menuItemId }" is already registered.` | ||
); | ||
} | ||
|
||
if ( ! settings.title ) { | ||
console.error( | ||
`Menu item "${ menuItemId }" must have a title.` | ||
); | ||
return null; | ||
} | ||
if ( typeof settings.title !== 'string' ) { | ||
console.error( | ||
'Menu items title must be strings.' | ||
); | ||
return null; | ||
} | ||
|
||
if ( settings.icon && isString( settings.icon ) ) { | ||
console.error( | ||
'Menu item icon must be a react component' | ||
); | ||
return null; | ||
} | ||
|
||
if ( ! settings.target ) { | ||
console.error( | ||
`Menu item "${ menuItemId }" must have a target.` | ||
); | ||
return null; | ||
} | ||
if ( typeof settings.target !== 'string' ) { | ||
console.error( | ||
'Menu items target must be strings.' | ||
); | ||
return null; | ||
} | ||
|
||
settings.callback = activateSidebar.bind( null, settings.target ); | ||
|
||
return menuItems[ menuItemId ] = settings; | ||
} | ||
|
||
/** | ||
* Retrieves all menu items that are registered. | ||
* | ||
* @return {Object} Registered menu items. | ||
*/ | ||
export function getMoreMenuItems() { | ||
return menuItems; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
let registerMoreMenuItem, getMoreMenuItems, registerSidebar; | ||
function requireAll() { | ||
jest.resetModules(); | ||
const sidebar = require( '../sidebar' ); | ||
registerSidebar = sidebar.registerSidebar; | ||
const moreMenuItem = require( '../more-menu-item' ); | ||
registerMoreMenuItem = moreMenuItem.registerMoreMenuItem; | ||
getMoreMenuItems = moreMenuItem.getMoreMenuItems; | ||
} | ||
|
||
requireAll(); | ||
|
||
describe( 'registerMoreMenuItem', () => { | ||
beforeEach( requireAll ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every test suite is executed in isolation. There is only one test in this file, so it is totally fine to import all dependencies using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added another test, because I do believe that if we would add more tests it's important that they are executed in a clean context. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth pointing out that resetting the Node There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand, ideally I would like only one module to be reset, but I haven't found out how. |
||
|
||
it( 'successfully registers a more menu item', () => { | ||
registerSidebar( 'plugins/sidebar', { | ||
title: 'Plugin Title', | ||
render: () => 'Component', | ||
} ); | ||
registerMoreMenuItem( 'gutenberg/plugin', { | ||
title: 'Plugin', | ||
target: 'gutenberg/plugin', | ||
} ); | ||
expect( getMoreMenuItems()[ 'gutenberg/plugin' ] ).toBeDefined(); | ||
} ); | ||
|
||
it( 'throws an error when a more menu item with the same id already exists', () => { | ||
registerSidebar( 'plugins/sidebar', { | ||
title: 'Plugin Title', | ||
render: () => 'Component', | ||
} ); | ||
registerMoreMenuItem( 'gutenberg/plugin', { | ||
title: 'Plugin', | ||
target: 'gutenberg/plugin', | ||
} ); | ||
registerMoreMenuItem( 'gutenberg/plugin', { | ||
title: 'Plugin', | ||
target: 'gutenberg/plugin', | ||
} ); | ||
expect( console ).toHaveErrored(); | ||
} ); | ||
} ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { map, isEmpty } from 'lodash'; | ||
import classnames from 'classnames'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { withInstanceId, IconButton, MenuItemsGroup } from '@wordpress/components'; | ||
import { withSelect } from '@wordpress/data'; | ||
import { compose } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import './style.scss'; | ||
import { getMoreMenuItems } from '../../../api/more-menu-item'; | ||
|
||
/** | ||
* Renders a list of plugins that will activate different UI elements. | ||
* | ||
* @param {Object} props The component props. | ||
* | ||
* @return {Object} The rendered list of menu items. | ||
*/ | ||
function Plugins( props ) { | ||
const ellipsisMenuItems = getMoreMenuItems(); | ||
|
||
if ( isEmpty( ellipsisMenuItems ) ) { | ||
return null; | ||
} | ||
|
||
/** | ||
* Handles the user clicking on one of the plugins in the menu | ||
* | ||
* @param {string} pluginId The plugin id. | ||
* | ||
* @return {void} | ||
*/ | ||
function onSelect( pluginId ) { | ||
props.onSelect(); | ||
ellipsisMenuItems[ pluginId ].callback(); | ||
} | ||
|
||
return ( | ||
<MenuItemsGroup | ||
label={ __( 'Plugins' ) } | ||
filterName="editPost.MoreMenu.plugins" > | ||
{ map( ellipsisMenuItems, menuItem => { | ||
const pluginActive = menuItem.target === props.activePlugin; | ||
|
||
let Icon = menuItem.icon ? ( | ||
<span className="edit-post-plugins__icon-container" > | ||
{ menuItem.icon } | ||
</span> | ||
) : null; | ||
|
||
if ( pluginActive ) { | ||
Icon = 'yes'; | ||
} | ||
|
||
const buttonClassName = classnames( | ||
'edit-post-plugins__button', | ||
{ | ||
'has-icon': Icon, | ||
'is-active': pluginActive, | ||
} | ||
); | ||
|
||
return ( | ||
<IconButton | ||
key={ menuItem.menuItemId } | ||
className={ buttonClassName } | ||
icon={ Icon } | ||
onClick={ () => onSelect( menuItem.menuItemId ) }> | ||
{ menuItem.title } | ||
</IconButton> | ||
); | ||
} ) } | ||
</MenuItemsGroup> | ||
); | ||
} | ||
|
||
export default compose( [ | ||
withSelect( select => { | ||
const editPost = select( 'core/edit-post' ); | ||
const openedSidebar = editPost.getOpenedGeneralSidebar(); | ||
if ( openedSidebar !== 'plugin' ) { | ||
return {}; | ||
} | ||
|
||
return { | ||
activePlugin: editPost.getActivePlugin(), | ||
}; | ||
} ), | ||
withInstanceId, | ||
] )( Plugins ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
.edit-post-plugins__button, | ||
.edit-post-plugins__button.components-icon-button { | ||
width: 100%; | ||
padding: 8px 0; | ||
padding-left: 25px; | ||
text-align: left; | ||
color: $dark-gray-500; | ||
|
||
.dashicon { | ||
margin-right: 5px; | ||
} | ||
|
||
&.has-icon { | ||
padding-left: 0; | ||
} | ||
|
||
&:hover { | ||
color: $black; | ||
} | ||
} | ||
|
||
.edit-post-plugins__icon-container { | ||
margin-right: 5px; | ||
width: 20px; | ||
height: 20px; | ||
overflow: hidden; | ||
|
||
svg { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we know there's an SVG ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See previous comment |
||
width: 100%; | ||
height: 100%; | ||
} | ||
} |
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.
Empty dependency set can be omitted.
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