Skip to content
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

Extensibility: Plugin sidebar api #5430

Merged
merged 88 commits into from
Mar 19, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
142f7b1
Initial implementation of slot-fill sidebar API
Mar 2, 2018
65af7e6
Attempt to implement createContext api for plugin slot fill API
Mar 6, 2018
0e921fe
Fixed error on start up
Mar 6, 2018
2b022f7
Implemented way to pass props from plugin API via context to PluginSi…
Mar 6, 2018
78623ad
Finalize first pass of registerPlugin API
Mar 6, 2018
0beaa4b
Remove create-react-context
Mar 6, 2018
cbfe6dc
Merge branch 'master' into add/slot-fill-sidebar-api
Mar 7, 2018
75e35ba
Removed old sidebar and refactored the way plugin context is handled
Mar 7, 2018
1ae8632
Improved withPluginContext HOC to prevent usage of cloneElement
Mar 7, 2018
0d3d03d
Implemented tests for registerPlugin
Mar 7, 2018
789e675
Added test for plugin/context components, revamped naming of passed c…
Mar 7, 2018
08f6e7b
Fixed eslint issues
Mar 7, 2018
ffe0870
Refactored plugin-sidebar to use withSelect/withDispatch and created …
Mar 7, 2018
c55eaaf
Removed PropTypes
Mar 7, 2018
a3a08de
Used enzyme instead of react-test-renderer in plugin context tests
Mar 7, 2018
37a41d8
Hidden internal Slot and Fill components in layout
Mar 7, 2018
ab5df97
Moved plugin-sidebar error-boundary to separate file
Mar 7, 2018
8fe79d0
Moved sidebar error boundary around the component only so the error c…
Mar 7, 2018
8c2930c
Updated documentation
Mar 7, 2018
a3fada7
Updated documentation
Mar 7, 2018
83a4dd3
Updated documentation
Mar 8, 2018
908112d
Refactored editpost/api/context component to reparate folder
Mar 8, 2018
913415e
Moved plugin components to api folder and made API experimental
Mar 8, 2018
2dc4a78
Updated docs to reflect experimental status
Mar 8, 2018
a4ce7d2
Updated documentation
Mar 8, 2018
1432bc4
Fixed failing test
Mar 8, 2018
ca63d62
Docs: Add a few tweaks to the Extensibility doc
gziolo Mar 9, 2018
4ac2152
Initial implementation of slot-fill sidebar API
Mar 2, 2018
fe93e2d
Attempt to implement createContext api for plugin slot fill API
Mar 6, 2018
0a6ee55
Fixed error on start up
Mar 6, 2018
499cb71
Implemented way to pass props from plugin API via context to PluginSi…
Mar 6, 2018
fde16b5
Finalize first pass of registerPlugin API
Mar 6, 2018
a268de4
Remove create-react-context
Mar 6, 2018
8bfcbee
Removed old sidebar and refactored the way plugin context is handled
Mar 7, 2018
ef02a0c
Improved withPluginContext HOC to prevent usage of cloneElement
Mar 7, 2018
c2e0f20
Implemented tests for registerPlugin
Mar 7, 2018
528bcdc
Added test for plugin/context components, revamped naming of passed c…
Mar 7, 2018
893dff8
Fixed eslint issues
Mar 7, 2018
86049a9
Refactored plugin-sidebar to use withSelect/withDispatch and created …
Mar 7, 2018
8d00bf4
Removed PropTypes
Mar 7, 2018
5b22af7
Used enzyme instead of react-test-renderer in plugin context tests
Mar 7, 2018
bb552ef
Hidden internal Slot and Fill components in layout
Mar 7, 2018
d4da2c5
Moved plugin-sidebar error-boundary to separate file
Mar 7, 2018
b110d70
Moved sidebar error boundary around the component only so the error c…
Mar 7, 2018
b4dfc8e
Updated documentation
Mar 7, 2018
c173637
Updated documentation
Mar 7, 2018
47ee199
Updated documentation
Mar 8, 2018
d56a58d
Refactored editpost/api/context component to reparate folder
Mar 8, 2018
646f72c
Moved plugin components to api folder and made API experimental
Mar 8, 2018
60d174c
Updated docs to reflect experimental status
Mar 8, 2018
42652a5
Updated documentation
Mar 8, 2018
210d0ab
Fixed failing test
Mar 8, 2018
f082bf0
Docs: Add a few tweaks to the Extensibility doc
gziolo Mar 9, 2018
b629a85
Rebase master
Mar 12, 2018
c871e8b
Merged remote changes
Mar 12, 2018
a960db8
Merge branch 'master' of https://github.com/WordPress/gutenberg into …
Mar 12, 2018
1f6b5f5
Register UI components in plugin.js
Mar 13, 2018
295d410
Updated tests
Mar 13, 2018
474ff55
created PluginRegistry singleton
Mar 13, 2018
b809684
Updated JSDoc
Mar 13, 2018
20eb71a
Made name first parameter of registerPlugin function instead of being…
Mar 13, 2018
688280c
Small documentation tweaks
Mar 13, 2018
d25b07d
Moved and documented plugin context provider and higher order component
Mar 14, 2018
4d5209a
Updated test
Mar 14, 2018
db3c98c
Fixed sass class names
Mar 14, 2018
b2526da
Made scss specific for plugin0-sidebar which nog longer depends on ed…
Mar 14, 2018
d3e038f
Moved PLuginsArea to separate file
Mar 14, 2018
eed00e2
Updated test
Mar 14, 2018
995d7de
Moved plugin api to plugins module
Mar 14, 2018
e706a7a
Updated documentation
Mar 14, 2018
dc717f8
Converted spaces to tabs in style.scss file
Mar 14, 2018
644b7e8
Comply to coding style conventions regarding css in plugin-sidebar
Mar 14, 2018
02a5924
Added slot to pluginsidebar fill for consistency
Mar 14, 2018
687e25c
Moved styles.scss under Internal dependencies
Mar 14, 2018
5f8beaa
removed unnecessary id from hidden div for plugin fills
Mar 14, 2018
b435d4b
Removed getPluginUIComponent API function in favor of 'plugin-sidebar…
Mar 15, 2018
90d0af8
Don't throw error on invalid PluginSidebar props
Mar 15, 2018
6540b05
Plugins: Refactor plugin UI registration as Slot/Fill
aduth Mar 15, 2018
f127727
Plugins: Remove unused code
aduth Mar 15, 2018
cdc51c5
Plugins: Represent plugin render as the component it is
aduth Mar 15, 2018
5488d06
Plugins: Revert plugin sidebar slot name to PluginSidebar
aduth Mar 15, 2018
9ee20f1
Plugins: Remove unused error boundary styles
aduth Mar 16, 2018
ce9b374
Plugin: Remove unused sidebar settings
aduth Mar 16, 2018
a322b70
Fix plugin sidebar with on mobile
atimmer Mar 16, 2018
d751d50
Merge branch 'master' into add/slot-fill-sidebar-api
atimmer Mar 16, 2018
10df23c
Merge branch 'master' into add/slot-fill-sidebar-api
atimmer Mar 16, 2018
853655c
Documentation: Remove experimental flag from plugins
aduth Mar 16, 2018
a1de97b
Testing: Include plugins folder in ESLint import rule
aduth Mar 16, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion edit-post/components/layout/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function Layout( {
mode,
editorSidebarOpened,
pluginSidebarOpened,
sidebarName,
publishSidebarOpened,
hasFixedToolbar,
closePublishSidebar,
Expand Down Expand Up @@ -82,7 +83,7 @@ function Layout( {
/>
) }
{ editorSidebarOpened && <Sidebar /> }
{ pluginSidebarOpened && <PluginSidebar.Slot /> }
{ pluginSidebarOpened && <PluginSidebar.Slot name={ sidebarName } /> }
<Popover.Slot />
<PluginArea />
</div>
Expand All @@ -94,6 +95,7 @@ export default compose(
mode: select( 'core/edit-post' ).getEditorMode(),
editorSidebarOpened: select( 'core/edit-post' ).isEditorSidebarOpened(),
pluginSidebarOpened: select( 'core/edit-post' ).isPluginSidebarOpened(),
sidebarName: select( 'core/edit-post' ).getActiveGeneralSidebarName(),
publishSidebarOpened: select( 'core/edit-post' ).isPublishSidebarOpened(),
hasFixedToolbar: select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ),
metaBoxes: select( 'core/edit-post' ).getMetaBoxes(),
Expand Down
99 changes: 23 additions & 76 deletions edit-post/components/plugin-sidebar/index.js
Original file line number Diff line number Diff line change
@@ -1,110 +1,57 @@
/**
* WordPress dependencies
*/
import { Component, Children, cloneElement, compose } from '@wordpress/element';
import { Slot, Fill, withFocusReturn } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
import { withPluginContext } from '@wordpress/plugins';
import { compose } from '@wordpress/element';
import { Slot, Fill, withFocusReturn, withContext } from '@wordpress/components';
import { withDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import './style.scss';
import SidebarLayout from './sidebar-layout';
import ErrorBoundary from './sidebar-error-boundary';

/**
* Name of slot in which the sidebar should fill.
*
* @type {String}
*/
const SLOT_NAME = 'PluginSidebar';
const SLOT_NAME = 'plugin-sidebar';

/**
* The plugin sidebar fill.
* Renders the plugin sidebar fill.
*
* @return {WPElement} Plugin sidebar fill.
*/
class PluginSidebar extends Component {
constructor( props ) {
super( props );

this.state = { invalid: false };

if ( typeof props.name !== 'string' ) {
// eslint-disable-next-line no-console
console.error( 'PluginSidebar name should be of type string' );
this.state.invalid = true;
}
if ( ! /^[a-z][a-z0-9-]*$/.test( props.name ) ) {
// eslint-disable-next-line no-console
console.error( 'Sidebar names must include only lowercase alphanumeric characters or dashes,' +
' and start with a letter. Example: "my-sidebar".' );
this.state.invalid = true;
}
}

/**
* Generates the UI plugin identifier by combining the plugin name and the sidebar name.
*
* Also registers the plugin in the plugin registry.
*/
componentWillMount() {
if ( ! this.namespacedName ) {
this.namespacedName = `plugin-sidebar/${ this.props.pluginContext.namespace }/${ this.props.name }`;
}
}

/**
* Renders the PluginSidebar component.
*
* @return {ReactElement} The rendered component.
*/
render() {
if ( this.props.activePlugin !== this.namespacedName || this.state.invalid ) {
return null;
}

const { children, ...props } = this.props;
const newProps = {
...props,
namespacedName: this.namespacedName,
};

return (
<Fill name={ SLOT_NAME }>
<SidebarLayout
title={ props.title }
onClose={ props.onClose } >
<ErrorBoundary pluginName={ this.namespacedName }>
{ typeof children === 'string' ? children : cloneElement( Children.only( children ), newProps ) }
</ErrorBoundary>
</SidebarLayout>
</Fill>
);
}
function PluginSidebar( { pluginName, name, title, onClose, children } ) {
return (
<Fill name={ [ SLOT_NAME, pluginName, name ].join( '/' ) }>
<SidebarLayout
title={ title }
onClose={ onClose } >
{ children }
</SidebarLayout>
</Fill>
);
}

const WrappedPluginSidebar = compose( [
withSelect( select => {
return {
activePlugin: select( 'core/edit-post' ).getActiveGeneralSidebarName(),
};
} ),
PluginSidebar = compose( [
withDispatch( dispatch => {
return {
onClose: dispatch( 'core/edit-post' ).closeGeneralSidebar,
};
} ),
withFocusReturn,
withPluginContext,
withContext( 'pluginName' )(),
] )( PluginSidebar );

/**
* The plugin sidebar slot.
*
* @return {ReactElement} The plugin sidebar slot.
* @return {WPElement} The plugin sidebar slot.
*/
WrappedPluginSidebar.Slot = () => (
<Slot name={ SLOT_NAME } />
PluginSidebar.Slot = ( { name } ) => (
<Slot name={ [ SLOT_NAME, name ].join( '/' ) } />
);

export default WrappedPluginSidebar;
export default PluginSidebar;
15 changes: 3 additions & 12 deletions edit-post/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,8 @@ export function isEditorSidebarOpened( state ) {
* @return {boolean} Whether the plugin sidebar is opened.
*/
export function isPluginSidebarOpened( state ) {
const activeGeneralSidebar = getPreference( state, 'activeGeneralSidebar', null );

if ( ! activeGeneralSidebar || typeof activeGeneralSidebar !== 'string' ) {
return false;
}

return activeGeneralSidebar.startsWith( 'plugin-sidebar/' );
const activeGeneralSidebar = getActiveGeneralSidebarName( state );
return !! activeGeneralSidebar && ! isEditorSidebarOpened( state );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually what I was trying to prevent because it doesn't allow any more sidebar types. But I guess that's a concern for later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the nice thing about a selector is that it abstracts away these underlying implementation-detail concerns.

}

/**
Expand All @@ -51,11 +46,7 @@ export function isPluginSidebarOpened( state ) {
* @return {?string} Active general sidebar name.
*/
export function getActiveGeneralSidebarName( state ) {
const activeGeneralSidebar = getPreference( state, 'activeGeneralSidebar', null );

return activeGeneralSidebar && ( isEditorSidebarOpened( state ) || isPluginSidebarOpened( state ) ) ?
activeGeneralSidebar :
null;
return getPreference( state, 'activeGeneralSidebar', null );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ wp.data.dispatch( 'core/edit-post' ).openGeneralSidebar( 'plugin-name/second-sid

### Available UI components

The available UI components are found in the global variable `wp.editPost.__experimental`, and are React components.
The available UI components are found in the global variable `wp.plugins.__experimental`, and are React components.

#### PluginSidebar

Expand Down
111 changes: 101 additions & 10 deletions plugins/api/index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,106 @@
/* eslint no-console: [ 'error', { allow: [ 'error' ] } ] */

/**
* WordPress dependencies
*/
import { applyFilters } from '@wordpress/hooks';

/**
* External dependencies
*/
import { isFunction } from 'lodash';

/**
* Plugin definitions keyed by plugin name.
*
* @type {Object.<string,WPPlugin>}
*/
const plugins = {};

/**
* Internal dependencies
* Registers a plugin to the editor.
*
* @param {string} name The name of the plugin.
* @param {Object} settings The settings for this plugin.
* @param {Function} settings.render The function that renders the plugin.
*
* @return {Object} The final plugin settings object.
*/
import PluginRegistry from './plugin';
export function registerPlugin( name, settings ) {
if ( typeof settings !== 'object' ) {
console.error(
'No settings object provided!'
);
return null;
}
if ( typeof name !== 'string' ) {
console.error(
'Plugin names must be strings.'
);
return null;
}
if ( ! /^[a-z][a-z0-9-]*$/.test( name ) ) {
console.error(
'Plugin names must include only lowercase alphanumeric characters or dashes, and start with a letter. Example: "my-plugin".'
);
return null;
}
if ( plugins[ name ] ) {
console.error(
`Plugin "${ name }" is already registered.`
);
}
if ( ! isFunction( settings.render ) ) {
console.error(
'The "render" property must be specified and must be a valid function.'
);
return null;
}

const registry = PluginRegistry.getInstance();
settings.name = name;
settings.sidebar = {};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't do anything


const __experimental = {
registerPlugin: registry.registerPlugin,
getRegisteredUIComponent: registry.getRegisteredUIComponent,
};
settings = applyFilters( 'plugins.registerPlugin', settings, name );

export {
__experimental,
};
return plugins[ settings.name ] = settings;
}

/**
* Unregisters a plugin by name.
*
* @param {string} name Plugin name.
*
* @return {?WPPlugin} The previous plugin settings object, if it has been
* successfully unregistered; otherwise `undefined`.
*/
export function unregisterPlugin( name ) {
if ( ! plugins[ name ] ) {
console.error(
'Plugin "' + name + '" is not registered.'
);
return;
}
const oldPlugin = plugins[ name ];
delete plugins[ name ];
return oldPlugin;
}

/**
* Returns a registered plugin settings.
*
* @param {string} name Plugin name.
*
* @return {?Object} Plugin setting.
*/
export function getPlugin( name ) {
return plugins[ name ];
}

/**
* Returns all registered plugins.
*
* @return {Array} Plugin settings.
*/
export function getPlugins() {
return Object.values( plugins );
}
Loading