diff --git a/edit-post/api/index.js b/edit-post/api/index.js new file mode 100644 index 0000000000000..1252f8010ec3f --- /dev/null +++ b/edit-post/api/index.js @@ -0,0 +1,4 @@ +export { + registerSidebar, + activateSidebar, +} from './sidebar'; diff --git a/edit-post/api/sidebar.js b/edit-post/api/sidebar.js new file mode 100644 index 0000000000000..b2d52152d421e --- /dev/null +++ b/edit-post/api/sidebar.js @@ -0,0 +1,98 @@ +/* eslint no-console: [ 'error', { allow: [ 'error' ] } ] */ + +/* External dependencies */ +import { isFunction } from 'lodash'; + +/* Internal dependencies */ +import store from '../store'; +import { setGeneralSidebarActivePanel, openGeneralSidebar } from '../store/actions'; +import { applyFilters } from '@wordpress/hooks'; + +const sidebars = {}; + +/** + * Registers a sidebar to the editor. + * + * A button will be shown in the settings menu to open the sidebar. The sidebar + * can be manually opened by calling the `activateSidebar` function. + * + * @param {string} name The name of the sidebar. Should be in + * `[plugin]/[sidebar]` format. + * @param {Object} settings The settings for this sidebar. + * @param {string} settings.title The name to show in the settings menu. + * @param {Function} settings.render The function that renders the sidebar. + * + * @return {Object} The final sidebar settings object. + */ +export function registerSidebar( name, settings ) { + settings = { + name, + ...settings, + }; + + if ( typeof name !== 'string' ) { + console.error( + 'Sidebar names must be strings.' + ); + return null; + } + if ( ! /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/.test( name ) ) { + console.error( + 'Sidebar names must contain a namespace prefix, include only lowercase alphanumeric characters or dashes, and start with a letter. Example: my-plugin/my-custom-sidebar.' + ); + return null; + } + if ( ! settings || ! isFunction( settings.render ) ) { + console.error( + 'The "render" property must be specified and must be a valid function.' + ); + return null; + } + if ( sidebars[ name ] ) { + console.error( + `Sidebar ${ name } is already registered.` + ); + } + + if ( ! settings.title ) { + console.error( + `The sidebar ${ name } must have a title.` + ); + return null; + } + if ( typeof settings.title !== 'string' ) { + console.error( + 'Sidebar titles must be strings.' + ); + return null; + } + + settings = applyFilters( 'editor.registerSidebar', settings, name ); + + return sidebars[ name ] = settings; +} + +/** + * Retrieves the sidebar settings object. + * + * @param {string} name The name of the sidebar to retrieve the settings for. + * + * @return {Object} The settings object of the sidebar. Or null if the + * sidebar doesn't exist. + */ +export function getSidebarSettings( name ) { + if ( ! sidebars.hasOwnProperty( name ) ) { + return null; + } + return sidebars[ name ]; +} +/** + * Activates the given sidebar. + * + * @param {string} name The name of the sidebar to activate. + * @return {void} + */ +export function activateSidebar( name ) { + store.dispatch( openGeneralSidebar( 'plugin' ) ); + store.dispatch( setGeneralSidebarActivePanel( 'plugin', name ) ); +} diff --git a/edit-post/components/header/index.js b/edit-post/components/header/index.js index a18a25ac01248..91eefd11767c2 100644 --- a/edit-post/components/header/index.js +++ b/edit-post/components/header/index.js @@ -20,15 +20,25 @@ import { import './style.scss'; import EllipsisMenu from './ellipsis-menu'; import HeaderToolbar from './header-toolbar'; -import { isSidebarOpened } from '../../store/selectors'; -import { toggleSidebar } from '../../store/actions'; +import { + getOpenedGeneralSidebar, + isPublishSidebarOpened, +} from '../../store/selectors'; +import { + openGeneralSidebar, + closeGeneralSidebar, + togglePublishSidebar, +} from '../../store/actions'; function Header( { - onToggleDefaultSidebar, + isGeneralSidebarEditorOpen, + onOpenGeneralSidebar, + onCloseGeneralSidebar, + isPublishSidebarOpen, onTogglePublishSidebar, - isDefaultSidebarOpened, - isPublishSidebarOpened, } ) { + const toggleGeneralSidebar = isGeneralSidebarEditorOpen ? onCloseGeneralSidebar : onOpenGeneralSidebar; + return (
- { ! isPublishSidebarOpened && ( + { ! isPublishSidebarOpen && (
@@ -61,12 +71,13 @@ function Header( { export default connect( ( state ) => ( { - isDefaultSidebarOpened: isSidebarOpened( state ), - isPublishSidebarOpened: isSidebarOpened( state, 'publish' ), + isGeneralSidebarEditorOpen: getOpenedGeneralSidebar( state ) === 'editor', + isPublishSidebarOpen: isPublishSidebarOpened( state ), } ), { - onToggleDefaultSidebar: () => toggleSidebar(), - onTogglePublishSidebar: () => toggleSidebar( 'publish' ), + onOpenGeneralSidebar: () => openGeneralSidebar( 'editor' ), + onCloseGeneralSidebar: closeGeneralSidebar, + onTogglePublishSidebar: togglePublishSidebar, }, undefined, { storeKey: 'edit-post' } diff --git a/edit-post/components/layout/index.js b/edit-post/components/layout/index.js index 8bea1893d7a43..6037e50d33251 100644 --- a/edit-post/components/layout/index.js +++ b/edit-post/components/layout/index.js @@ -29,23 +29,41 @@ import VisualEditor from '../modes/visual-editor'; import EditorModeKeyboardShortcuts from '../modes/keyboard-shortcuts'; import { getEditorMode, - hasFixedToolbar, hasOpenSidebar, - isSidebarOpened, + isFeatureActive, + getOpenedGeneralSidebar, + isPublishSidebarOpened, + getActivePlugin, } from '../../store/selectors'; -import { toggleSidebar } from '../../store/actions'; +import { closePublishSidebar } from '../../store/actions'; +import PluginsPanel from '../../components/plugins-panel/index.js'; +import { getSidebarSettings } from '../../api/sidebar'; + +function GeneralSidebar( { openedGeneralSidebar } ) { + switch ( openedGeneralSidebar ) { + case 'editor': + return ; + case 'plugin': + return ; + default: + } + return null; +} function Layout( { mode, layoutHasOpenSidebar, - isDefaultSidebarOpened, - isPublishSidebarOpened, - fixedToolbarActive, - onClosePublishPanel, + publishSidebarOpen, + openedGeneralSidebar, + hasFixedToolbar, + onClosePublishSidebar, + plugin, } ) { + const isSidebarOpened = layoutHasOpenSidebar && + ( openedGeneralSidebar !== 'plugin' || getSidebarSettings( plugin ) ); const className = classnames( 'edit-post-layout', { - 'is-sidebar-opened': layoutHasOpenSidebar, - 'has-fixed-toolbar': fixedToolbarActive, + 'is-sidebar-opened': isSidebarOpened, + 'has-fixed-toolbar': hasFixedToolbar, } ); return ( @@ -68,8 +86,11 @@ function Layout( {
- { isDefaultSidebarOpened && } - { isPublishSidebarOpened && } + { publishSidebarOpen && } + { + openedGeneralSidebar !== null && + } ); @@ -79,12 +100,13 @@ export default connect( ( state ) => ( { mode: getEditorMode( state ), layoutHasOpenSidebar: hasOpenSidebar( state ), - isDefaultSidebarOpened: isSidebarOpened( state ), - isPublishSidebarOpened: isSidebarOpened( state, 'publish' ), - fixedToolbarActive: hasFixedToolbar( state ), + openedGeneralSidebar: getOpenedGeneralSidebar( state ), + publishSidebarOpen: isPublishSidebarOpened( state ), + hasFixedToolbar: isFeatureActive( state, 'fixedToolbar' ), + plugin: getActivePlugin( state ), } ), { - onClosePublishPanel: () => toggleSidebar( 'publish', false ), + onClosePublishSidebar: closePublishSidebar, }, undefined, { storeKey: 'edit-post' } diff --git a/edit-post/components/modes/visual-editor/block-inspector-button.js b/edit-post/components/modes/visual-editor/block-inspector-button.js index 55fb5077cfe6b..dd53a77622644 100644 --- a/edit-post/components/modes/visual-editor/block-inspector-button.js +++ b/edit-post/components/modes/visual-editor/block-inspector-button.js @@ -13,39 +13,31 @@ import { IconButton, withSpokenMessages } from '@wordpress/components'; /** * Internal dependencies */ -import { getActivePanel, isSidebarOpened } from '../../../store/selectors'; -import { toggleSidebar, setActivePanel } from '../../../store/actions'; +import { getActiveEditorPanel, isGeneralSidebarPanelOpened } from '../../../store/selectors'; +import { openGeneralSidebar } from '../../../store/actions'; export function BlockInspectorButton( { - isDefaultSidebarOpened, + isGeneralSidebarEditorOpened, + onOpenGeneralSidebarEditor, panel, - toggleDefaultSidebar, - onShowInspector, onClick = noop, small = false, speak, } ) { - const toggleInspector = () => { - onShowInspector(); - if ( ! isDefaultSidebarOpened || panel === 'block' ) { - toggleDefaultSidebar(); - } - }; - const speakMessage = () => { - if ( ! isDefaultSidebarOpened || ( isDefaultSidebarOpened && panel !== 'block' ) ) { + if ( ! isGeneralSidebarEditorOpened || ( isGeneralSidebarEditorOpened && panel !== 'block' ) ) { speak( __( 'Additional settings are now available in the Editor advanced settings sidebar' ) ); } else { speak( __( 'Advanced settings closed' ) ); } }; - const label = ( isDefaultSidebarOpened && panel === 'block' ) ? __( 'Hide Advanced Settings' ) : __( 'Show Advanced Settings' ); + const label = ( isGeneralSidebarEditorOpened && panel === 'block' ) ? __( 'Hide Advanced Settings' ) : __( 'Show Advanced Settings' ); return ( @@ -56,15 +48,12 @@ export function BlockInspectorButton( { export default connect( ( state ) => ( { - isDefaultSidebarOpened: isSidebarOpened( state ), - panel: getActivePanel( state ), + isGeneralSidebarEditorOpened: isGeneralSidebarPanelOpened( state, 'editor' ), + panel: getActiveEditorPanel( state ), } ), ( dispatch ) => ( { - onShowInspector() { - dispatch( setActivePanel( 'block' ) ); - }, - toggleDefaultSidebar() { - dispatch( toggleSidebar() ); + onOpenGeneralSidebarEditor() { + dispatch( openGeneralSidebar( 'editor', 'block' ) ); }, } ), undefined, diff --git a/edit-post/components/plugins-panel/index.js b/edit-post/components/plugins-panel/index.js new file mode 100644 index 0000000000000..57be1622457e9 --- /dev/null +++ b/edit-post/components/plugins-panel/index.js @@ -0,0 +1,63 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton, withFocusReturn } from '@wordpress/components'; + +/** + * Internal Dependencies + */ +import './style.scss'; +import { getSidebarSettings } from '../../api/sidebar'; +import { getActivePlugin } from '../../store/selectors'; +import { closeGeneralSidebar } from '../../store/actions'; + +function PluginsPanel( { onClose, plugin } ) { + const pluginSidebar = getSidebarSettings( plugin ); + + if ( ! pluginSidebar ) { + return null; + } + + const { + title, + render, + } = pluginSidebar; + + return ( +
+
+

{ title }

+ +
+
+ { render() } +
+
+ ); +} + +export default connect( + ( state ) => { + return { + plugin: getActivePlugin( state ), + }; + }, { + onClose: closeGeneralSidebar, + }, + undefined, + { storeKey: 'edit-post' } +)( withFocusReturn( PluginsPanel ) ); diff --git a/edit-post/components/plugins-panel/style.scss b/edit-post/components/plugins-panel/style.scss new file mode 100644 index 0000000000000..9a1e1dfb5a4f8 --- /dev/null +++ b/edit-post/components/plugins-panel/style.scss @@ -0,0 +1,21 @@ +.edit-post-layout { + .edit-post-plugins-panel__header { + padding: 0 8px 0 16px; + height: $panel-header-height; + border-bottom: 1px solid $light-gray-500; + display: flex; + align-items: center; + + .components-icon-button { + margin-left: auto; + } + + h3 { + margin: 0; + font-weight: 400; + color: inherit; + } + } +} + + diff --git a/edit-post/components/sidebar/discussion-panel/index.js b/edit-post/components/sidebar/discussion-panel/index.js index d20c0f012d8a4..44a05155c8f76 100644 --- a/edit-post/components/sidebar/discussion-panel/index.js +++ b/edit-post/components/sidebar/discussion-panel/index.js @@ -14,7 +14,7 @@ import { PostComments, PostPingbacks, PostTypeSupportCheck } from '@wordpress/ed * Internal Dependencies */ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -49,7 +49,7 @@ export default connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/document-outline-panel/index.js b/edit-post/components/sidebar/document-outline-panel/index.js index 19ed39a7688af..c99be78806ba5 100644 --- a/edit-post/components/sidebar/document-outline-panel/index.js +++ b/edit-post/components/sidebar/document-outline-panel/index.js @@ -14,7 +14,7 @@ import { DocumentOutline, DocumentOutlineCheck } from '@wordpress/editor'; * Internal dependencies */ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module constants @@ -39,7 +39,7 @@ export default connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/featured-image/index.js b/edit-post/components/sidebar/featured-image/index.js index 9263b8614aa3d..2a16b4681d0ed 100644 --- a/edit-post/components/sidebar/featured-image/index.js +++ b/edit-post/components/sidebar/featured-image/index.js @@ -17,7 +17,7 @@ import { query } from '@wordpress/data'; * Internal dependencies */ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -54,7 +54,7 @@ const applyConnect = connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/header.js b/edit-post/components/sidebar/header.js index 62201a9952a7b..17d60605ed5bd 100644 --- a/edit-post/components/sidebar/header.js +++ b/edit-post/components/sidebar/header.js @@ -14,13 +14,12 @@ import { query } from '@wordpress/data'; /** * Internal Dependencies */ -import { getActivePanel } from '../../store/selectors'; -import { toggleSidebar, setActivePanel } from '../../store/actions'; +import { getActiveEditorPanel } from '../../store/selectors'; +import { closeGeneralSidebar, setGeneralSidebarActivePanel } from '../../store/actions'; -const SidebarHeader = ( { panel, onSetPanel, onToggleSidebar, count } ) => { +const SidebarHeader = ( { panel, onSetPanel, onCloseSidebar, count } ) => { // Do not display "0 Blocks". count = count === 0 ? 1 : count; - const closeSidebar = () => onToggleSidebar( undefined, false ); return (
@@ -39,7 +38,7 @@ const SidebarHeader = ( { panel, onSetPanel, onToggleSidebar, count } ) => { { sprintf( _n( 'Block', '%d Blocks', count ), count ) } @@ -53,11 +52,11 @@ export default compose( } ) ), connect( ( state ) => ( { - panel: getActivePanel( state ), + panel: getActiveEditorPanel( state ), } ), { - onSetPanel: setActivePanel, - onToggleSidebar: toggleSidebar, + onSetPanel: setGeneralSidebarActivePanel.bind( null, 'editor' ), + onCloseSidebar: closeGeneralSidebar, }, undefined, { storeKey: 'edit-post' } diff --git a/edit-post/components/sidebar/index.js b/edit-post/components/sidebar/index.js index 7c046e7d1359d..6a696b443b5fd 100644 --- a/edit-post/components/sidebar/index.js +++ b/edit-post/components/sidebar/index.js @@ -16,10 +16,40 @@ import './style.scss'; import PostSettings from './post-settings'; import BlockInspectorPanel from './block-inspector-panel'; import Header from './header'; +import { getActiveEditorPanel } from '../../store/selectors'; -import { getActivePanel } from '../../store/selectors'; +/** + * Returns the panel that should be rendered in the sidebar. + * + * @param {string} panel The currently active panel. + * + * @return {Object} The React element to render as a panel. + */ +function getPanel( panel ) { + switch ( panel ) { + case 'document': + return PostSettings; + case 'block': + return BlockInspectorPanel; + default: + return PostSettings; + } +} +/** + * Renders a sidebar with the relevant panel. + * + * @param {string} panel The currently active panel. + * + * @return {Object} The rendered sidebar. + */ const Sidebar = ( { panel } ) => { + const ActivePanel = getPanel( panel ); + + const props = { + panel, + }; + return (
{ tabIndex="-1" >
- { panel === 'document' && } - { panel === 'block' && } +
); }; @@ -37,7 +66,7 @@ const Sidebar = ( { panel } ) => { export default connect( ( state ) => { return { - panel: getActivePanel( state ), + panel: getActiveEditorPanel( state ), }; }, undefined, diff --git a/edit-post/components/sidebar/page-attributes/index.js b/edit-post/components/sidebar/page-attributes/index.js index d20573e144188..7f1afc5fa4d90 100644 --- a/edit-post/components/sidebar/page-attributes/index.js +++ b/edit-post/components/sidebar/page-attributes/index.js @@ -16,7 +16,7 @@ import { query } from '@wordpress/data'; /** * Internal dependencies */ -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; import { isEditorSidebarPanelOpened } from '../../../store/selectors'; /** @@ -57,7 +57,7 @@ const applyConnect = connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/post-excerpt/index.js b/edit-post/components/sidebar/post-excerpt/index.js index b7ecc0f392622..cf46d944e2c54 100644 --- a/edit-post/components/sidebar/post-excerpt/index.js +++ b/edit-post/components/sidebar/post-excerpt/index.js @@ -14,7 +14,7 @@ import { PostExcerpt as PostExcerptForm, PostExcerptCheck } from '@wordpress/edi * Internal Dependencies */ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -39,7 +39,7 @@ export default connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/post-status/index.js b/edit-post/components/sidebar/post-status/index.js index f8051e6ab3445..dbdf0a7056e13 100644 --- a/edit-post/components/sidebar/post-status/index.js +++ b/edit-post/components/sidebar/post-status/index.js @@ -23,7 +23,7 @@ import PostPendingStatus from '../post-pending-status'; import { isEditorSidebarPanelOpened, } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -50,7 +50,7 @@ export default connect( } ), { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/post-taxonomies/index.js b/edit-post/components/sidebar/post-taxonomies/index.js index 995197bb250f8..aebae97031f71 100644 --- a/edit-post/components/sidebar/post-taxonomies/index.js +++ b/edit-post/components/sidebar/post-taxonomies/index.js @@ -14,7 +14,7 @@ import { PostTaxonomies as PostTaxonomiesForm, PostTaxonomiesCheck } from '@word * Internal dependencies */ import { isEditorSidebarPanelOpened } from '../../../store/selectors'; -import { toggleSidebarPanel } from '../../../store/actions'; +import { toggleGeneralSidebarEditorPanel } from '../../../store/actions'; /** * Module Constants @@ -43,7 +43,7 @@ export default connect( }, { onTogglePanel() { - return toggleSidebarPanel( PANEL_NAME ); + return toggleGeneralSidebarEditorPanel( PANEL_NAME ); }, }, undefined, diff --git a/edit-post/components/sidebar/style.scss b/edit-post/components/sidebar/style.scss index d7de73101db4e..e1c118f570741 100644 --- a/edit-post/components/sidebar/style.scss +++ b/edit-post/components/sidebar/style.scss @@ -29,14 +29,12 @@ overflow: auto; -webkit-overflow-scrolling: touch; height: 100%; - padding-top: $panel-header-height; margin-top: -1px; margin-bottom: -1px; @include break-small() { overflow: inherit; height: auto; - padding-top: 0; } } @@ -90,13 +88,16 @@ } } -.edit-post-layout.is-sidebar-opened .edit-post-sidebar { - /* Sidebar covers screen on mobile */ - width: 100%; +.edit-post-layout.is-sidebar-opened { + .edit-post-sidebar, + .edit-post-plugins-panel { + /* Sidebar covers screen on mobile */ + width: 100%; - /* Sidebar sits on the side on larger breakpoints */ - @include break-medium() { - width: $sidebar-width; + /* Sidebar sits on the side on larger breakpoints */ + @include break-medium() { + width: $sidebar-width; + } } } diff --git a/edit-post/index.js b/edit-post/index.js index 3fb2777fe1bc0..3a01e5532c82c 100644 --- a/edit-post/index.js +++ b/edit-post/index.js @@ -19,6 +19,8 @@ import './assets/stylesheets/main.scss'; import Layout from './components/layout'; import store from './store'; +export * from './api'; + // Configure moment globally moment.locale( dateSettings.l10n.locale ); if ( dateSettings.timezone.string ) { diff --git a/edit-post/store/actions.js b/edit-post/store/actions.js index 0e61b4a900cae..4bdd38952d7d9 100644 --- a/edit-post/store/actions.js +++ b/edit-post/store/actions.js @@ -1,52 +1,107 @@ /** - * Returns an action object used in signalling that the user toggled the - * sidebar. - * - * @param {string} sidebar Name of the sidebar to toggle - * (desktop, mobile or publish). - * @param {boolean?} forcedValue Force a sidebar state. + * Returns an action object used in signalling that the user switched the active + * sidebar tab panel. * - * @return {Object} Action object. + * @param {string} sidebar Sidebar name + * @param {string} panel Panel name + * @return {Object} Action object */ -export function toggleSidebar( sidebar, forcedValue ) { +export function setGeneralSidebarActivePanel( sidebar, panel ) { return { - type: 'TOGGLE_SIDEBAR', + type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', sidebar, - forcedValue, + panel, }; } /** - * Returns an action object used in signalling that the user switched the active - * sidebar tab panel. + * Returns an action object used in signalling that the user opened a sidebar. * - * @param {string} panel The panel name. + * @param {string} sidebar Sidebar to open. + * @param {string} [panel = null] Panel to open in the sidebar. Null if unchanged. + * @return {Object} Action object. + */ +export function openGeneralSidebar( sidebar, panel = null ) { + return { + type: 'OPEN_GENERAL_SIDEBAR', + sidebar, + panel, + }; +} + +/** + * Returns an action object signalling that the user closed the sidebar. * * @return {Object} Action object. */ -export function setActivePanel( panel ) { +export function closeGeneralSidebar() { return { - type: 'SET_ACTIVE_PANEL', - panel, + type: 'CLOSE_GENERAL_SIDEBAR', }; } /** - * Returns an action object used in signalling that the user toggled a - * sidebar panel. + * Returns an action object used in signalling that the user opened the publish + * sidebar. * - * @param {string} panel The panel name. + * @return {Object} Action object + */ +export function openPublishSidebar() { + return { + type: 'OPEN_PUBLISH_SIDEBAR', + }; +} + +/** + * Returns an action object used in signalling that the user closed the + * publish sidebar. * * @return {Object} Action object. */ -export function toggleSidebarPanel( panel ) { +export function closePublishSidebar() { + return { + type: 'CLOSE_PUBLISH_SIDEBAR', + }; +} + +/** + * Returns an action object used in signalling that the user toggles the publish sidebar + * + * @return {Object} Action object + */ +export function togglePublishSidebar() { return { - type: 'TOGGLE_SIDEBAR_PANEL', + type: 'TOGGLE_PUBLISH_SIDEBAR', + }; +} + +/** + * Returns an action object used in signalling that use toggled a panel in the editor. + * + * @param {string} panel The panel to toggle. + * @return {Object} Action object. +*/ +export function toggleGeneralSidebarEditorPanel( panel ) { + return { + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel, }; } +/** + * Returns an action object used in signalling that the viewport type preference should be set. + * + * @param {string} viewportType The viewport type (desktop or mobile). + * @return {Object} Action object. + */ +export function setViewportType( viewportType ) { + return { + type: 'SET_VIEWPORT_TYPE', + viewportType, + }; +} + /** * Returns an action object used to toggle a feature flag. * diff --git a/edit-post/store/defaults.js b/edit-post/store/defaults.js index bae11caa8e7b8..bb5277c4efb79 100644 --- a/edit-post/store/defaults.js +++ b/edit-post/store/defaults.js @@ -1,9 +1,10 @@ export const PREFERENCES_DEFAULTS = { - mode: 'visual', - sidebars: { - desktop: true, - mobile: false, - publish: false, + editorMode: 'visual', + viewportType: 'desktop', // 'desktop' | 'mobile' + activeGeneralSidebar: 'editor', // null | 'editor' | 'plugin' + activeSidebarPanel: { // The keys in this object should match activeSidebarPanel values + editor: null, // 'document' | 'block' + plugin: null, // pluginId }, panels: { 'post-status': true }, features: { diff --git a/edit-post/store/index.js b/edit-post/store/index.js index 3dfc4b9691663..3673b09241e74 100644 --- a/edit-post/store/index.js +++ b/edit-post/store/index.js @@ -8,7 +8,6 @@ import { registerReducer, withRehydratation, loadAndPersist } from '@wordpress/d */ import reducer from './reducer'; import enhanceWithBrowserSize from './mobile'; -import applyMiddlewares from './middlewares'; import { BREAK_MEDIUM } from './constants'; /** @@ -17,9 +16,8 @@ import { BREAK_MEDIUM } from './constants'; const STORAGE_KEY = `WP_EDIT_POST_PREFERENCES_${ window.userSettings.uid }`; const MODULE_KEY = 'core/edit-post'; -const store = applyMiddlewares( - registerReducer( MODULE_KEY, withRehydratation( reducer, 'preferences', STORAGE_KEY ) ) -); +const store = registerReducer( MODULE_KEY, withRehydratation( reducer, 'preferences', STORAGE_KEY ) ); + loadAndPersist( store, reducer, 'preferences', STORAGE_KEY ); enhanceWithBrowserSize( store, BREAK_MEDIUM ); diff --git a/edit-post/store/reducer.js b/edit-post/store/reducer.js index 8ab871fbc97b0..40b3f4a6d03ad 100644 --- a/edit-post/store/reducer.js +++ b/edit-post/store/reducer.js @@ -22,15 +22,30 @@ import { PREFERENCES_DEFAULTS } from './defaults'; */ export function preferences( state = PREFERENCES_DEFAULTS, action ) { switch ( action.type ) { - case 'TOGGLE_SIDEBAR': + case 'OPEN_GENERAL_SIDEBAR': + const activeSidebarPanel = action.panel ? action.panel : state.activeSidebarPanel[ action.sidebar ]; return { ...state, - sidebars: { - ...state.sidebars, - [ action.sidebar ]: action.forcedValue !== undefined ? action.forcedValue : ! state.sidebars[ action.sidebar ], + activeGeneralSidebar: action.sidebar, + activeSidebarPanel: { + ...state.activeSidebarPanel, + [ action.sidebar ]: activeSidebarPanel, }, }; - case 'TOGGLE_SIDEBAR_PANEL': + case 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL': + return { + ...state, + activeSidebarPanel: { + ...state.activeSidebarPanel, + [ action.sidebar ]: action.panel, + }, + }; + case 'CLOSE_GENERAL_SIDEBAR': + return { + ...state, + activeGeneralSidebar: null, + }; + case 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL': return { ...state, panels: { @@ -38,10 +53,27 @@ export function preferences( state = PREFERENCES_DEFAULTS, action ) { [ action.panel ]: ! get( state, [ 'panels', action.panel ], false ), }, }; + case 'SET_VIEWPORT_TYPE': + return { + ...state, + viewportType: action.viewportType, + }; + case 'UPDATE_MOBILE_STATE': + if ( action.isMobile ) { + return { + ...state, + viewportType: 'mobile', + activeGeneralSidebar: null, + }; + } + return { + ...state, + viewportType: 'desktop', + }; case 'SWITCH_MODE': return { ...state, - mode: action.mode, + editorMode: action.mode, }; case 'TOGGLE_FEATURE': return { @@ -67,6 +99,18 @@ export function panel( state = 'document', action ) { return state; } +export function publishSidebarActive( state = false, action ) { + switch ( action.type ) { + case 'OPEN_PUBLISH_SIDEBAR': + return true; + case 'CLOSE_PUBLISH_SIDEBAR': + return false; + case 'TOGGLE_PUBLISH_SIDEBAR': + return ! state; + } + return state; +} + export function mobile( state = false, action ) { if ( action.type === 'UPDATE_MOBILE_STATE' ) { return action.isMobile; @@ -77,5 +121,6 @@ export function mobile( state = false, action ) { export default combineReducers( { preferences, panel, + publishSidebarActive, mobile, } ); diff --git a/edit-post/store/selectors.js b/edit-post/store/selectors.js index a1300fe6f0e9b..72b41c87fd8a6 100644 --- a/edit-post/store/selectors.js +++ b/edit-post/store/selectors.js @@ -6,7 +6,7 @@ * @return {string} Editing mode. */ export function getEditorMode( state ) { - return getPreference( state, 'mode', 'visual' ); + return getPreference( state, 'editorMode', 'visual' ); } /** @@ -16,8 +16,18 @@ export function getEditorMode( state ) { * * @return {string} Active sidebar panel. */ -export function getActivePanel( state ) { - return state.panel; +export function getActiveEditorPanel( state ) { + return getPreference( state, 'activeSidebarPanel', {} ).editor; +} + +/** + * Returns the current active plugin for the plugin sidebar. + * + * @param {Object} state Global application state + * @return {string} Active plugin sidebar plugin + */ +export function getActivePlugin( state ) { + return getPreference( state, 'activeSidebarPanel', {} ).plugin; } /** @@ -46,20 +56,37 @@ export function getPreference( state, preferenceKey, defaultValue ) { } /** - * Returns true if the sidebar is open, or false otherwise. + * Returns the opened general sidebar and null if the sidebar is closed. * - * @param {Object} state Global application state. - * @param {string} sidebar Sidebar name (leave undefined for the default sidebar). + * @param {Object} state Global application state. + * @return {string} The opened general sidebar panel. + */ +export function getOpenedGeneralSidebar( state ) { + return getPreference( state, 'activeGeneralSidebar' ); +} + +/** + * Returns true if the panel is open in the currently opened sidebar. * - * @return {boolean} Whether the given sidebar is open. + * @param {Object} state Global application state + * @param {string} sidebar Sidebar name (leave undefined for the default sidebar) + * @param {string} panel Sidebar panel name (leave undefined for the default panel) + * @return {boolean} Whether the given general sidebar panel is open */ -export function isSidebarOpened( state, sidebar ) { - const sidebars = getPreference( state, 'sidebars' ); - if ( sidebar !== undefined ) { - return sidebars[ sidebar ]; - } +export function isGeneralSidebarPanelOpened( state, sidebar, panel ) { + const activeGeneralSidebar = getPreference( state, 'activeGeneralSidebar' ); + const activeSidebarPanel = getPreference( state, 'activeSidebarPanel' ); + return activeGeneralSidebar === sidebar && activeSidebarPanel === panel; +} - return isMobile( state ) ? sidebars.mobile : sidebars.desktop; +/** + * Returns true if the publish sidebar is opened. + * + * @param {Object} state Global application state + * @return {boolean} Whether the publish sidebar is open. + */ +export function isPublishSidebarOpened( state ) { + return state.publishSidebarActive; } /** @@ -70,19 +97,18 @@ export function isSidebarOpened( state, sidebar ) { * @return {boolean} Whether sidebar is open. */ export function hasOpenSidebar( state ) { - const sidebars = getPreference( state, 'sidebars' ); - return isMobile( state ) ? - sidebars.mobile || sidebars.publish : - sidebars.desktop || sidebars.publish; + const generalSidebarOpen = getPreference( state, 'activeGeneralSidebar' ) !== null; + const publishSidebarOpen = state.publishSidebarActive; + + return generalSidebarOpen || publishSidebarOpen; } /** * Returns true if the editor sidebar panel is open, or false otherwise. * - * @param {Object} state Global application state. - * @param {string} panel Sidebar panel name. - * - * @return {boolean} Whether sidebar is open. + * @param {Object} state Global application state. + * @param {string} panel Sidebar panel name. + * @return {boolean} Whether the sidebar panel is open. */ export function isEditorSidebarPanelOpened( state, panel ) { const panels = getPreference( state, 'panels' ); @@ -118,7 +144,7 @@ export function hasFixedToolbar( state ) { * @param {Object} state Global application state. * @param {string} feature Feature slug. * - * @return {booleean} Is active. + * @return {boolean} Is active. */ export function isFeatureActive( state, feature ) { return !! state.preferences.features[ feature ]; diff --git a/edit-post/store/test/actions.js b/edit-post/store/test/actions.js index 013f65e39bc7d..7314d760ac220 100644 --- a/edit-post/store/test/actions.js +++ b/edit-post/store/test/actions.js @@ -2,41 +2,92 @@ * Internal dependencies */ import { - toggleSidebar, - setActivePanel, - toggleSidebarPanel, + setGeneralSidebarActivePanel, + toggleGeneralSidebarEditorPanel, + openGeneralSidebar, + closeGeneralSidebar, + openPublishSidebar, + closePublishSidebar, + togglePublishSidebar, + setViewportType, toggleFeature, } from '../actions'; describe( 'actions', () => { - describe( 'toggleSidebar', () => { - it( 'should return TOGGLE_SIDEBAR action', () => { - expect( toggleSidebar( 'publish', true ) ).toEqual( { - type: 'TOGGLE_SIDEBAR', - sidebar: 'publish', - forcedValue: true, + describe( 'setGeneralSidebarActivePanel', () => { + it( 'should return SET_GENERAL_SIDEBAR_ACTIVE_PANEL action', () => { + expect( setGeneralSidebarActivePanel( 'editor', 'document' ) ).toEqual( { + type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', + sidebar: 'editor', + panel: 'document', } ); } ); } ); - describe( 'setActivePanel', () => { - const panel = 'panelName'; - expect( setActivePanel( panel ) ).toEqual( { - type: 'SET_ACTIVE_PANEL', - panel, + describe( 'openGeneralSidebar', () => { + it( 'should return OPEN_GENERAL_SIDEBAR action', () => { + const sidebar = 'sidebarName'; + const panel = 'panelName'; + expect( openGeneralSidebar( sidebar, panel ) ).toEqual( { + type: 'OPEN_GENERAL_SIDEBAR', + sidebar, + panel, + } ); + } ); + } ); + + describe( 'closeGeneralSidebar', () => { + it( 'should return CLOSE_GENERAL_SIDEBAR action', () => { + expect( closeGeneralSidebar() ).toEqual( { + type: 'CLOSE_GENERAL_SIDEBAR', + } ); + } ); + } ); + + describe( 'openPublishSidebar', () => { + it( 'should return an OPEN_PUBLISH_SIDEBAR action', () => { + expect( openPublishSidebar() ).toEqual( { + type: 'OPEN_PUBLISH_SIDEBAR', + } ); + } ); + } ); + + describe( 'closePublishSidebar', () => { + it( 'should return an CLOSE_PUBLISH_SIDEBAR action', () => { + expect( closePublishSidebar() ).toEqual( { + type: 'CLOSE_PUBLISH_SIDEBAR', + } ); + } ); + } ); + + describe( 'togglePublishSidebar', () => { + it( 'should return an TOGGLE_PUBLISH_SIDEBAR action', () => { + expect( togglePublishSidebar() ).toEqual( { + type: 'TOGGLE_PUBLISH_SIDEBAR', + } ); } ); } ); describe( 'toggleSidebarPanel', () => { - it( 'should return TOGGLE_SIDEBAR_PANEL action', () => { + it( 'should return TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL action', () => { const panel = 'panelName'; - expect( toggleSidebarPanel( panel ) ).toEqual( { - type: 'TOGGLE_SIDEBAR_PANEL', + expect( toggleGeneralSidebarEditorPanel( panel ) ).toEqual( { + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel, } ); } ); } ); + describe( 'setViewportType', () => { + it( 'should return SET_VIEWPORT_TYPE action', () => { + const viewportType = 'mobile'; + expect( setViewportType( viewportType ) ).toEqual( { + type: 'SET_VIEWPORT_TYPE', + viewportType, + } ); + } ); + } ); + describe( 'toggleFeature', () => { it( 'should return TOGGLE_FEATURE action', () => { const feature = 'name'; diff --git a/edit-post/store/test/reducer.js b/edit-post/store/test/reducer.js index 2e829713d2f9f..c0f7e9499f433 100644 --- a/edit-post/store/test/reducer.js +++ b/edit-post/store/test/reducer.js @@ -16,64 +16,42 @@ describe( 'state', () => { const state = preferences( undefined, {} ); expect( state ).toEqual( { - mode: 'visual', - sidebars: { - desktop: true, - mobile: false, - publish: false, + activeGeneralSidebar: 'editor', + activeSidebarPanel: { + editor: null, + plugin: null, }, + editorMode: 'visual', panels: { 'post-status': true }, features: { fixedToolbar: false }, + viewportType: 'desktop', } ); } ); - it( 'should toggle the given sidebar flag', () => { - const state = preferences( deepFreeze( { sidebars: { - mobile: true, - desktop: true, - } } ), { - type: 'TOGGLE_SIDEBAR', - sidebar: 'desktop', - } ); - - expect( state.sidebars ).toEqual( { - mobile: true, - desktop: false, - } ); - } ); - - it( 'should set the sidebar open flag to true if unset', () => { - const state = preferences( deepFreeze( { sidebars: { - mobile: true, - } } ), { - type: 'TOGGLE_SIDEBAR', - sidebar: 'desktop', - } ); - - expect( state.sidebars ).toEqual( { - mobile: true, - desktop: true, - } ); - } ); - - it( 'should force the given sidebar flag', () => { - const state = preferences( deepFreeze( { sidebars: { - mobile: true, - } } ), { - type: 'TOGGLE_SIDEBAR', - sidebar: 'desktop', - forcedValue: false, + it( 'should set the general sidebar active panel', () => { + const state = preferences( deepFreeze( { + activeGeneralSidebar: 'editor', + activeSidebarPanel: { + editor: null, + plugin: null, + }, + } ), { + type: 'SET_GENERAL_SIDEBAR_ACTIVE_PANEL', + sidebar: 'editor', + panel: 'document', } ); - - expect( state.sidebars ).toEqual( { - mobile: true, - desktop: false, + expect( state ).toEqual( { + activeGeneralSidebar: 'editor', + activeSidebarPanel: { + editor: 'document', + plugin: null, + }, } ); } ); it( 'should set the sidebar panel open flag to true if unset', () => { const state = preferences( deepFreeze( {} ), { - type: 'TOGGLE_SIDEBAR_PANEL', + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel: 'post-taxonomies', } ); @@ -82,7 +60,7 @@ describe( 'state', () => { it( 'should toggle the sidebar panel open flag', () => { const state = preferences( deepFreeze( { panels: { 'post-taxonomies': true } } ), { - type: 'TOGGLE_SIDEBAR_PANEL', + type: 'TOGGLE_GENERAL_SIDEBAR_EDITOR_PANEL', panel: 'post-taxonomies', } ); @@ -95,7 +73,7 @@ describe( 'state', () => { mode: 'text', } ); - expect( state ).toEqual( { mode: 'text' } ); + expect( state ).toEqual( { editorMode: 'text' } ); } ); it( 'should toggle a feature flag', () => { diff --git a/edit-post/store/test/selectors.js b/edit-post/store/test/selectors.js index 581b0db3a0cbc..3c21c37741d4f 100644 --- a/edit-post/store/test/selectors.js +++ b/edit-post/store/test/selectors.js @@ -4,7 +4,7 @@ import { getEditorMode, getPreference, - isSidebarOpened, + isGeneralSidebarPanelOpened, hasOpenSidebar, isEditorSidebarPanelOpened, isMobile, @@ -20,7 +20,7 @@ describe( 'selectors', () => { describe( 'getEditorMode', () => { it( 'should return the selected editor mode', () => { const state = { - preferences: { mode: 'text' }, + preferences: { editorMode: 'text' }, }; expect( getEditorMode( state ) ).toEqual( 'text' ); @@ -61,143 +61,66 @@ describe( 'selectors', () => { } ); } ); - describe( 'isSidebarOpened', () => { - it( 'should return true when is not mobile and the normal sidebar is opened', () => { + describe( 'isGeneralSidebarPanelOpened', () => { + it( 'should return true when specified the sidebar panel is opened', () => { const state = { - mobile: false, - preferences: { - sidebars: { - desktop: true, - mobile: false, - }, - }, - }; - - expect( isSidebarOpened( state ) ).toBe( true ); - } ); - - it( 'should return false when is not mobile and the normal sidebar is closed', () => { - const state = { - mobile: false, - preferences: { - sidebars: { - desktop: false, - mobile: true, - }, - }, - }; - - expect( isSidebarOpened( state ) ).toBe( false ); - } ); - - it( 'should return true when is mobile and the mobile sidebar is opened', () => { - const state = { - mobile: true, preferences: { - sidebars: { - desktop: false, - mobile: true, - }, + activeGeneralSidebar: 'editor', + viewportType: 'desktop', + activeSidebarPanel: 'document', }, }; + const panel = 'document'; + const sidebar = 'editor'; - expect( isSidebarOpened( state ) ).toBe( true ); + expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( true ); } ); - it( 'should return false when is mobile and the mobile sidebar is closed', () => { + it( 'should return false when another panel than the specified sidebar panel is opened', () => { const state = { - mobile: true, preferences: { - sidebars: { - desktop: true, - mobile: false, - }, + activeGeneralSidebar: 'editor', + viewportType: 'desktop', + activeSidebarPanel: 'blocks', }, }; + const panel = 'document'; + const sidebar = 'editor'; - expect( isSidebarOpened( state ) ).toBe( false ); + expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( false ); } ); - it( 'should return true when the given is opened', () => { + it( 'should return false when no sidebar panel is opened', () => { const state = { preferences: { - sidebars: { - publish: true, - }, + activeGeneralSidebar: null, + viewportType: 'desktop', + activeSidebarPanel: null, }, }; + const panel = 'blocks'; + const sidebar = 'editor'; - expect( isSidebarOpened( state, 'publish' ) ).toBe( true ); - } ); - - it( 'should return false when the given is not opened', () => { - const state = { - preferences: { - sidebars: { - publish: false, - }, - }, - }; - - expect( isSidebarOpened( state, 'publish' ) ).toBe( false ); + expect( isGeneralSidebarPanelOpened( state, sidebar, panel ) ).toBe( false ); } ); } ); describe( 'hasOpenSidebar', () => { - it( 'should return true if at least one sidebar is open (using the desktop sidebar as default)', () => { + it( 'should return true if at least one sidebar is open', () => { const state = { - mobile: false, preferences: { - sidebars: { - desktop: true, - mobile: false, - publish: false, - }, + activeSidebarPanel: null, }, }; expect( hasOpenSidebar( state ) ).toBe( true ); } ); - it( 'should return true if at no sidebar is open (using the desktop sidebar as default)', () => { + it( 'should return false if no sidebar is open', () => { const state = { - mobile: false, + publishSidebarActive: false, preferences: { - sidebars: { - desktop: false, - mobile: true, - publish: false, - }, - }, - }; - - expect( hasOpenSidebar( state ) ).toBe( false ); - } ); - - it( 'should return true if at least one sidebar is open (using the mobile sidebar as default)', () => { - const state = { - mobile: true, - preferences: { - sidebars: { - desktop: false, - mobile: true, - publish: false, - }, - }, - }; - - expect( hasOpenSidebar( state ) ).toBe( true ); - } ); - - it( 'should return true if at no sidebar is open (using the mobile sidebar as default)', () => { - const state = { - mobile: true, - preferences: { - sidebars: { - desktop: true, - mobile: false, - publish: false, - }, + activeGeneralSidebar: null, }, }; diff --git a/edit-post/utils/mobile/README.md b/edit-post/utils/mobile/README.md deleted file mode 100644 index fff39c3f681c8..0000000000000 --- a/edit-post/utils/mobile/README.md +++ /dev/null @@ -1,15 +0,0 @@ -mobileMiddleware -=========== - -`mobileMiddleware` is a very simple [redux middleware](https://redux.js.org/docs/advanced/Middleware.html) that sets the isSidebarOpened flag to false on REDUX_REHYDRATE payloads. -This useful to make isSidebarOpened false on mobile even if the value that was saved to local storage was true. -The middleware just needs to be added to the enhancers list: - -```js - const enhancers = [ - ... - applyMiddleware( mobileMiddleware ), - ]; - ... - const store = createStore( reducer, flowRight( enhancers ) ); -``` diff --git a/edit-post/utils/mobile/index.js b/edit-post/utils/mobile/index.js deleted file mode 100644 index 67e941bb2bd40..0000000000000 --- a/edit-post/utils/mobile/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import { isMobile } from '../../store/selectors'; -import { toggleSidebar } from '../../store/actions'; - -/** - * Middleware - */ - -export const mobileMiddleware = ( { getState } ) => next => action => { - if ( action.type === 'TOGGLE_SIDEBAR' && action.sidebar === undefined ) { - return next( toggleSidebar( isMobile( getState() ) ? 'mobile' : 'desktop', action.forcedValue ) ); - } - return next( action ); -};