diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index c01997e5377e0..0f2ad4fd0eb85 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -1,9 +1,13 @@ -## V.V.V (Unreleased) +## Master ### New Features - Implement the `addToGallery` option in the `MediaUpload` hook. The option allows users to open the media modal in the `gallery-library`instead of `gallery-edit` state. +### Refactor + +- convert `INIT` effect to controls & actions [#14740](https://github.com/WordPress/gutenberg/pull/14740) + ## 3.2.0 (2019-03-06) diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index ecfac09f9347e..cf8070c8c7b4e 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -76,6 +76,7 @@ export function initializeEditor( id, postType, postId, settings, initialEdits ) console.warn( "Your browser is using Quirks Mode. \nThis can cause rendering issues such as blocks overlaying meta boxes in the editor. Quirks Mode can be triggered by PHP errors or HTML code appearing before the opening . Try checking the raw page source or your site's PHP error log and resolving errors there, removing any HTML before the doctype, or disabling plugins." ); } + dispatch( 'core/edit-post' ).__unstableInitialize(); dispatch( 'core/nux' ).triggerGuide( [ 'core/editor.inserter', 'core/editor.settings', diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index c12eb674837eb..e3538cccddc64 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -3,6 +3,13 @@ */ import { castArray } from 'lodash'; +/** + * Internal dependencies + */ +import { __unstableSubscribe } from './controls'; +import { onChangeListener } from './utils'; +import { STORE_KEY, VIEW_AS_LINK_SELECTOR } from './constants'; + /** * Returns an action object used in signalling that the user opened an editor sidebar. * @@ -232,3 +239,74 @@ export function metaBoxUpdatesSuccess() { }; } +/** + * Returns an action generator used to initialize some subscriptions for the + * post editor: + * + * - subscription for toggling the `edit-post/block` general sidebar when a + * block is selected. + * - subscription for hiding/showing the sidebar depending on size of viewport. + * - subscription for updating the "View Post" link in the admin bar when + * permalink is updated. + */ +export function* __unstableInitialize() { + // Select the block settings tab when the selected block changes + yield __unstableSubscribe( ( registry ) => onChangeListener( + () => !! registry.select( 'core/block-editor' ) + .getBlockSelectionStart(), + ( hasBlockSelection ) => { + if ( ! registry.select( 'core/edit-post' ).isEditorSidebarOpened() ) { + return; + } + if ( hasBlockSelection ) { + registry.dispatch( STORE_KEY ) + .openGeneralSidebar( 'edit-post/block' ); + } else { + registry.dispatch( STORE_KEY ) + .openGeneralSidebar( 'edit-post/document' ); + } + } + ) ); + // hide/show the sidebar depending on size of viewport. + yield __unstableSubscribe( ( registry ) => onChangeListener( + () => registry.select( 'core/viewport' ) + .isViewportMatch( '< medium' ), + ( () => { + let sidebarToReOpenOnExpand = null; + return ( isSmall ) => { + const { getActiveGeneralSidebarName } = registry.select( STORE_KEY ); + const { + closeGeneralSidebar: closeSidebar, + openGeneralSidebar: openSidebar, + } = registry.dispatch( STORE_KEY ); + if ( isSmall ) { + sidebarToReOpenOnExpand = getActiveGeneralSidebarName(); + if ( sidebarToReOpenOnExpand ) { + closeSidebar(); + } + } else if ( + sidebarToReOpenOnExpand && + ! getActiveGeneralSidebarName() + ) { + openSidebar( sidebarToReOpenOnExpand ); + } + }; + } )(), + true + ) ); + // Update View Post link in the admin bar when permalink is updated. + yield __unstableSubscribe( ( registry ) => onChangeListener( + () => registry.select( 'core/editor' ).getCurrentPost().link, + ( newPermalink ) => { + if ( ! newPermalink ) { + return; + } + const nodeToUpdate = document.querySelector( VIEW_AS_LINK_SELECTOR ); + if ( ! nodeToUpdate ) { + return; + } + nodeToUpdate.setAttribute( 'href', newPermalink ); + } + ) ); +} + diff --git a/packages/edit-post/src/store/constants.js b/packages/edit-post/src/store/constants.js new file mode 100644 index 0000000000000..60f80d914a5c7 --- /dev/null +++ b/packages/edit-post/src/store/constants.js @@ -0,0 +1,11 @@ +/** + * The identifier for the data store. + * @type {string} + */ +export const STORE_KEY = 'core/edit-post'; + +/** + * CSS selector string for the admin bar view post link anchor tag. + * @type {string} + */ +export const VIEW_AS_LINK_SELECTOR = '#wp-admin-bar-view a'; diff --git a/packages/edit-post/src/store/controls.js b/packages/edit-post/src/store/controls.js new file mode 100644 index 0000000000000..462741a173407 --- /dev/null +++ b/packages/edit-post/src/store/controls.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { createRegistryControl } from '@wordpress/data'; + +/** + * Calls a selector using the current state. + * + * @param {string} storeName Store name. + * @param {string} selectorName Selector name. + * @param {Array} args Selector arguments. + * + * @return {Object} control descriptor. + */ +export function select( storeName, selectorName, ...args ) { + return { + type: 'SELECT', + storeName, + selectorName, + args, + }; +} + +/** + * Calls a subscriber using the current state. + * + * @param {function} listenerCallback A callback for the subscriber that + * receives the registry. + * @return {Object} control descriptor. + */ +export function __unstableSubscribe( listenerCallback ) { + return { type: 'SUBSCRIBE', listenerCallback }; +} + +const controls = { + SELECT: createRegistryControl( + ( registry ) => ( { storeName, selectorName, args } ) => { + return registry.select( storeName )[ selectorName ]( ...args ); + } + ), + SUBSCRIBE: createRegistryControl( + ( registry ) => ( { listenerCallback } ) => { + return registry.subscribe( listenerCallback( registry ) ); + } + ), +}; + +export default controls; diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js index b273d0a4b3e35..6c64a17313097 100644 --- a/packages/edit-post/src/store/effects.js +++ b/packages/edit-post/src/store/effects.js @@ -14,20 +14,9 @@ import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import { - metaBoxUpdatesSuccess, - requestMetaBoxUpdates, - openGeneralSidebar, - closeGeneralSidebar, -} from './actions'; -import { - getActiveMetaBoxLocations, - getActiveGeneralSidebarName, -} from './selectors'; +import { metaBoxUpdatesSuccess, requestMetaBoxUpdates } from './actions'; +import { getActiveMetaBoxLocations } from './selectors'; import { getMetaBoxContainer } from '../utils/meta-boxes'; -import { onChangeListener } from './utils'; - -const VIEW_AS_LINK_SELECTOR = '#wp-admin-bar-view a'; const effects = { SET_META_BOXES_PER_LOCATIONS( action, store ) { @@ -126,66 +115,6 @@ const effects = { const message = action.mode === 'visual' ? __( 'Visual editor selected' ) : __( 'Code editor selected' ); speak( message, 'assertive' ); }, - INIT( _, store ) { - // Select the block settings tab when the selected block changes - subscribe( onChangeListener( - () => !! select( 'core/block-editor' ).getBlockSelectionStart(), - ( hasBlockSelection ) => { - if ( ! select( 'core/edit-post' ).isEditorSidebarOpened() ) { - return; - } - if ( hasBlockSelection ) { - store.dispatch( openGeneralSidebar( 'edit-post/block' ) ); - } else { - store.dispatch( openGeneralSidebar( 'edit-post/document' ) ); - } - } ) - ); - - const isMobileViewPort = () => select( 'core/viewport' ).isViewportMatch( '< medium' ); - const adjustSidebar = ( () => { - // contains the sidebar we close when going to viewport sizes lower than medium. - // This allows to reopen it when going again to viewport sizes greater than medium. - let sidebarToReOpenOnExpand = null; - return ( isSmall ) => { - if ( isSmall ) { - sidebarToReOpenOnExpand = getActiveGeneralSidebarName( store.getState() ); - if ( sidebarToReOpenOnExpand ) { - store.dispatch( closeGeneralSidebar() ); - } - } else if ( sidebarToReOpenOnExpand && ! getActiveGeneralSidebarName( store.getState() ) ) { - store.dispatch( openGeneralSidebar( sidebarToReOpenOnExpand ) ); - } - }; - } )(); - - adjustSidebar( isMobileViewPort() ); - - // Collapse sidebar when viewport shrinks. - // Reopen sidebar it if viewport expands and it was closed because of a previous shrink. - subscribe( onChangeListener( isMobileViewPort, adjustSidebar ) ); - - // Update View as link when currentPost link changes - const updateViewAsLink = ( newPermalink ) => { - if ( ! newPermalink ) { - return; - } - - const nodeToUpdate = document.querySelector( - VIEW_AS_LINK_SELECTOR - ); - if ( ! nodeToUpdate ) { - return; - } - nodeToUpdate.setAttribute( 'href', newPermalink ); - }; - - subscribe( onChangeListener( - () => select( 'core/editor' ).getCurrentPost().link, - updateViewAsLink - ) ); - }, - }; export default effects; diff --git a/packages/edit-post/src/store/index.js b/packages/edit-post/src/store/index.js index 9294d7cd90ac6..4560fd12d46bb 100644 --- a/packages/edit-post/src/store/index.js +++ b/packages/edit-post/src/store/index.js @@ -10,15 +10,17 @@ import reducer from './reducer'; import applyMiddlewares from './middlewares'; import * as actions from './actions'; import * as selectors from './selectors'; +import controls from './controls'; +import { STORE_KEY } from './constants'; -const store = registerStore( 'core/edit-post', { +const store = registerStore( STORE_KEY, { reducer, actions, selectors, + controls, persist: [ 'preferences' ], } ); applyMiddlewares( store ); -store.dispatch( { type: 'INIT' } ); export default store; diff --git a/packages/edit-post/src/store/index.native.js b/packages/edit-post/src/store/index.native.js index 611a2b4eaae55..10355071bd454 100644 --- a/packages/edit-post/src/store/index.native.js +++ b/packages/edit-post/src/store/index.native.js @@ -10,8 +10,9 @@ import reducer from './reducer'; import applyMiddlewares from './middlewares'; import * as actions from './actions'; import * as selectors from './selectors'; +import { STORE_KEY } from './constants'; -const store = registerStore( 'core/edit-post', { +const store = registerStore( STORE_KEY, { reducer, actions, selectors, diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index 07dc4b81ece68..a16e58f9cc18d 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -15,7 +15,9 @@ import { toggleFeature, togglePinnedPluginItem, requestMetaBoxUpdates, + __unstableInitialize, } from '../actions'; +import { STORE_KEY, VIEW_AS_LINK_SELECTOR } from '../constants'; describe( 'actions', () => { describe( 'openGeneralSidebar', () => { @@ -133,4 +135,180 @@ describe( 'actions', () => { } ); } ); } ); + + describe( '__unstableInitialize', () => { + let fulfillment; + const reset = () => fulfillment = __unstableInitialize(); + const registryMock = { select: {}, dispatch: {} }; + describe( 'yields subscribe control descriptor for block settings', () => { + reset(); + const { value } = fulfillment.next(); + const listenerCallback = value.listenerCallback; + const isEditorSidebarOpened = jest.fn(); + const getBlockSelectionStart = jest.fn(); + const openSidebar = jest.fn(); + beforeEach( () => { + registryMock.select = ( store ) => { + const stores = { + 'core/block-editor': { getBlockSelectionStart }, + 'core/edit-post': { isEditorSidebarOpened }, + }; + return stores[ store ]; + }; + registryMock.dispatch = () => ( { openGeneralSidebar: openSidebar } ); + } ); + afterEach( () => { + isEditorSidebarOpened.mockClear(); + getBlockSelectionStart.mockClear(); + openSidebar.mockClear(); + } ); + it( 'returns subscribe control descriptor', () => { + expect( value.type ).toBe( 'SUBSCRIBE' ); + } ); + it( 'does nothing if sidebar is not opened', () => { + getBlockSelectionStart.mockReturnValue( true ); + isEditorSidebarOpened.mockReturnValue( false ); + const listener = listenerCallback( registryMock ); + getBlockSelectionStart.mockReturnValue( false ); + listener(); + expect( getBlockSelectionStart ).toHaveBeenCalled(); + expect( isEditorSidebarOpened ).toHaveBeenCalled(); + expect( openSidebar ).not.toHaveBeenCalled(); + } ); + it( 'opens block sidebar if block is selected', () => { + isEditorSidebarOpened.mockReturnValue( true ); + getBlockSelectionStart.mockReturnValue( false ); + const listener = listenerCallback( registryMock ); + getBlockSelectionStart.mockReturnValue( true ); + listener(); + expect( openSidebar ).toHaveBeenCalledWith( 'edit-post/block' ); + } ); + it( 'opens document sidebar if block is not selected', () => { + isEditorSidebarOpened.mockReturnValue( true ); + getBlockSelectionStart.mockReturnValue( true ); + const listener = listenerCallback( registryMock ); + getBlockSelectionStart.mockReturnValue( false ); + listener(); + expect( openSidebar ).toHaveBeenCalledWith( 'edit-post/document' ); + } ); + } ); + describe( 'yields subscribe control descriptor for adjusting the ' + + 'sidebar', () => { + reset(); + fulfillment.next(); + const { value } = fulfillment.next(); + const listenerCallback = value.listenerCallback; + const isViewportMatch = jest.fn(); + const getActiveGeneralSidebarName = jest.fn(); + const willCloseGeneralSidebar = jest.fn(); + const willOpenGeneralSidebar = jest.fn(); + beforeEach( () => { + registryMock.select = ( store ) => { + const stores = { + 'core/viewport': { isViewportMatch }, + [ STORE_KEY ]: { getActiveGeneralSidebarName }, + }; + return stores[ store ]; + }; + registryMock.dispatch = ( store ) => { + const stores = { + [ STORE_KEY ]: { + closeGeneralSidebar: willCloseGeneralSidebar, + openGeneralSidebar: willOpenGeneralSidebar, + }, + }; + return stores[ store ]; + }; + registryMock.subscribe = jest.fn(); + isViewportMatch.mockReturnValue( true ); + } ); + afterEach( () => { + isViewportMatch.mockClear(); + getActiveGeneralSidebarName.mockClear(); + willCloseGeneralSidebar.mockClear(); + willOpenGeneralSidebar.mockClear(); + } ); + it( 'returns subscribe control descriptor', () => { + expect( value.type ).toBe( 'SUBSCRIBE' ); + } ); + it( 'initializes and does nothing when viewport is not small', () => { + isViewportMatch.mockReturnValue( false ); + listenerCallback( registryMock )(); + expect( isViewportMatch ).toHaveBeenCalled(); + expect( getActiveGeneralSidebarName ).not.toHaveBeenCalled(); + } ); + it( 'does not close sidebar if viewport is small and there is no ' + + 'active sidebar name available', () => { + getActiveGeneralSidebarName.mockReturnValue( false ); + listenerCallback( registryMock )(); + expect( willCloseGeneralSidebar ).not.toHaveBeenCalled(); + expect( willOpenGeneralSidebar ).not.toHaveBeenCalled(); + } ); + it( 'closes sidebar if viewport is small and there is an active ' + + 'sidebar name available', () => { + getActiveGeneralSidebarName.mockReturnValue( 'someSidebar' ); + listenerCallback( registryMock )(); + expect( willCloseGeneralSidebar ).toHaveBeenCalled(); + expect( willOpenGeneralSidebar ).not.toHaveBeenCalled(); + } ); + it( 'opens sidebar if viewport is not small, there is a cached sidebar to ' + + 'reopen on expand, and there is no current sidebar name available', () => { + getActiveGeneralSidebarName.mockReturnValue( 'someSidebar' ); + const listener = listenerCallback( registryMock ); + listener(); + isViewportMatch.mockReturnValue( false ); + getActiveGeneralSidebarName.mockReturnValue( false ); + listener(); + expect( willCloseGeneralSidebar ).toHaveBeenCalledTimes( 1 ); + expect( willOpenGeneralSidebar ).toHaveBeenCalledTimes( 1 ); + } ); + } ); + describe( 'yields subscribe control descriptor for updating the ' + + 'view post link when the permalink changes', () => { + reset(); + fulfillment.next(); + fulfillment.next(); + const { value } = fulfillment.next(); + const listenerCallback = value.listenerCallback; + const getCurrentPost = jest.fn(); + const setAttribute = jest.fn(); + beforeEach( () => { + document.querySelector = jest.fn().mockReturnValue( { setAttribute } ); + getCurrentPost.mockReturnValue( { link: 'foo' } ); + registryMock.select = ( store ) => { + const stores = { 'core/editor': { getCurrentPost } }; + return stores[ store ]; + }; + } ); + afterEach( () => { + setAttribute.mockClear(); + getCurrentPost.mockClear(); + } ); + it( 'returns expected control descriptor', () => { + expect( value.type ).toBe( 'SUBSCRIBE' ); + } ); + it( 'updates nothing if there is no new permalink', () => { + const listener = listenerCallback( registryMock ); + listener(); + expect( getCurrentPost ).toHaveBeenCalledTimes( 2 ); + expect( document.querySelector ).not.toHaveBeenCalled(); + expect( setAttribute ).not.toHaveBeenCalled(); + } ); + it( 'does not do anything if the node is not found', () => { + const listener = listenerCallback( registryMock ); + getCurrentPost.mockReturnValue( { link: 'bar' } ); + document.querySelector.mockReturnValue( false ); + listener(); + expect( document.querySelector ) + .toHaveBeenCalledWith( VIEW_AS_LINK_SELECTOR ); + expect( setAttribute ).not.toHaveBeenCalled(); + } ); + it( 'updates with the new permalink when node is found', () => { + const listener = listenerCallback( registryMock ); + getCurrentPost.mockReturnValue( { link: 'bar' } ); + listener(); + expect( setAttribute ).toHaveBeenCalledWith( 'href', 'bar' ); + } ); + } ); + } ); } ); diff --git a/packages/edit-post/src/store/utils.js b/packages/edit-post/src/store/utils.js index 86b043327e291..7dfd1ea08ed89 100644 --- a/packages/edit-post/src/store/utils.js +++ b/packages/edit-post/src/store/utils.js @@ -4,10 +4,15 @@ * * @param {function} selector Selector. * @param {function} listener Listener. + * @param {boolean} initial Flags whether listener should be invoked on + * initial call. * @return {function} Listener creator. */ -export const onChangeListener = ( selector, listener ) => { +export const onChangeListener = ( selector, listener, initial = false ) => { let previousValue = selector(); + if ( initial ) { + listener( selector() ); + } return () => { const selectedValue = selector(); if ( selectedValue !== previousValue ) {