From b176ee67bb44503e9faac3f7e2903b7681791db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 09:47:27 +0200 Subject: [PATCH 01/30] Squash --- packages/edit-navigation/package.json | 1 + .../src/components/menu-editor/index.js | 83 +++--- .../components/menu-editor/promise-queue.js | 51 ---- .../use-create-missing-menu-items.js | 67 ----- .../components/menu-editor/use-menu-items.js | 172 ------------ .../use-navigation-block-editor.js | 33 +++ .../menu-editor/use-navigation-blocks.js | 114 -------- packages/edit-navigation/src/index.js | 2 + packages/edit-navigation/src/store/actions.js | 251 ++++++++++++++++++ .../edit-navigation/src/store/controls.js | 113 ++++++++ packages/edit-navigation/src/store/index.js | 40 +++ packages/edit-navigation/src/store/reducer.js | 36 +++ .../edit-navigation/src/store/resolvers.js | 90 +++++++ .../edit-navigation/src/store/selectors.js | 39 +++ packages/edit-navigation/src/store/utils.js | 20 ++ 15 files changed, 669 insertions(+), 443 deletions(-) delete mode 100644 packages/edit-navigation/src/components/menu-editor/promise-queue.js delete mode 100644 packages/edit-navigation/src/components/menu-editor/use-create-missing-menu-items.js delete mode 100644 packages/edit-navigation/src/components/menu-editor/use-menu-items.js create mode 100644 packages/edit-navigation/src/components/menu-editor/use-navigation-block-editor.js delete mode 100644 packages/edit-navigation/src/components/menu-editor/use-navigation-blocks.js create mode 100644 packages/edit-navigation/src/store/actions.js create mode 100644 packages/edit-navigation/src/store/controls.js create mode 100644 packages/edit-navigation/src/store/index.js create mode 100644 packages/edit-navigation/src/store/reducer.js create mode 100644 packages/edit-navigation/src/store/resolvers.js create mode 100644 packages/edit-navigation/src/store/selectors.js create mode 100644 packages/edit-navigation/src/store/utils.js diff --git a/packages/edit-navigation/package.json b/packages/edit-navigation/package.json index 169d803b037373..d91e86ab7d1fbb 100644 --- a/packages/edit-navigation/package.json +++ b/packages/edit-navigation/package.json @@ -35,6 +35,7 @@ "@wordpress/blocks": "file:../blocks", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", + "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", "@wordpress/data-controls": "file:../data-controls", "@wordpress/dom-ready": "file:../dom-ready", diff --git a/packages/edit-navigation/src/components/menu-editor/index.js b/packages/edit-navigation/src/components/menu-editor/index.js index ce40b46c4f6aea..afbf04177cd3ef 100644 --- a/packages/edit-navigation/src/components/menu-editor/index.js +++ b/packages/edit-navigation/src/components/menu-editor/index.js @@ -6,66 +6,71 @@ import { BlockEditorProvider, } from '@wordpress/block-editor'; import { useViewportMatch } from '@wordpress/compose'; -import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import useMenuItems from './use-menu-items'; -import useNavigationBlocks from './use-navigation-blocks'; import MenuEditorShortcuts from './shortcuts'; import BlockEditorArea from './block-editor-area'; import NavigationStructureArea from './navigation-structure-area'; +import useNavigationBlockEditor from './use-navigation-block-editor'; +import { useDispatch, useSelect } from '@wordpress/data'; export default function MenuEditor( { menuId, blockEditorSettings, onDeleteMenu, } ) { - const isLargeViewport = useViewportMatch( 'medium' ); - const query = useMemo( () => ( { menus: menuId, per_page: -1 } ), [ - menuId, - ] ); - const { - menuItems, - eventuallySaveMenuItems, - createMissingMenuItems, - } = useMenuItems( query ); - const { blocks, setBlocks, menuItemsRef } = useNavigationBlocks( - menuItems + const post = useSelect( ( select ) => + select( 'core/edit-navigation' ).getNavigationPost( menuId ) ); - const saveMenuItems = () => eventuallySaveMenuItems( blocks, menuItemsRef ); - return (
- setBlocks( updatedBlocks ) } - onChange={ ( updatedBlocks ) => { - createMissingMenuItems( updatedBlocks, menuItemsRef ); - setBlocks( updatedBlocks ); - } } - settings={ { - ...blockEditorSettings, - templateLock: 'all', - hasFixedToolbar: true, - } } - > - - - - - + ) }
); } + +const NavigationBlockEditorProvider = ( { + post, + blockEditorSettings, + onDeleteMenu, +} ) => { + const isLargeViewport = useViewportMatch( 'medium' ); + const [ blocks, onInput, onChange ] = useNavigationBlockEditor( post ); + const { saveNavigationPost } = useDispatch( 'core/edit-navigation' ); + const save = () => saveNavigationPost( post ); + return ( + + + + + + + ); +}; diff --git a/packages/edit-navigation/src/components/menu-editor/promise-queue.js b/packages/edit-navigation/src/components/menu-editor/promise-queue.js deleted file mode 100644 index d560164e91e76d..00000000000000 --- a/packages/edit-navigation/src/components/menu-editor/promise-queue.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * A concurrency primitive that runs at most `concurrency` async tasks at once. - */ -export default class PromiseQueue { - constructor( concurrency = 1 ) { - this.concurrency = concurrency; - this.queue = []; - this.active = []; - this.listeners = []; - } - - enqueue( action ) { - this.queue.push( action ); - this.run(); - } - - run() { - while ( this.queue.length && this.active.length <= this.concurrency ) { - const action = this.queue.shift(); - const promise = action().then( () => { - this.active.splice( this.active.indexOf( promise ), 1 ); - this.run(); - this.notifyIfEmpty(); - } ); - this.active.push( promise ); - } - } - - notifyIfEmpty() { - if ( this.active.length === 0 && this.queue.length === 0 ) { - for ( const l of this.listeners ) { - l(); - } - this.listeners = []; - } - } - - /** - * Calls `callback` once all async actions in the queue are finished, - * or immediately if no actions are running. - * - * @param {Function} callback Callback to call - */ - then( callback ) { - if ( this.active.length ) { - this.listeners.push( callback ); - } else { - callback(); - } - } -} diff --git a/packages/edit-navigation/src/components/menu-editor/use-create-missing-menu-items.js b/packages/edit-navigation/src/components/menu-editor/use-create-missing-menu-items.js deleted file mode 100644 index b21fa5416d1e62..00000000000000 --- a/packages/edit-navigation/src/components/menu-editor/use-create-missing-menu-items.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { useRef, useCallback } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { flattenBlocks } from './helpers'; -import PromiseQueue from './promise-queue'; - -/** - * When a new Navigation child block is added, we create a draft menuItem for it because - * the batch save endpoint expects all the menu items to have a valid id already. - * PromiseQueue is used in order to - * 1) limit the amount of requests processed at the same time - * 2) save the menu only after all requests are finalized - * - * @return {function(*=): void} Function registering it's argument to be called once all menuItems are created. - */ -export default function useCreateMissingMenuItems() { - const promiseQueueRef = useRef( new PromiseQueue() ); - const enqueuedBlocksIds = useRef( [] ); - const createMissingMenuItems = ( blocks, menuItemsRef ) => { - for ( const { clientId, name } of flattenBlocks( blocks ) ) { - // No need to create menuItems for the wrapping navigation block - if ( name === 'core/navigation' ) { - continue; - } - // Menu item was already created - if ( clientId in menuItemsRef.current ) { - continue; - } - // Menu item already in the queue - if ( enqueuedBlocksIds.current.includes( clientId ) ) { - continue; - } - enqueuedBlocksIds.current.push( clientId ); - promiseQueueRef.current.enqueue( () => - createDraftMenuItem( clientId ).then( ( menuItem ) => { - menuItemsRef.current[ clientId ] = menuItem; - enqueuedBlocksIds.current.splice( - enqueuedBlocksIds.current.indexOf( clientId ) - ); - } ) - ); - } - }; - const onCreated = useCallback( - ( callback ) => promiseQueueRef.current.then( callback ), - [ promiseQueueRef.current ] - ); - return { createMissingMenuItems, onCreated }; -} - -function createDraftMenuItem() { - return apiFetch( { - path: `/__experimental/menu-items`, - method: 'POST', - data: { - title: 'Placeholder', - url: 'Placeholder', - menu_order: 0, - }, - } ); -} diff --git a/packages/edit-navigation/src/components/menu-editor/use-menu-items.js b/packages/edit-navigation/src/components/menu-editor/use-menu-items.js deleted file mode 100644 index 250b9927153ab0..00000000000000 --- a/packages/edit-navigation/src/components/menu-editor/use-menu-items.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * External dependencies - */ -import { keyBy, omit } from 'lodash'; - -/** - * WordPress dependencies - */ -import { useDispatch, useSelect } from '@wordpress/data'; -import { useEffect, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; - -/** - * Internal dependencies - */ -import useCreateMissingMenuItems from './use-create-missing-menu-items'; - -export default function useMenuItems( query ) { - const menuItems = useFetchMenuItems( query ); - const saveMenuItems = useSaveMenuItems( query ); - const { createMissingMenuItems, onCreated } = useCreateMissingMenuItems(); - const eventuallySaveMenuItems = ( blocks, menuItemsRef ) => - onCreated( () => saveMenuItems( blocks, menuItemsRef ) ); - return { menuItems, eventuallySaveMenuItems, createMissingMenuItems }; -} - -export function useFetchMenuItems( query ) { - const { menuItems, isResolving } = useSelect( ( select ) => ( { - menuItems: select( 'core' ).getMenuItems( query ), - isResolving: select( 'core/data' ).isResolving( - 'core', - 'getMenuItems', - [ query ] - ), - } ) ); - - const [ resolvedMenuItems, setResolvedMenuItems ] = useState( null ); - - useEffect( () => { - if ( isResolving || menuItems === null ) { - return; - } - - setResolvedMenuItems( menuItems ); - }, [ isResolving, menuItems ] ); - - return resolvedMenuItems; -} - -export function useSaveMenuItems( query ) { - const { receiveEntityRecords } = useDispatch( 'core' ); - const { createSuccessNotice, createErrorNotice } = useDispatch( - 'core/notices' - ); - - const saveBlocks = async ( blocks, menuItemsRef ) => { - const result = await batchSave( - query.menus, - menuItemsRef, - blocks[ 0 ] - ); - - if ( result.success ) { - receiveEntityRecords( 'root', 'menuItem', [], query, true ); - createSuccessNotice( __( 'Navigation saved.' ), { - type: 'snackbar', - } ); - } else { - createErrorNotice( __( 'There was an error.' ), { - type: 'snackbar', - } ); - } - }; - - return saveBlocks; -} - -async function batchSave( menuId, menuItemsRef, navigationBlock ) { - const { nonce, stylesheet } = await apiFetch( { - path: '/__experimental/customizer-nonces/get-save-nonce', - } ); - - // eslint-disable-next-line no-undef - const body = new FormData(); - body.append( 'wp_customize', 'on' ); - body.append( 'customize_theme', stylesheet ); - body.append( 'nonce', nonce ); - body.append( 'customize_changeset_uuid', uuidv4() ); - body.append( 'customize_autosaved', 'on' ); - body.append( 'customize_changeset_status', 'publish' ); - body.append( 'action', 'customize_save' ); - body.append( - 'customized', - computeCustomizedAttribute( - navigationBlock.innerBlocks, - menuId, - menuItemsRef - ) - ); - - return await apiFetch( { - url: '/wp-admin/admin-ajax.php', - method: 'POST', - body, - } ); -} - -function computeCustomizedAttribute( blocks, menuId, menuItemsRef ) { - const blocksList = blocksTreeToFlatList( blocks ); - const dataList = blocksList.map( ( { block, parentId, position } ) => - linkBlockToRequestItem( block, parentId, position ) - ); - - // Create an object like { "nav_menu_item[12]": {...}} } - const computeKey = ( item ) => `nav_menu_item[${ item.id }]`; - const dataObject = keyBy( dataList, computeKey ); - - // Deleted menu items should be sent as false, e.g. { "nav_menu_item[13]": false } - for ( const clientId in menuItemsRef.current ) { - const key = computeKey( menuItemsRef.current[ clientId ] ); - if ( ! ( key in dataObject ) ) { - dataObject[ key ] = false; - } - } - - return JSON.stringify( dataObject ); - - function blocksTreeToFlatList( innerBlocks, parentId = 0 ) { - return innerBlocks.flatMap( ( block, index ) => - [ { block, parentId, position: index + 1 } ].concat( - blocksTreeToFlatList( - block.innerBlocks, - getMenuItemForBlock( block )?.id - ) - ) - ); - } - - function linkBlockToRequestItem( block, parentId, position ) { - const menuItem = omit( getMenuItemForBlock( block ), 'menus', 'meta' ); - return { - ...menuItem, - position, - title: block.attributes?.label, - url: block.attributes.url, - original_title: '', - classes: ( menuItem.classes || [] ).join( ' ' ), - xfn: ( menuItem.xfn || [] ).join( ' ' ), - nav_menu_term_id: menuId, - menu_item_parent: parentId, - status: 'publish', - _invalid: false, - }; - } - - function getMenuItemForBlock( block ) { - return omit( menuItemsRef.current[ block.clientId ] || {}, '_links' ); - } -} - -function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, ( c ) => { - // eslint-disable-next-line no-restricted-syntax - const a = Math.random() * 16; - // eslint-disable-next-line no-bitwise - const r = a | 0; - // eslint-disable-next-line no-bitwise - const v = c === 'x' ? r : ( r & 0x3 ) | 0x8; - return v.toString( 16 ); - } ); -} diff --git a/packages/edit-navigation/src/components/menu-editor/use-navigation-block-editor.js b/packages/edit-navigation/src/components/menu-editor/use-navigation-block-editor.js new file mode 100644 index 00000000000000..4c8672171b870b --- /dev/null +++ b/packages/edit-navigation/src/components/menu-editor/use-navigation-block-editor.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; +import { useEntityBlockEditor } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { KIND, POST_TYPE } from '../../store/utils'; + +export default function useNavigationBlockEditor( post ) { + const { createMissingMenuItems } = useDispatch( 'core/edit-navigation' ); + + const [ blocks, onInput, _onChange ] = useEntityBlockEditor( + KIND, + POST_TYPE, + { id: post.id } + ); + const onChange = useCallback( + ( updatedBlocks ) => { + async function handle() { + await _onChange( updatedBlocks ); + createMissingMenuItems( post ); + } + handle(); + }, + [ blocks, onChange ] + ); + + return [ blocks, onInput, onChange ]; +} diff --git a/packages/edit-navigation/src/components/menu-editor/use-navigation-blocks.js b/packages/edit-navigation/src/components/menu-editor/use-navigation-blocks.js deleted file mode 100644 index 1b7899737fd784..00000000000000 --- a/packages/edit-navigation/src/components/menu-editor/use-navigation-blocks.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * External dependencies - */ -import { keyBy, groupBy, sortBy } from 'lodash'; - -/** - * WordPress dependencies - */ -import { createBlock } from '@wordpress/blocks'; -import { useState, useRef, useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { flattenBlocks } from './helpers'; - -export default function useNavigationBlocks( menuItems ) { - const [ blocks, setBlocks ] = useState( [] ); - const menuItemsRef = useRef( {} ); - - // Refresh our model whenever menuItems change - useEffect( () => { - const [ innerBlocks, clientIdToMenuItemMapping ] = menuItemsToBlocks( - menuItems, - blocks[ 0 ]?.innerBlocks, - menuItemsRef.current - ); - - const navigationBlock = blocks[ 0 ] - ? { ...blocks[ 0 ], innerBlocks } - : createBlock( 'core/navigation', {}, innerBlocks ); - - setBlocks( [ navigationBlock ] ); - menuItemsRef.current = clientIdToMenuItemMapping; - }, [ menuItems ] ); - - return { - blocks, - setBlocks, - menuItemsRef, - }; -} - -const menuItemsToBlocks = ( - menuItems, - prevBlocks = [], - prevClientIdToMenuItemMapping = {} -) => { - const blocksByMenuId = mapBlocksByMenuId( - prevBlocks, - prevClientIdToMenuItemMapping - ); - - const itemsByParentID = groupBy( menuItems, 'parent' ); - const clientIdToMenuItemMapping = {}; - const menuItemsToTreeOfBlocks = ( items ) => { - const innerBlocks = []; - if ( ! items ) { - return; - } - - const sortedItems = sortBy( items, 'menu_order' ); - for ( const item of sortedItems ) { - let menuItemInnerBlocks = []; - if ( itemsByParentID[ item.id ]?.length ) { - menuItemInnerBlocks = menuItemsToTreeOfBlocks( - itemsByParentID[ item.id ] - ); - } - const linkBlock = menuItemToLinkBlock( - item, - menuItemInnerBlocks, - blocksByMenuId[ item.id ] - ); - clientIdToMenuItemMapping[ linkBlock.clientId ] = item; - innerBlocks.push( linkBlock ); - } - return innerBlocks; - }; - - // menuItemsToTreeOfLinkBlocks takes an array of top-level menu items and recursively creates all their innerBlocks - const blocks = menuItemsToTreeOfBlocks( itemsByParentID[ 0 ] || [] ); - return [ blocks, clientIdToMenuItemMapping ]; -}; - -function menuItemToLinkBlock( - menuItem, - innerBlocks = [], - existingBlock = null -) { - const attributes = { - label: menuItem.title.rendered, - url: menuItem.url, - }; - - if ( existingBlock ) { - return { - ...existingBlock, - attributes, - innerBlocks, - }; - } - return createBlock( 'core/navigation-link', attributes, innerBlocks ); -} - -const mapBlocksByMenuId = ( blocks, menuItemsByClientId ) => { - const blocksByClientId = keyBy( flattenBlocks( blocks ), 'clientId' ); - const blocksByMenuId = {}; - for ( const clientId in menuItemsByClientId ) { - const menuItem = menuItemsByClientId[ clientId ]; - blocksByMenuId[ menuItem.id ] = blocksByClientId[ clientId ]; - } - return blocksByMenuId; -}; diff --git a/packages/edit-navigation/src/index.js b/packages/edit-navigation/src/index.js index 558b9949a0b3e4..7d29ac35e8c1fa 100644 --- a/packages/edit-navigation/src/index.js +++ b/packages/edit-navigation/src/index.js @@ -63,3 +63,5 @@ export function initialize( id, settings ) { document.getElementById( id ) ); } + +export { storeConfig } from './store'; diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js new file mode 100644 index 00000000000000..2cf3d8f7ca72ae --- /dev/null +++ b/packages/edit-navigation/src/store/actions.js @@ -0,0 +1,251 @@ +/** + * External dependencies + */ +import { invert, keyBy, omit } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { select, resolveMenuItems, dispatch, apiFetch } from './controls'; +import { uuidv4, menuItemsQuery, KIND, POST_TYPE } from './utils'; + +// Hits POST /wp/v2/menu-items once for every Link block that doesn't have an +// associated menu item. (IDK what a good name for this is.) +export const createMissingMenuItems = serializeProcessing( function* ( post ) { + const menuId = post.meta.menuId; + const mapping = post.meta.menuItemIdToClientId; + const clientIdToMenuId = invert( mapping ); + + const stack = [ post.blocks[ 0 ] ]; + while ( stack.length ) { + const block = stack.pop(); + if ( ! ( block.clientId in clientIdToMenuId ) ) { + const menuItem = yield apiFetch( { + path: `/__experimental/menu-items`, + method: 'POST', + data: { + title: 'Placeholder', + url: 'Placeholder', + menu_order: 0, + }, + } ); + + mapping[ menuItem.id ] = block.clientId; + const menuItems = yield resolveMenuItems( menuId ); + yield dispatch( + 'core', + 'receiveEntityRecords', + 'root', + 'menuItem', + [ ...menuItems, menuItem ], + menuItemsQuery( menuId ), + false + ); + } + stack.push( ...block.innerBlocks ); + } + + yield dispatch( 'core', 'editEntityRecord', KIND, POST_TYPE, post.id, { + meta: { + ...post.meta, + menuItemIdToClientId: mapping, + }, + } ); +} ); + +export const saveNavigationPost = serializeProcessing( function* ( post ) { + const menuId = post.meta.menuId; + const menuItemsByClientId = mapMenuItemsByClientId( + yield resolveMenuItems( menuId ), + post.meta.menuItemIdToClientId + ); + + try { + yield* batchSave( menuId, menuItemsByClientId, post.blocks[ 0 ] ); + yield dispatch( + 'core/notices', + 'createSuccessNotice', + __( 'Navigation saved.' ), + { + type: 'snackbar', + } + ); + } catch ( e ) { + yield dispatch( + 'core/notices', + 'createErrorNotice', + __( 'There was an error.' ), + { + type: 'snackbar', + } + ); + } +} ); + +function mapMenuItemsByClientId( menuItems, clientIdsByMenuId ) { + const result = {}; + if ( ! menuItems || ! clientIdsByMenuId ) { + return result; + } + for ( const menuItem of menuItems ) { + const clientId = clientIdsByMenuId[ menuItem.id ]; + if ( clientId ) { + result[ clientId ] = menuItem; + } + } + return result; +} + +function* batchSave( menuId, menuItemsByClientId, navigationBlock ) { + const { nonce, stylesheet } = yield apiFetch( { + path: '/__experimental/customizer-nonces/get-save-nonce', + } ); + + // eslint-disable-next-line no-undef + const body = new FormData(); + body.append( 'wp_customize', 'on' ); + body.append( 'customize_theme', stylesheet ); + body.append( 'nonce', nonce ); + body.append( 'customize_changeset_uuid', uuidv4() ); + body.append( 'customize_autosaved', 'on' ); + body.append( 'customize_changeset_status', 'publish' ); + body.append( 'action', 'customize_save' ); + body.append( + 'customized', + computeCustomizedAttribute( + navigationBlock.innerBlocks, + menuId, + menuItemsByClientId + ) + ); + + yield apiFetch( { + url: '/wp-admin/admin-ajax.php', + method: 'POST', + body, + } ); +} + +function computeCustomizedAttribute( blocks, menuId, menuItemsByClientId ) { + const blocksList = blocksTreeToFlatList( blocks ); + const dataList = blocksList.map( ( { block, parentId, position } ) => + linkBlockToRequestItem( block, parentId, position ) + ); + + // Create an object like { "nav_menu_item[12]": {...}} } + const computeKey = ( item ) => `nav_menu_item[${ item.id }]`; + const dataObject = keyBy( dataList, computeKey ); + + // Deleted menu items should be sent as false, e.g. { "nav_menu_item[13]": false } + for ( const clientId in menuItemsByClientId ) { + const key = computeKey( menuItemsByClientId[ clientId ] ); + if ( ! ( key in dataObject ) ) { + dataObject[ key ] = false; + } + } + + return JSON.stringify( dataObject ); + + function blocksTreeToFlatList( innerBlocks, parentId = 0 ) { + return innerBlocks.flatMap( ( block, index ) => + [ { block, parentId, position: index + 1 } ].concat( + blocksTreeToFlatList( + block.innerBlocks, + getMenuItemForBlock( block )?.id + ) + ) + ); + } + + function linkBlockToRequestItem( block, parentId, position ) { + const menuItem = omit( getMenuItemForBlock( block ), 'menus', 'meta' ); + return { + ...menuItem, + position, + title: block.attributes?.label, + url: block.attributes.url, + original_title: '', + classes: ( menuItem.classes || [] ).join( ' ' ), + xfn: ( menuItem.xfn || [] ).join( ' ' ), + nav_menu_term_id: menuId, + menu_item_parent: parentId, + status: 'publish', + _invalid: false, + }; + } + + function getMenuItemForBlock( block ) { + return omit( menuItemsByClientId[ block.clientId ] || {}, '_links' ); + } +} + +function serializeProcessing( callback ) { + return function* ( menuId ) { + const isProcessing = yield select( + 'core/edit-navigation', + 'isProcessingMenuItems', + menuId + ); + if ( isProcessing ) { + yield dispatch( + 'core/edit-navigation', + 'enqueueAfterProcessing', + menuId, + callback + ); + return { status: 'pending' }; + } + + yield dispatch( + 'core/edit-navigation', + 'startProcessingMenuItems', + menuId + ); + + try { + yield* callback( menuId ); + } finally { + yield dispatch( + 'core/edit-navigation', + 'finishProcessingMenuItems', + menuId + ); + + const pendingActions = yield select( + 'core/edit-navigation', + 'getPendingActions', + menuId + ); + if ( pendingActions.length ) { + yield* pendingActions[ 0 ]( menuId ); + } + } + }; +} + +export function startProcessingMenuItems( menuId ) { + return { + type: 'START_PROCESSING_MENU_ITEMS', + menuId, + }; +} + +export function finishProcessingMenuItems( menuId ) { + return { + type: 'FINISH_PROCESSING_MENU_ITEMS', + menuId, + }; +} + +export function enqueueAfterProcessing( menuId, action ) { + return { + type: 'ENQUEUE_AFTER_PROCESSING', + menuId, + action, + }; +} diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js new file mode 100644 index 00000000000000..5d79dba4163961 --- /dev/null +++ b/packages/edit-navigation/src/store/controls.js @@ -0,0 +1,113 @@ +/** + * WordPress dependencies + */ +import { default as triggerApiFetch } from '@wordpress/api-fetch'; +import { createRegistryControl } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { menuItemsQuery } from './utils'; + +/** + * Trigger an API Fetch request. + * + * @param {Object} request API Fetch Request Object. + * @return {Object} control descriptor. + */ +export function apiFetch( request ) { + return { + type: 'API_FETCH', + request, + }; +} + +export function getNavigationPost( menuId ) { + return { + type: 'SELECT', + registryName: 'core/edit-navigation', + selectorName: 'getNavigationPost', + args: [ menuId ], + }; +} + +export function resolveMenuItems( menuId ) { + return { + type: 'RESOLVE_SELECT', + registryName: 'core', + selectorName: 'getMenuItems', + args: [ menuItemsQuery( menuId ) ], + }; +} + +/** + * Calls a selector using the current state. + * + * @param {string} registryName + * @param {string} selectorName + * @param {Array} args Selector arguments. + * @return {Object} control descriptor. + */ +export function select( registryName, selectorName, ...args ) { + return { + type: 'SELECT', + registryName, + selectorName, + args, + }; +} + +/** + * Dispatches a control action for triggering a registry select that has a + * resolver. + * + * @param {string} registryName + * @param {string} selectorName + * @param {Array} args Arguments for the select. + * @return {Object} control descriptor. + */ +export function resolveSelect( registryName, selectorName, ...args ) { + return { + type: 'RESOLVE_SELECT', + registryName, + selectorName, + args, + }; +} + +export function dispatch( registryName, actionName, ...args ) { + return { + type: 'DISPATCH', + registryName, + actionName, + args, + }; +} + +const controls = { + API_FETCH( { request } ) { + return triggerApiFetch( request ); + }, + + SELECT: createRegistryControl( + ( registry ) => ( { registryName, selectorName, args } ) => { + return registry.select( registryName )[ selectorName ]( ...args ); + } + ), + + DISPATCH: createRegistryControl( + ( registry ) => ( { registryName, actionName, args } ) => { + return registry.dispatch( registryName )[ actionName ]( ...args ); + } + ), + + RESOLVE_SELECT: createRegistryControl( + ( registry ) => ( { registryName, selectorName, args } ) => { + return registry + .__experimentalResolveSelect( registryName ) + [ selectorName ]( ...args ); + } + ), +}; + +export default controls; diff --git a/packages/edit-navigation/src/store/index.js b/packages/edit-navigation/src/store/index.js new file mode 100644 index 00000000000000..62ce678b1f8a2f --- /dev/null +++ b/packages/edit-navigation/src/store/index.js @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as resolvers from './resolvers'; +import * as selectors from './selectors'; +import * as actions from './actions'; +import controls from './controls'; + +/** + * Module Constants + */ +const MODULE_KEY = 'core/edit-navigation'; + +/** + * Block editor data store configuration. + * + * @see https://github.com/WordPress/gutenberg/blob/master/packages/data/README.md#registerStore + * + * @type {Object} + */ +export const storeConfig = { + reducer, + controls, + selectors, + resolvers, + actions, +}; + +const store = registerStore( MODULE_KEY, { + ...storeConfig, + persist: [ 'preferences' ], +} ); + +export default store; diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js new file mode 100644 index 00000000000000..8b1801f0c97706 --- /dev/null +++ b/packages/edit-navigation/src/store/reducer.js @@ -0,0 +1,36 @@ +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +function processing( state, { type, menuId, ...rest } ) { + switch ( type ) { + case 'START_PROCESSING_MENU_ITEMS': + state[ menuId ] = { + ...state[ menuId ], + inProgress: true, + }; + break; + case 'FINISH_PROCESSING_MENU_ITEMS': + state[ menuId ] = { + ...state[ menuId ], + inProgress: false, + }; + break; + case 'ENQUEUE_AFTER_PROCESSING': + const pendingActions = state[ menuId ]?.pendingActions || []; + if ( ! pendingActions.includes( rest.action ) ) { + state[ menuId ] = { + ...state[ menuId ], + pendingActions: [ ...pendingActions, rest.action ], + }; + } + break; + } + + return state || {}; +} + +export default combineReducers( { + processing, +} ); diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js new file mode 100644 index 00000000000000..2980f3e7927c6d --- /dev/null +++ b/packages/edit-navigation/src/store/resolvers.js @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import { groupBy, sortBy } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { resolveMenuItems, dispatch } from './controls'; +import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; + +export function* getNavigationPost( menuId ) { + const menuItems = yield resolveMenuItems( menuId ); + + const post = createStubPost( menuId, menuItems ); + yield dispatch( + 'core', + 'receiveEntityRecords', + KIND, + POST_TYPE, + post, + { id: post.id }, + false + ); +} + +const createStubPost = ( menuId, menuItems ) => { + const [ navigationBlock, menuItemIdToClientId ] = createNavigationBlock( + menuItems + ); + const id = buildNavigationPostId( menuId ); + return { + id, + slug: id, + status: 'draft', + type: 'page', + blocks: [ navigationBlock ], + meta: { + menuId, + menuItemIdToClientId, + }, + }; +}; + +function createNavigationBlock( menuItems ) { + const itemsByParentID = groupBy( menuItems, 'parent' ); + const menuItemIdToClientId = {}; + const menuItemsToTreeOfBlocks = ( items ) => { + const innerBlocks = []; + if ( ! items ) { + return; + } + + const sortedItems = sortBy( items, 'menu_order' ); + for ( const item of sortedItems ) { + let menuItemInnerBlocks = []; + if ( itemsByParentID[ item.id ]?.length ) { + menuItemInnerBlocks = menuItemsToTreeOfBlocks( + itemsByParentID[ item.id ] + ); + } + const linkBlock = convertMenuItemToLinkBlock( + item, + menuItemInnerBlocks + ); + menuItemIdToClientId[ item.id ] = linkBlock.clientId; + innerBlocks.push( linkBlock ); + } + return innerBlocks; + }; + + // menuItemsToTreeOfLinkBlocks takes an array of top-level menu items and recursively creates all their innerBlocks + const innerBlocks = menuItemsToTreeOfBlocks( itemsByParentID[ 0 ] || [] ); + const navigationBlock = createBlock( 'core/navigation', {}, innerBlocks ); + return [ navigationBlock, menuItemIdToClientId ]; +} + +function convertMenuItemToLinkBlock( menuItem, innerBlocks = [] ) { + const attributes = { + label: menuItem.title.rendered, + url: menuItem.url, + }; + + return createBlock( 'core/navigation-link', attributes, innerBlocks ); +} diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js new file mode 100644 index 00000000000000..6985ebecb92da7 --- /dev/null +++ b/packages/edit-navigation/src/store/selectors.js @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { invert } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createRegistrySelector } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; + +export function getPendingActions( state, menuId ) { + return state.processing[ menuId ]?.pendingActions || []; +} + +export function isProcessingMenuItems( state, menuId ) { + return state.processing[ menuId ]?.inProgress; +} + +export const getNavigationPost = createRegistrySelector( + ( select ) => ( state, menuId ) => { + return select( 'core' ).getEditedEntityRecord( + KIND, + POST_TYPE, + buildNavigationPostId( menuId ) + ); + } +); + +export const getMenuItemForClientId = createRegistrySelector( + ( select ) => ( state, post, clientId ) => { + const mapping = invert( post.meta.menuItemIdToClientId ); + return select( 'core' ).getMenuItem( mapping[ clientId ] ); + } +); diff --git a/packages/edit-navigation/src/store/utils.js b/packages/edit-navigation/src/store/utils.js new file mode 100644 index 00000000000000..8a02790e83aaca --- /dev/null +++ b/packages/edit-navigation/src/store/utils.js @@ -0,0 +1,20 @@ +export const KIND = 'root'; +export const POST_TYPE = 'postType'; +export const buildNavigationPostId = ( menuId ) => + `navigation-post-${ menuId }`; + +export function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, ( c ) => { + // eslint-disable-next-line no-restricted-syntax + const a = Math.random() * 16; + // eslint-disable-next-line no-bitwise + const r = a | 0; + // eslint-disable-next-line no-bitwise + const v = c === 'x' ? r : ( r & 0x3 ) | 0x8; + return v.toString( 16 ); + } ); +} + +export function menuItemsQuery( menuId ) { + return { menus: menuId, per_page: -1 }; +} From c05a9f99854ce30c277023b4be0b71a2d5e5f2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 14:29:36 +0200 Subject: [PATCH 02/30] menu editor -> navigation editor --- .../src/components/menus-editor/index.js | 4 +- .../block-editor-area.js | 0 .../helpers.js | 0 .../index.js | 82 +++++++++---------- .../navigation-structure-area.js | 0 .../shortcuts.js | 0 .../style.scss | 0 .../use-navigation-block-editor.js | 0 .../edit-navigation/src/store/resolvers.js | 18 +++- .../edit-navigation/src/store/selectors.js | 5 ++ packages/edit-navigation/src/style.scss | 2 +- 11 files changed, 59 insertions(+), 52 deletions(-) rename packages/edit-navigation/src/components/{menu-editor => navigation-editor}/block-editor-area.js (100%) rename packages/edit-navigation/src/components/{menu-editor => navigation-editor}/helpers.js (100%) rename packages/edit-navigation/src/components/{menu-editor => navigation-editor}/index.js (51%) rename packages/edit-navigation/src/components/{menu-editor => navigation-editor}/navigation-structure-area.js (100%) rename packages/edit-navigation/src/components/{menu-editor => navigation-editor}/shortcuts.js (100%) rename packages/edit-navigation/src/components/{menu-editor => navigation-editor}/style.scss (100%) rename packages/edit-navigation/src/components/{menu-editor => navigation-editor}/use-navigation-block-editor.js (100%) diff --git a/packages/edit-navigation/src/components/menus-editor/index.js b/packages/edit-navigation/src/components/menus-editor/index.js index 04507772174cbd..a602ef60cb1957 100644 --- a/packages/edit-navigation/src/components/menus-editor/index.js +++ b/packages/edit-navigation/src/components/menus-editor/index.js @@ -16,7 +16,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import CreateMenuArea from './create-menu-area'; -import MenuEditor from '../menu-editor'; +import NavigationEditor from '../navigation-editor'; export default function MenusEditor( { blockEditorSettings } ) { const { menus, hasLoadedMenus } = useSelect( ( select ) => { @@ -111,7 +111,7 @@ export default function MenusEditor( { blockEditorSettings } ) { /> ) } { hasMenus && ( - { diff --git a/packages/edit-navigation/src/components/menu-editor/block-editor-area.js b/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js similarity index 100% rename from packages/edit-navigation/src/components/menu-editor/block-editor-area.js rename to packages/edit-navigation/src/components/navigation-editor/block-editor-area.js diff --git a/packages/edit-navigation/src/components/menu-editor/helpers.js b/packages/edit-navigation/src/components/navigation-editor/helpers.js similarity index 100% rename from packages/edit-navigation/src/components/menu-editor/helpers.js rename to packages/edit-navigation/src/components/navigation-editor/helpers.js diff --git a/packages/edit-navigation/src/components/menu-editor/index.js b/packages/edit-navigation/src/components/navigation-editor/index.js similarity index 51% rename from packages/edit-navigation/src/components/menu-editor/index.js rename to packages/edit-navigation/src/components/navigation-editor/index.js index afbf04177cd3ef..7f143570fbf0b5 100644 --- a/packages/edit-navigation/src/components/menu-editor/index.js +++ b/packages/edit-navigation/src/components/navigation-editor/index.js @@ -6,6 +6,7 @@ import { BlockEditorProvider, } from '@wordpress/block-editor'; import { useViewportMatch } from '@wordpress/compose'; +import { Spinner } from '@wordpress/components'; /** * Internal dependencies @@ -16,61 +17,52 @@ import NavigationStructureArea from './navigation-structure-area'; import useNavigationBlockEditor from './use-navigation-block-editor'; import { useDispatch, useSelect } from '@wordpress/data'; -export default function MenuEditor( { +export default function NavigationEditor( { menuId, blockEditorSettings, onDeleteMenu, } ) { - const post = useSelect( ( select ) => - select( 'core/edit-navigation' ).getNavigationPost( menuId ) - ); + const { post, isLoading } = useSelect( ( select ) => ( { + post: select( 'core/edit-navigation' ).getNavigationPost( menuId ), + isLoading: select( 'core/edit-navigation' ).isResolvingNavigationPost( + menuId + ), + } ) ); + const isLargeViewport = useViewportMatch( 'medium' ); + const [ blocks, onInput, onChange ] = useNavigationBlockEditor( post ); + const { saveNavigationPost } = useDispatch( 'core/edit-navigation' ); + const save = () => saveNavigationPost( post ); return (
- { post && ( - + { isLoading ? ( + + ) : ( + + + + + + ) }
); } - -const NavigationBlockEditorProvider = ( { - post, - blockEditorSettings, - onDeleteMenu, -} ) => { - const isLargeViewport = useViewportMatch( 'medium' ); - const [ blocks, onInput, onChange ] = useNavigationBlockEditor( post ); - const { saveNavigationPost } = useDispatch( 'core/edit-navigation' ); - const save = () => saveNavigationPost( post ); - return ( - - - - - - - ); -}; diff --git a/packages/edit-navigation/src/components/menu-editor/navigation-structure-area.js b/packages/edit-navigation/src/components/navigation-editor/navigation-structure-area.js similarity index 100% rename from packages/edit-navigation/src/components/menu-editor/navigation-structure-area.js rename to packages/edit-navigation/src/components/navigation-editor/navigation-structure-area.js diff --git a/packages/edit-navigation/src/components/menu-editor/shortcuts.js b/packages/edit-navigation/src/components/navigation-editor/shortcuts.js similarity index 100% rename from packages/edit-navigation/src/components/menu-editor/shortcuts.js rename to packages/edit-navigation/src/components/navigation-editor/shortcuts.js diff --git a/packages/edit-navigation/src/components/menu-editor/style.scss b/packages/edit-navigation/src/components/navigation-editor/style.scss similarity index 100% rename from packages/edit-navigation/src/components/menu-editor/style.scss rename to packages/edit-navigation/src/components/navigation-editor/style.scss diff --git a/packages/edit-navigation/src/components/menu-editor/use-navigation-block-editor.js b/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js similarity index 100% rename from packages/edit-navigation/src/components/menu-editor/use-navigation-block-editor.js rename to packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index 2980f3e7927c6d..a4c44a7771e520 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -15,10 +15,20 @@ import { resolveMenuItems, dispatch } from './controls'; import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; export function* getNavigationPost( menuId ) { + // Ensure an empty post to start with + yield persistPost( + createStubPost( menuId, [], { + isResolving: true, + } ) + ); + + // Now let's create a proper one hydrated with actual menu items const menuItems = yield resolveMenuItems( menuId ); + yield persistPost( createStubPost( menuId, menuItems ) ); +} - const post = createStubPost( menuId, menuItems ); - yield dispatch( +const persistPost = ( post ) => + dispatch( 'core', 'receiveEntityRecords', KIND, @@ -27,9 +37,8 @@ export function* getNavigationPost( menuId ) { { id: post.id }, false ); -} -const createStubPost = ( menuId, menuItems ) => { +const createStubPost = ( menuId, menuItems, meta ) => { const [ navigationBlock, menuItemIdToClientId ] = createNavigationBlock( menuItems ); @@ -41,6 +50,7 @@ const createStubPost = ( menuId, menuItems ) => { type: 'page', blocks: [ navigationBlock ], meta: { + ...meta, menuId, menuItemIdToClientId, }, diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 6985ebecb92da7..207d88b3631b03 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -31,6 +31,11 @@ export const getNavigationPost = createRegistrySelector( } ); +export const isResolvingNavigationPost = ( state, menuId ) => { + const post = getNavigationPost( state, menuId ); + return ! post || post.meta.isResolving; +}; + export const getMenuItemForClientId = createRegistrySelector( ( select ) => ( state, post, clientId ) => { const mapping = invert( post.meta.menuItemIdToClientId ); diff --git a/packages/edit-navigation/src/style.scss b/packages/edit-navigation/src/style.scss index 9146298802b266..82b1896c579564 100644 --- a/packages/edit-navigation/src/style.scss +++ b/packages/edit-navigation/src/style.scss @@ -5,7 +5,7 @@ } @import "./components/layout/style.scss"; -@import "./components/menu-editor/style.scss"; +@import "./components/navigation-editor/style.scss"; @import "./components/menus-editor/style.scss"; @import "./components/delete-menu-button/style.scss"; @import "./components/notices/style.scss"; From 38f2ffc99f3798406e98d3ee4c25d29f44198f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 14:38:16 +0200 Subject: [PATCH 03/30] Rename CSS classes and shortcuts components to reflect menu vs navigation --- .../navigation-editor/block-editor-area.js | 6 +++--- .../src/components/navigation-editor/index.js | 8 ++++---- .../navigation-editor/navigation-structure-area.js | 6 +++--- .../src/components/navigation-editor/shortcuts.js | 10 +++++----- .../src/components/navigation-editor/style.scss | 12 ++++++------ 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js b/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js index 9c06ba2c8776e9..31651432ed1b9b 100644 --- a/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js +++ b/packages/edit-navigation/src/components/navigation-editor/block-editor-area.js @@ -66,9 +66,9 @@ export default function BlockEditorArea( { }, [ rootBlockId ] ); return ( - + -
+
{ __( 'Navigation menu' ) }
@@ -79,7 +79,7 @@ export default function BlockEditorArea( { saveNavigationPost( post ); return ( -
+
- + { isLoading ? ( @@ -51,7 +51,7 @@ export default function NavigationEditor( { } } > - + + ) : ( - - + + { __( 'Navigation structure' ) } { content } diff --git a/packages/edit-navigation/src/components/navigation-editor/shortcuts.js b/packages/edit-navigation/src/components/navigation-editor/shortcuts.js index 96889b67a8daa9..02a76d70b6c4ad 100644 --- a/packages/edit-navigation/src/components/navigation-editor/shortcuts.js +++ b/packages/edit-navigation/src/components/navigation-editor/shortcuts.js @@ -6,7 +6,7 @@ import { useDispatch } from '@wordpress/data'; import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { __ } from '@wordpress/i18n'; -function MenuEditorShortcuts( { saveBlocks } ) { +function NavigationEditorShortcuts( { saveBlocks } ) { useShortcut( 'core/edit-navigation/save-menu', useCallback( ( event ) => { @@ -21,13 +21,13 @@ function MenuEditorShortcuts( { saveBlocks } ) { return null; } -function RegisterMenuEditorShortcuts() { +function RegisterNavigationEditorShortcuts() { const { registerShortcut } = useDispatch( 'core/keyboard-shortcuts' ); useEffect( () => { registerShortcut( { name: 'core/edit-navigation/save-menu', category: 'global', - description: __( 'Save the menu currently being edited.' ), + description: __( 'Save the navigation currently being edited.' ), keyCombination: { modifier: 'primary', character: 's', @@ -38,6 +38,6 @@ function RegisterMenuEditorShortcuts() { return null; } -MenuEditorShortcuts.Register = RegisterMenuEditorShortcuts; +NavigationEditorShortcuts.Register = RegisterNavigationEditorShortcuts; -export default MenuEditorShortcuts; +export default NavigationEditorShortcuts; diff --git a/packages/edit-navigation/src/components/navigation-editor/style.scss b/packages/edit-navigation/src/components/navigation-editor/style.scss index 59ce9536123520..2405ad20475f90 100644 --- a/packages/edit-navigation/src/components/navigation-editor/style.scss +++ b/packages/edit-navigation/src/components/navigation-editor/style.scss @@ -1,4 +1,4 @@ -.edit-navigation-menu-editor { +.edit-navigation-editor { display: grid; align-items: self-start; grid-gap: 10px; @@ -13,7 +13,7 @@ } } -.edit-navigation-menu-editor__block-editor-toolbar { +.edit-navigation-editor__block-editor-toolbar { height: 46px; margin-bottom: 12px; border: 1px solid #e2e4e7; @@ -58,7 +58,7 @@ } } -.edit-navigation-menu-editor__navigation-structure-panel { +.edit-navigation-editor__navigation-structure-panel { // IE11 requires the column to be explicitly declared. grid-column: 1; @@ -73,11 +73,11 @@ } } -.edit-navigation-menu-editor__navigation-structure-header { +.edit-navigation-editor__navigation-structure-header { font-weight: bold; } -.edit-navigation-menu-editor__block-editor-area { +.edit-navigation-editor__block-editor-area { @include break-medium { // IE11 requires the column to be explicitly declared. // Only shift this into the second column on desktop. @@ -91,7 +91,7 @@ justify-content: space-between; } - .edit-navigation-menu-editor__block-editor-area-header-text { + .edit-navigation-editor__block-editor-area-header-text { flex-grow: 1; font-weight: bold; } From 9000b88eb05e52d008815233f7d5e5fdc850fb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 14:43:15 +0200 Subject: [PATCH 04/30] "Privatize" concurrency-related actions --- packages/edit-navigation/src/store/actions.js | 50 +++++-------------- 1 file changed, 13 insertions(+), 37 deletions(-) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 2cf3d8f7ca72ae..a92a489b66806a 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -191,30 +191,28 @@ function serializeProcessing( callback ) { 'isProcessingMenuItems', menuId ); + if ( isProcessing ) { - yield dispatch( - 'core/edit-navigation', - 'enqueueAfterProcessing', + yield { + type: 'ENQUEUE_AFTER_PROCESSING', menuId, - callback - ); + action: callback, + }; return { status: 'pending' }; } - yield dispatch( - 'core/edit-navigation', - 'startProcessingMenuItems', - menuId - ); + yield { + type: 'START_PROCESSING_MENU_ITEMS', + menuId, + }; try { yield* callback( menuId ); } finally { - yield dispatch( - 'core/edit-navigation', - 'finishProcessingMenuItems', - menuId - ); + yield { + type: 'FINISH_PROCESSING_MENU_ITEMS', + menuId, + }; const pendingActions = yield select( 'core/edit-navigation', @@ -227,25 +225,3 @@ function serializeProcessing( callback ) { } }; } - -export function startProcessingMenuItems( menuId ) { - return { - type: 'START_PROCESSING_MENU_ITEMS', - menuId, - }; -} - -export function finishProcessingMenuItems( menuId ) { - return { - type: 'FINISH_PROCESSING_MENU_ITEMS', - menuId, - }; -} - -export function enqueueAfterProcessing( menuId, action ) { - return { - type: 'ENQUEUE_AFTER_PROCESSING', - menuId, - action, - }; -} From 514167893ec06593b01ff270280347759b4ef766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 14:59:48 +0200 Subject: [PATCH 05/30] Migrate to uuid module --- packages/edit-navigation/package.json | 3 ++- packages/edit-navigation/src/store/actions.js | 5 +++-- packages/edit-navigation/src/store/utils.js | 12 ------------ 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/edit-navigation/package.json b/packages/edit-navigation/package.json index d91e86ab7d1fbb..2b8bba53657ef7 100644 --- a/packages/edit-navigation/package.json +++ b/packages/edit-navigation/package.json @@ -49,7 +49,8 @@ "@wordpress/url": "file:../url", "classnames": "^2.2.5", "lodash": "^4.17.15", - "rememo": "^3.0.0" + "rememo": "^3.0.0", + "uuid": "^7.0.2" }, "publishConfig": { "access": "public" diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index a92a489b66806a..f2736549cd3a5b 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -2,6 +2,7 @@ * External dependencies */ import { invert, keyBy, omit } from 'lodash'; +import { v4 as uuid } from 'uuid'; /** * WordPress dependencies @@ -12,7 +13,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { select, resolveMenuItems, dispatch, apiFetch } from './controls'; -import { uuidv4, menuItemsQuery, KIND, POST_TYPE } from './utils'; +import { menuItemsQuery, KIND, POST_TYPE } from './utils'; // Hits POST /wp/v2/menu-items once for every Link block that doesn't have an // associated menu item. (IDK what a good name for this is.) @@ -111,7 +112,7 @@ function* batchSave( menuId, menuItemsByClientId, navigationBlock ) { body.append( 'wp_customize', 'on' ); body.append( 'customize_theme', stylesheet ); body.append( 'nonce', nonce ); - body.append( 'customize_changeset_uuid', uuidv4() ); + body.append( 'customize_changeset_uuid', uuid() ); body.append( 'customize_autosaved', 'on' ); body.append( 'customize_changeset_status', 'publish' ); body.append( 'action', 'customize_save' ); diff --git a/packages/edit-navigation/src/store/utils.js b/packages/edit-navigation/src/store/utils.js index 8a02790e83aaca..e61e87e851ddf2 100644 --- a/packages/edit-navigation/src/store/utils.js +++ b/packages/edit-navigation/src/store/utils.js @@ -3,18 +3,6 @@ export const POST_TYPE = 'postType'; export const buildNavigationPostId = ( menuId ) => `navigation-post-${ menuId }`; -export function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( /[xy]/g, ( c ) => { - // eslint-disable-next-line no-restricted-syntax - const a = Math.random() * 16; - // eslint-disable-next-line no-bitwise - const r = a | 0; - // eslint-disable-next-line no-bitwise - const v = c === 'x' ? r : ( r & 0x3 ) | 0x8; - return v.toString( 16 ); - } ); -} - export function menuItemsQuery( menuId ) { return { menus: menuId, per_page: -1 }; } From 23b24e6c020e65c59009278b836d18e9f91ff387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 15:46:31 +0200 Subject: [PATCH 06/30] Remove persist: [ 'preferences' ] --- packages/block-editor/src/store/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index bfc7766a508762..3c3932d8a41620 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -31,10 +31,7 @@ export const storeConfig = { controls, }; -const store = registerStore( MODULE_KEY, { - ...storeConfig, - persist: [ 'preferences' ], -} ); +const store = registerStore( MODULE_KEY, storeConfig ); applyMiddlewares( store ); export default store; From ebf95869c234560ca29732cb70dfae29b6e82f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 16:20:06 +0200 Subject: [PATCH 07/30] Remove processing-related selectors in favor of controls which are not a public API --- packages/edit-navigation/src/store/actions.js | 36 +++++++++---------- .../edit-navigation/src/store/controls.js | 28 +++++++++++++++ packages/edit-navigation/src/store/reducer.js | 20 +++++------ .../edit-navigation/src/store/selectors.js | 8 ----- 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index f2736549cd3a5b..e6dd0a485bc4b1 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -12,7 +12,13 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { select, resolveMenuItems, dispatch, apiFetch } from './controls'; +import { + getPendingActions, + isProcessingPost, + resolveMenuItems, + dispatch, + apiFetch, +} from './controls'; import { menuItemsQuery, KIND, POST_TYPE } from './utils'; // Hits POST /wp/v2/menu-items once for every Link block that doesn't have an @@ -186,42 +192,34 @@ function computeCustomizedAttribute( blocks, menuId, menuItemsByClientId ) { } function serializeProcessing( callback ) { - return function* ( menuId ) { - const isProcessing = yield select( - 'core/edit-navigation', - 'isProcessingMenuItems', - menuId - ); + return function* ( post ) { + const isProcessing = yield isProcessingPost( post.id ); if ( isProcessing ) { yield { type: 'ENQUEUE_AFTER_PROCESSING', - menuId, + id: post.id, action: callback, }; return { status: 'pending' }; } yield { - type: 'START_PROCESSING_MENU_ITEMS', - menuId, + type: 'START_PROCESSING_POST', + id: post.id, }; try { - yield* callback( menuId ); + yield* callback( post ); } finally { yield { - type: 'FINISH_PROCESSING_MENU_ITEMS', - menuId, + type: 'FINISH_PROCESSING_POST', + id: post.id, }; - const pendingActions = yield select( - 'core/edit-navigation', - 'getPendingActions', - menuId - ); + const pendingActions = yield getPendingActions( post.id ); if ( pendingActions.length ) { - yield* pendingActions[ 0 ]( menuId ); + yield* pendingActions[ 0 ]( post ); } } }; diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index 5d79dba4163961..36ebce4e27a449 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -22,6 +22,20 @@ export function apiFetch( request ) { }; } +export function getPendingActions( postId ) { + return { + type: 'GET_PENDING_ACTIONS', + id: postId, + }; +} + +export function isProcessingPost( postId ) { + return { + type: 'IS_PROCESSING_POST', + id: postId, + }; +} + export function getNavigationPost( menuId ) { return { type: 'SELECT', @@ -95,6 +109,20 @@ const controls = { } ), + GET_PENDING_ACTIONS: createRegistryControl( ( registry ) => ( { id } ) => { + const state = registry.stores[ + 'core/edit-navigation' + ].store.getState(); + return state.processing[ id ]?.pendingActions || []; + } ), + + IS_PROCESSING_POST: createRegistryControl( ( registry ) => ( { id } ) => { + const state = registry.stores[ + 'core/edit-navigation' + ].store.getState(); + return state.processing[ id ]?.inProgress; + } ), + DISPATCH: createRegistryControl( ( registry ) => ( { registryName, actionName, args } ) => { return registry.dispatch( registryName )[ actionName ]( ...args ); diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index 8b1801f0c97706..913cd7e087a1ba 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -3,25 +3,25 @@ */ import { combineReducers } from '@wordpress/data'; -function processing( state, { type, menuId, ...rest } ) { +function processing( state, { type, id, ...rest } ) { switch ( type ) { - case 'START_PROCESSING_MENU_ITEMS': - state[ menuId ] = { - ...state[ menuId ], + case 'START_PROCESSING_POST': + state[ id ] = { + ...state[ id ], inProgress: true, }; break; - case 'FINISH_PROCESSING_MENU_ITEMS': - state[ menuId ] = { - ...state[ menuId ], + case 'FINISH_PROCESSING_POST': + state[ id ] = { + ...state[ id ], inProgress: false, }; break; case 'ENQUEUE_AFTER_PROCESSING': - const pendingActions = state[ menuId ]?.pendingActions || []; + const pendingActions = state[ id ]?.pendingActions || []; if ( ! pendingActions.includes( rest.action ) ) { - state[ menuId ] = { - ...state[ menuId ], + state[ id ] = { + ...state[ id ], pendingActions: [ ...pendingActions, rest.action ], }; } diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 207d88b3631b03..fb5c82e1cf42da 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -13,14 +13,6 @@ import { createRegistrySelector } from '@wordpress/data'; */ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; -export function getPendingActions( state, menuId ) { - return state.processing[ menuId ]?.pendingActions || []; -} - -export function isProcessingMenuItems( state, menuId ) { - return state.processing[ menuId ]?.inProgress; -} - export const getNavigationPost = createRegistrySelector( ( select ) => ( state, menuId ) => { return select( 'core' ).getEditedEntityRecord( From c475769157f04a6bf355d86a0613e182687e3778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 16:23:37 +0200 Subject: [PATCH 08/30] Wrap pending action in serializeProcessing() --- packages/edit-navigation/src/store/actions.js | 6 +++++- packages/edit-navigation/src/store/reducer.js | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index e6dd0a485bc4b1..1f4cf6b7b530a1 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -215,11 +215,15 @@ function serializeProcessing( callback ) { yield { type: 'FINISH_PROCESSING_POST', id: post.id, + action: callback, }; const pendingActions = yield getPendingActions( post.id ); if ( pendingActions.length ) { - yield* pendingActions[ 0 ]( post ); + const serializedCallback = serializeProcessing( + pendingActions[ 0 ] + ); + yield* serializedCallback( post ); } } }; diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index 913cd7e087a1ba..4d3908edf1c5b3 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -15,6 +15,10 @@ function processing( state, { type, id, ...rest } ) { state[ id ] = { ...state[ id ], inProgress: false, + pendingActions: + state[ id ]?.pendingActions?.filter( + ( item ) => item !== rest.action + ) || [], }; break; case 'ENQUEUE_AFTER_PROCESSING': From c8a1d836ad8f0687efd2e658f6b91bd9bd167f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 16:26:20 +0200 Subject: [PATCH 09/30] Rename processing to processingQueue --- packages/edit-navigation/src/store/controls.js | 4 ++-- packages/edit-navigation/src/store/reducer.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index 36ebce4e27a449..7ef43da23358c2 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -113,14 +113,14 @@ const controls = { const state = registry.stores[ 'core/edit-navigation' ].store.getState(); - return state.processing[ id ]?.pendingActions || []; + return state.processingQueue[ id ]?.pendingActions || []; } ), IS_PROCESSING_POST: createRegistryControl( ( registry ) => ( { id } ) => { const state = registry.stores[ 'core/edit-navigation' ].store.getState(); - return state.processing[ id ]?.inProgress; + return state.processingQueue[ id ]?.inProgress; } ), DISPATCH: createRegistryControl( diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index 4d3908edf1c5b3..dedeb4208e3e82 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -3,7 +3,7 @@ */ import { combineReducers } from '@wordpress/data'; -function processing( state, { type, id, ...rest } ) { +function processingQueue( state, { type, id, ...rest } ) { switch ( type ) { case 'START_PROCESSING_POST': state[ id ] = { @@ -36,5 +36,5 @@ function processing( state, { type, id, ...rest } ) { } export default combineReducers( { - processing, + processingQueue, } ); From 7343c845fe890de3943b70558399217e81bf74b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 16:57:48 +0200 Subject: [PATCH 10/30] Move the mapping back to state --- packages/edit-navigation/src/store/actions.js | 51 ++++++++++++------- .../edit-navigation/src/store/controls.js | 45 +++++++++++----- packages/edit-navigation/src/store/reducer.js | 27 ++++++---- .../edit-navigation/src/store/resolvers.js | 25 +++++---- .../edit-navigation/src/store/selectors.js | 4 +- 5 files changed, 99 insertions(+), 53 deletions(-) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 1f4cf6b7b530a1..a84629940d27fd 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -13,19 +13,25 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { + getNavigationPost, getPendingActions, isProcessingPost, + getMenuItemToClientIdMapping, resolveMenuItems, dispatch, apiFetch, } from './controls'; -import { menuItemsQuery, KIND, POST_TYPE } from './utils'; +import { menuItemsQuery } from './utils'; // Hits POST /wp/v2/menu-items once for every Link block that doesn't have an // associated menu item. (IDK what a good name for this is.) export const createMissingMenuItems = serializeProcessing( function* ( post ) { const menuId = post.meta.menuId; - const mapping = post.meta.menuItemIdToClientId; + + const mapping = yield { + type: 'GET_MENU_ITEM_TO_CLIENT_ID_MAPPING', + postId: post.id, + }; const clientIdToMenuId = invert( mapping ); const stack = [ post.blocks[ 0 ] ]; @@ -57,23 +63,29 @@ export const createMissingMenuItems = serializeProcessing( function* ( post ) { stack.push( ...block.innerBlocks ); } - yield dispatch( 'core', 'editEntityRecord', KIND, POST_TYPE, post.id, { - meta: { - ...post.meta, - menuItemIdToClientId: mapping, - }, - } ); + yield { + type: 'SET_MENU_ITEM_TO_CLIENT_ID_MAPPING', + postId: post.id, + mapping, + }; } ); export const saveNavigationPost = serializeProcessing( function* ( post ) { const menuId = post.meta.menuId; const menuItemsByClientId = mapMenuItemsByClientId( yield resolveMenuItems( menuId ), - post.meta.menuItemIdToClientId + yield getMenuItemToClientIdMapping( post.id ) ); try { - yield* batchSave( menuId, menuItemsByClientId, post.blocks[ 0 ] ); + const response = yield* batchSave( + menuId, + menuItemsByClientId, + post.blocks[ 0 ] + ); + if ( ! response.success ) { + throw new Error(); + } yield dispatch( 'core/notices', 'createSuccessNotice', @@ -131,7 +143,7 @@ function* batchSave( menuId, menuItemsByClientId, navigationBlock ) { ) ); - yield apiFetch( { + return yield apiFetch( { url: '/wp-admin/admin-ajax.php', method: 'POST', body, @@ -193,12 +205,13 @@ function computeCustomizedAttribute( blocks, menuId, menuItemsByClientId ) { function serializeProcessing( callback ) { return function* ( post ) { - const isProcessing = yield isProcessingPost( post.id ); + const postId = post.id; + const isProcessing = yield isProcessingPost( postId ); if ( isProcessing ) { yield { type: 'ENQUEUE_AFTER_PROCESSING', - id: post.id, + postId, action: callback, }; return { status: 'pending' }; @@ -206,7 +219,7 @@ function serializeProcessing( callback ) { yield { type: 'START_PROCESSING_POST', - id: post.id, + postId, }; try { @@ -214,16 +227,20 @@ function serializeProcessing( callback ) { } finally { yield { type: 'FINISH_PROCESSING_POST', - id: post.id, + postId, action: callback, }; - const pendingActions = yield getPendingActions( post.id ); + const pendingActions = yield getPendingActions( postId ); if ( pendingActions.length ) { const serializedCallback = serializeProcessing( pendingActions[ 0 ] ); - yield* serializedCallback( post ); + + // re-fetch the post as running the callback() likely updated it + yield* serializedCallback( + yield getNavigationPost( post.meta.menuId ) + ); } } }; diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index 7ef43da23358c2..ff5e140559b113 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -25,14 +25,21 @@ export function apiFetch( request ) { export function getPendingActions( postId ) { return { type: 'GET_PENDING_ACTIONS', - id: postId, + postId, }; } export function isProcessingPost( postId ) { return { type: 'IS_PROCESSING_POST', - id: postId, + postId, + }; +} + +export function getMenuItemToClientIdMapping( postId ) { + return { + type: 'GET_MENU_ITEM_TO_CLIENT_ID_MAPPING', + postId, }; } @@ -109,19 +116,26 @@ const controls = { } ), - GET_PENDING_ACTIONS: createRegistryControl( ( registry ) => ( { id } ) => { - const state = registry.stores[ - 'core/edit-navigation' - ].store.getState(); - return state.processingQueue[ id ]?.pendingActions || []; - } ), + GET_PENDING_ACTIONS: createRegistryControl( + ( registry ) => ( { postId } ) => { + return ( + getState( registry ).processingQueue[ postId ] + ?.pendingActions || [] + ); + } + ), + + IS_PROCESSING_POST: createRegistryControl( + ( registry ) => ( { postId } ) => { + return getState( registry ).processingQueue[ postId ]?.inProgress; + } + ), - IS_PROCESSING_POST: createRegistryControl( ( registry ) => ( { id } ) => { - const state = registry.stores[ - 'core/edit-navigation' - ].store.getState(); - return state.processingQueue[ id ]?.inProgress; - } ), + GET_MENU_ITEM_TO_CLIENT_ID_MAPPING: createRegistryControl( + ( registry ) => ( { postId } ) => { + return getState( registry ).mapping[ postId ] || {}; + } + ), DISPATCH: createRegistryControl( ( registry ) => ( { registryName, actionName, args } ) => { @@ -138,4 +152,7 @@ const controls = { ), }; +const getState = ( registry ) => + registry.stores[ 'core/edit-navigation' ].store.getState(); + export default controls; diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index dedeb4208e3e82..db8d072af98bc6 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -3,29 +3,37 @@ */ import { combineReducers } from '@wordpress/data'; -function processingQueue( state, { type, id, ...rest } ) { +function mapping( state, { type, postId, ...rest } ) { + if ( type === 'SET_MENU_ITEM_TO_CLIENT_ID_MAPPING' ) { + state[ postId ] = rest.mapping; + } + + return state || {}; +} + +function processingQueue( state, { type, postId, ...rest } ) { switch ( type ) { case 'START_PROCESSING_POST': - state[ id ] = { - ...state[ id ], + state[ postId ] = { + ...state[ postId ], inProgress: true, }; break; case 'FINISH_PROCESSING_POST': - state[ id ] = { - ...state[ id ], + state[ postId ] = { + ...state[ postId ], inProgress: false, pendingActions: - state[ id ]?.pendingActions?.filter( + state[ postId ]?.pendingActions?.filter( ( item ) => item !== rest.action ) || [], }; break; case 'ENQUEUE_AFTER_PROCESSING': - const pendingActions = state[ id ]?.pendingActions || []; + const pendingActions = state[ postId ]?.pendingActions || []; if ( ! pendingActions.includes( rest.action ) ) { - state[ id ] = { - ...state[ id ], + state[ postId ] = { + ...state[ postId ], pendingActions: [ ...pendingActions, rest.action ], }; } @@ -36,5 +44,6 @@ function processingQueue( state, { type, id, ...rest } ) { } export default combineReducers( { + mapping, processingQueue, } ); diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index a4c44a7771e520..9c7543eb913efb 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -16,15 +16,22 @@ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; export function* getNavigationPost( menuId ) { // Ensure an empty post to start with - yield persistPost( - createStubPost( menuId, [], { - isResolving: true, - } ) - ); + const stubPost = createStubPost( menuId, null, { + isResolving: true, + } ); + yield persistPost( stubPost ); // Now let's create a proper one hydrated with actual menu items const menuItems = yield resolveMenuItems( menuId ); - yield persistPost( createStubPost( menuId, menuItems ) ); + const [ navigationBlock, menuItemIdToClientId ] = createNavigationBlock( + menuItems + ); + yield { + type: 'SET_MENU_ITEM_TO_CLIENT_ID_MAPPING', + postId: stubPost.id, + mapping: menuItemIdToClientId, + }; + yield persistPost( createStubPost( menuId, navigationBlock ) ); } const persistPost = ( post ) => @@ -38,10 +45,7 @@ const persistPost = ( post ) => false ); -const createStubPost = ( menuId, menuItems, meta ) => { - const [ navigationBlock, menuItemIdToClientId ] = createNavigationBlock( - menuItems - ); +const createStubPost = ( menuId, navigationBlock, meta ) => { const id = buildNavigationPostId( menuId ); return { id, @@ -52,7 +56,6 @@ const createStubPost = ( menuId, menuItems, meta ) => { meta: { ...meta, menuId, - menuItemIdToClientId, }, }; }; diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index fb5c82e1cf42da..2b9dc31f2c0ed9 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -29,8 +29,8 @@ export const isResolvingNavigationPost = ( state, menuId ) => { }; export const getMenuItemForClientId = createRegistrySelector( - ( select ) => ( state, post, clientId ) => { - const mapping = invert( post.meta.menuItemIdToClientId ); + ( select ) => ( state, postId, clientId ) => { + const mapping = invert( state.mapping[ postId ] ); return select( 'core' ).getMenuItem( mapping[ clientId ] ); } ); From ed929a8e00ade42d99fd14732a0504f6e54899b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 16:59:36 +0200 Subject: [PATCH 11/30] Remove general resolveSelect control --- .../edit-navigation/src/store/controls.js | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index ff5e140559b113..6d042a26fb1a42 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -54,10 +54,8 @@ export function getNavigationPost( menuId ) { export function resolveMenuItems( menuId ) { return { - type: 'RESOLVE_SELECT', - registryName: 'core', - selectorName: 'getMenuItems', - args: [ menuItemsQuery( menuId ) ], + type: 'RESOLVE_MENU_ITEMS', + query: menuItemsQuery( menuId ), }; } @@ -78,24 +76,6 @@ export function select( registryName, selectorName, ...args ) { }; } -/** - * Dispatches a control action for triggering a registry select that has a - * resolver. - * - * @param {string} registryName - * @param {string} selectorName - * @param {Array} args Arguments for the select. - * @return {Object} control descriptor. - */ -export function resolveSelect( registryName, selectorName, ...args ) { - return { - type: 'RESOLVE_SELECT', - registryName, - selectorName, - args, - }; -} - export function dispatch( registryName, actionName, ...args ) { return { type: 'DISPATCH', @@ -143,11 +123,11 @@ const controls = { } ), - RESOLVE_SELECT: createRegistryControl( - ( registry ) => ( { registryName, selectorName, args } ) => { + RESOLVE_MENU_ITEMS: createRegistryControl( + ( registry ) => ( { query } ) => { return registry - .__experimentalResolveSelect( registryName ) - [ selectorName ]( ...args ); + .__experimentalResolveSelect( 'core' ) + .getMenuItems( query ); } ), }; From 79e8b4160b694975d6662b7afb6390cebf7e7283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 17:25:39 +0200 Subject: [PATCH 12/30] Add undo/redo shortcuts --- .../components/navigation-editor/shortcuts.js | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/edit-navigation/src/components/navigation-editor/shortcuts.js b/packages/edit-navigation/src/components/navigation-editor/shortcuts.js index 02a76d70b6c4ad..d75d3f176e2e40 100644 --- a/packages/edit-navigation/src/components/navigation-editor/shortcuts.js +++ b/packages/edit-navigation/src/components/navigation-editor/shortcuts.js @@ -18,6 +18,25 @@ function NavigationEditorShortcuts( { saveBlocks } ) { } ); + const { redo, undo } = useDispatch( 'core' ); + useShortcut( + 'core/edit-navigation/undo', + ( event ) => { + undo(); + event.preventDefault(); + }, + { bindGlobal: true } + ); + + useShortcut( + 'core/edit-navigation/redo', + ( event ) => { + redo(); + event.preventDefault(); + }, + { bindGlobal: true } + ); + return null; } @@ -33,6 +52,24 @@ function RegisterNavigationEditorShortcuts() { character: 's', }, } ); + registerShortcut( { + name: 'core/edit-navigation/undo', + category: 'global', + description: __( 'Undo your last changes.' ), + keyCombination: { + modifier: 'primary', + character: 'z', + }, + } ); + registerShortcut( { + name: 'core/edit-navigation/redo', + category: 'global', + description: __( 'Redo your last undo.' ), + keyCombination: { + modifier: 'primaryShift', + character: 'z', + }, + } ); }, [ registerShortcut ] ); return null; From 42e2ffd9888585cc5e53cea984e7495daf2398c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 17 Jun 2020 17:34:18 +0200 Subject: [PATCH 13/30] Remove persist option in proper store --- packages/block-editor/src/store/index.js | 5 ++++- packages/edit-navigation/src/store/index.js | 5 +---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 3c3932d8a41620..bfc7766a508762 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -31,7 +31,10 @@ export const storeConfig = { controls, }; -const store = registerStore( MODULE_KEY, storeConfig ); +const store = registerStore( MODULE_KEY, { + ...storeConfig, + persist: [ 'preferences' ], +} ); applyMiddlewares( store ); export default store; diff --git a/packages/edit-navigation/src/store/index.js b/packages/edit-navigation/src/store/index.js index 62ce678b1f8a2f..78da40045a09b5 100644 --- a/packages/edit-navigation/src/store/index.js +++ b/packages/edit-navigation/src/store/index.js @@ -32,9 +32,6 @@ export const storeConfig = { actions, }; -const store = registerStore( MODULE_KEY, { - ...storeConfig, - persist: [ 'preferences' ], -} ); +const store = registerStore( MODULE_KEY, storeConfig ); export default store; From 47709c4bce8ffe42eb5c8de4ce6626ba0b76971e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 18 Jun 2020 15:59:45 +0200 Subject: [PATCH 14/30] Use the canonical way of keeping track of post resolution (and fix the rogue http query) --- .../src/components/navigation-editor/index.js | 70 +++++++++++-------- .../edit-navigation/src/store/resolvers.js | 20 ++++-- .../edit-navigation/src/store/selectors.js | 19 +++-- 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/packages/edit-navigation/src/components/navigation-editor/index.js b/packages/edit-navigation/src/components/navigation-editor/index.js index eeef56efc56ee9..dee25989c5dfee 100644 --- a/packages/edit-navigation/src/components/navigation-editor/index.js +++ b/packages/edit-navigation/src/components/navigation-editor/index.js @@ -17,52 +17,62 @@ import NavigationStructureArea from './navigation-structure-area'; import useNavigationBlockEditor from './use-navigation-block-editor'; import { useDispatch, useSelect } from '@wordpress/data'; -export default function NavigationEditor( { +export default function NavigationEditorWrapper( { menuId, blockEditorSettings, onDeleteMenu, } ) { - const { post, isLoading } = useSelect( ( select ) => ( { + const { post, hasResolved } = useSelect( ( select ) => ( { post: select( 'core/edit-navigation' ).getNavigationPost( menuId ), - isLoading: select( 'core/edit-navigation' ).isResolvingNavigationPost( + hasResolved: select( 'core/edit-navigation' ).hasResolvedNavigationPost( menuId ), } ) ); - const isLargeViewport = useViewportMatch( 'medium' ); - const [ blocks, onInput, onChange ] = useNavigationBlockEditor( post ); - const { saveNavigationPost } = useDispatch( 'core/edit-navigation' ); - const save = () => saveNavigationPost( post ); return (
- { isLoading ? ( + { ! hasResolved ? ( ) : ( - - - - - - + ) }
); } + +function NavigationPostEditor( { post, blockEditorSettings, onDeleteMenu } ) { + const isLargeViewport = useViewportMatch( 'medium' ); + const [ blocks, onInput, onChange ] = useNavigationBlockEditor( post ); + const { saveNavigationPost } = useDispatch( 'core/edit-navigation' ); + const save = () => saveNavigationPost( post ); + return ( + + + + + + + ); +} diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index 9c7543eb913efb..514a1f93a4689d 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -15,13 +15,16 @@ import { resolveMenuItems, dispatch } from './controls'; import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; export function* getNavigationPost( menuId ) { - // Ensure an empty post to start with - const stubPost = createStubPost( menuId, null, { - isResolving: true, - } ); + const stubPost = createStubPost( menuId ); + // Persist an empty post to warm up the state yield persistPost( stubPost ); - // Now let's create a proper one hydrated with actual menu items + // Dispatch startResolution to skip the execution of the real getEntityRecord resolver - it would + // issue an http request and fail. + const args = [ KIND, POST_TYPE, stubPost.id ]; + yield dispatch( 'core', 'startResolution', 'getEntityRecord', args ); + + // Now let's create a proper one hydrated using actual menu items const menuItems = yield resolveMenuItems( menuId ); const [ navigationBlock, menuItemIdToClientId ] = createNavigationBlock( menuItems @@ -31,7 +34,11 @@ export function* getNavigationPost( menuId ) { postId: stubPost.id, mapping: menuItemIdToClientId, }; + // Persist the actual post containing the navigation block yield persistPost( createStubPost( menuId, navigationBlock ) ); + + // Dispatch finishResolution to conclude startResolution dispatched earlier + yield dispatch( 'core', 'finishResolution', 'getEntityRecord', args ); } const persistPost = ( post ) => @@ -45,7 +52,7 @@ const persistPost = ( post ) => false ); -const createStubPost = ( menuId, navigationBlock, meta ) => { +const createStubPost = ( menuId, navigationBlock ) => { const id = buildNavigationPostId( menuId ); return { id, @@ -54,7 +61,6 @@ const createStubPost = ( menuId, navigationBlock, meta ) => { type: 'page', blocks: [ navigationBlock ], meta: { - ...meta, menuId, }, }; diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 2b9dc31f2c0ed9..dd8739e7244d11 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -15,6 +15,12 @@ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; export const getNavigationPost = createRegistrySelector( ( select ) => ( state, menuId ) => { + // When the record is unavailable, calling getEditedEntityRecord triggers a http + // request via it's related resolver. Let's return nothing until getNavigationPost + // resolver marks the record as resolved. + if ( ! hasResolvedNavigationPost( state, menuId ) ) { + return null; + } return select( 'core' ).getEditedEntityRecord( KIND, POST_TYPE, @@ -23,10 +29,15 @@ export const getNavigationPost = createRegistrySelector( } ); -export const isResolvingNavigationPost = ( state, menuId ) => { - const post = getNavigationPost( state, menuId ); - return ! post || post.meta.isResolving; -}; +export const hasResolvedNavigationPost = createRegistrySelector( + ( select ) => ( state, menuId ) => { + return select( 'core' ).hasFinishedResolution( 'getEntityRecord', [ + KIND, + POST_TYPE, + buildNavigationPostId( menuId ), + ] ); + } +); export const getMenuItemForClientId = createRegistrySelector( ( select ) => ( state, postId, clientId ) => { From 93f557510f55a1221f6d07c00e5464128c929ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 18 Jun 2020 16:19:28 +0200 Subject: [PATCH 15/30] Add comments --- packages/edit-navigation/src/store/actions.js | 25 +++++++++++- .../edit-navigation/src/store/controls.js | 16 ++++++-- .../edit-navigation/src/store/resolvers.js | 38 +++++++++++++------ .../edit-navigation/src/store/selectors.js | 10 +++++ 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index a84629940d27fd..5020cb2fd54c34 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -23,8 +23,13 @@ import { } from './controls'; import { menuItemsQuery } from './utils'; -// Hits POST /wp/v2/menu-items once for every Link block that doesn't have an -// associated menu item. (IDK what a good name for this is.) +/** + * Creates a menu item for every block that doesn't have an associated menuItem. + * Requests POST /wp/v2/menu-items once for every menu item created. + * + * @param {Object} post A navigation post to process + * @return {Function} An action creator + */ export const createMissingMenuItems = serializeProcessing( function* ( post ) { const menuId = post.meta.menuId; @@ -70,6 +75,12 @@ export const createMissingMenuItems = serializeProcessing( function* ( post ) { }; } ); +/** + * Converts all the blocks into menu items and submits a batch request to save everything at once. + * + * @param {Object} post A navigation post to process + * @return {Function} An action creator + */ export const saveNavigationPost = serializeProcessing( function* ( post ) { const menuId = post.meta.menuId; const menuItemsByClientId = mapMenuItemsByClientId( @@ -203,6 +214,16 @@ function computeCustomizedAttribute( blocks, menuId, menuItemsByClientId ) { } } +/** + * This wrapper guarantees serial execution of data processing actions. + * + * Examples: + * * saveNavigationPost() needs to wait for all the missing items to be created. + * * Concurrent createMissingMenuItems() could result in sending more requests than required. + * + * @param {Function} callback An action creator to wrap + * @return {Function} Original callback wrapped in a serial execution context + */ function serializeProcessing( callback ) { return function* ( post ) { const postId = post.id; diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index 6d042a26fb1a42..57f2eb3a0ec8c3 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -60,11 +60,11 @@ export function resolveMenuItems( menuId ) { } /** - * Calls a selector using the current state. + * Calls a selector using chosen registry. * - * @param {string} registryName - * @param {string} selectorName - * @param {Array} args Selector arguments. + * @param {string} registryName Registry name. + * @param {string} selectorName Selector name. + * @param {Array} args Selector arguments. * @return {Object} control descriptor. */ export function select( registryName, selectorName, ...args ) { @@ -76,6 +76,14 @@ export function select( registryName, selectorName, ...args ) { }; } +/** + * Dispatches an action using chosen registry. + * + * @param {string} registryName Registry name. + * @param {string} actionName Action name. + * @param {Array} args Selector arguments. + * @return {Object} control descriptor. + */ export function dispatch( registryName, actionName, ...args ) { return { type: 'DISPATCH', diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index 514a1f93a4689d..a1546c5772ab34 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -14,6 +14,16 @@ import { createBlock } from '@wordpress/blocks'; import { resolveMenuItems, dispatch } from './controls'; import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; +/** + * Creates a "stub" navigation post reflecting the contents of menu with id=menuId. The + * post is meant as a convenient to only exists in runtime and should never be saved. It + * enables a convenient way of editing the navigation by using a regular post editor. + * + * Fetches all menu items, converts them into blocks, and hydrates a new post with them. + * + * @param {number} menuId The id of menu to create a post from + * @return {void} + */ export function* getNavigationPost( menuId ) { const stubPost = createStubPost( menuId ); // Persist an empty post to warm up the state @@ -41,17 +51,6 @@ export function* getNavigationPost( menuId ) { yield dispatch( 'core', 'finishResolution', 'getEntityRecord', args ); } -const persistPost = ( post ) => - dispatch( - 'core', - 'receiveEntityRecords', - KIND, - POST_TYPE, - post, - { id: post.id }, - false - ); - const createStubPost = ( menuId, navigationBlock ) => { const id = buildNavigationPostId( menuId ); return { @@ -66,6 +65,23 @@ const createStubPost = ( menuId, navigationBlock ) => { }; }; +const persistPost = ( post ) => + dispatch( + 'core', + 'receiveEntityRecords', + KIND, + POST_TYPE, + post, + { id: post.id }, + false + ); + +/** + * Converts an adjacency list of menuItems into a navigation block. + * + * @param {Array} menuItems a list of menu items + * @return {Object} Navigation block + */ function createNavigationBlock( menuItems ) { const itemsByParentID = groupBy( menuItems, 'parent' ); const menuItemIdToClientId = {}; diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index dd8739e7244d11..0a31065f306d52 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -13,6 +13,9 @@ import { createRegistrySelector } from '@wordpress/data'; */ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; +/** + * @see resolvers.js + */ export const getNavigationPost = createRegistrySelector( ( select ) => ( state, menuId ) => { // When the record is unavailable, calling getEditedEntityRecord triggers a http @@ -39,6 +42,13 @@ export const hasResolvedNavigationPost = createRegistrySelector( } ); +/** + * Returns a menu item represented by the block with id clientId. + * + * @param {number} postId Navigation post id + * @param {number} clientId Block clientId + * @return {Object|null} Menu item entity + */ export const getMenuItemForClientId = createRegistrySelector( ( select ) => ( state, postId, clientId ) => { const mapping = invert( state.mapping[ postId ] ); From 03a9147f945275ad7b79cd886ae93b02ee9b7cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 18 Jun 2020 16:37:33 +0200 Subject: [PATCH 16/30] Enforce menuId to be string --- .../edit-navigation/src/components/navigation-editor/index.js | 2 +- packages/edit-navigation/src/store/resolvers.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-navigation/src/components/navigation-editor/index.js b/packages/edit-navigation/src/components/navigation-editor/index.js index dee25989c5dfee..183f60c72108bb 100644 --- a/packages/edit-navigation/src/components/navigation-editor/index.js +++ b/packages/edit-navigation/src/components/navigation-editor/index.js @@ -23,7 +23,7 @@ export default function NavigationEditorWrapper( { onDeleteMenu, } ) { const { post, hasResolved } = useSelect( ( select ) => ( { - post: select( 'core/edit-navigation' ).getNavigationPost( menuId ), + post: select( 'core/edit-navigation' ).getNavigationPost( menuId + '' ), hasResolved: select( 'core/edit-navigation' ).hasResolvedNavigationPost( menuId ), diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index a1546c5772ab34..c595b8b4656cbf 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -21,7 +21,7 @@ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; * * Fetches all menu items, converts them into blocks, and hydrates a new post with them. * - * @param {number} menuId The id of menu to create a post from + * @param {string} menuId The id of menu to create a post from * @return {void} */ export function* getNavigationPost( menuId ) { From 5c3c387d7bae776be330d9b3e92cd78a4af7c5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 18 Jun 2020 16:49:59 +0200 Subject: [PATCH 17/30] Add unit tests for mapping reducer --- packages/edit-navigation/src/store/reducer.js | 44 +++++++++------- .../edit-navigation/src/store/test/reducer.js | 50 +++++++++++++++++++ 2 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 packages/edit-navigation/src/store/test/reducer.js diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index db8d072af98bc6..d308231b6b7859 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -3,38 +3,46 @@ */ import { combineReducers } from '@wordpress/data'; -function mapping( state, { type, postId, ...rest } ) { +export function mapping( state, { type, postId, ...rest } ) { if ( type === 'SET_MENU_ITEM_TO_CLIENT_ID_MAPPING' ) { - state[ postId ] = rest.mapping; + return { ...state, [ postId ]: rest.mapping }; } return state || {}; } -function processingQueue( state, { type, postId, ...rest } ) { +export function processingQueue( state, { type, postId, ...rest } ) { switch ( type ) { case 'START_PROCESSING_POST': - state[ postId ] = { - ...state[ postId ], - inProgress: true, + return { + ...state, + [ postId ]: { + ...state[ postId ], + inProgress: true, + }, }; - break; + case 'FINISH_PROCESSING_POST': - state[ postId ] = { - ...state[ postId ], - inProgress: false, - pendingActions: - state[ postId ]?.pendingActions?.filter( - ( item ) => item !== rest.action - ) || [], + return { + ...state, + [ postId ]: { + inProgress: false, + pendingActions: + state[ postId ]?.pendingActions?.filter( + ( item ) => item !== rest.action + ) || [], + }, }; - break; + case 'ENQUEUE_AFTER_PROCESSING': const pendingActions = state[ postId ]?.pendingActions || []; if ( ! pendingActions.includes( rest.action ) ) { - state[ postId ] = { - ...state[ postId ], - pendingActions: [ ...pendingActions, rest.action ], + return { + ...state, + [ postId ]: { + ...state[ postId ], + pendingActions: [ ...pendingActions, rest.action ], + }, }; } break; diff --git a/packages/edit-navigation/src/store/test/reducer.js b/packages/edit-navigation/src/store/test/reducer.js new file mode 100644 index 00000000000000..06aaebf8a28072 --- /dev/null +++ b/packages/edit-navigation/src/store/test/reducer.js @@ -0,0 +1,50 @@ +/** + * Internal dependencies + */ +import { mapping } from '../reducer'; + +describe( 'mapping', () => { + it( 'should initialize empty mapping when there is no original state', () => { + expect( mapping( null, {} ) ).toEqual( {} ); + } ); + + it( 'should add the mapping to state', () => { + const originalState = {}; + const newState = mapping( originalState, { + type: 'SET_MENU_ITEM_TO_CLIENT_ID_MAPPING', + postId: 1, + mapping: { a: 'b' }, + } ); + expect( newState ).not.toBe( originalState ); + expect( newState ).toEqual( { + 1: { + a: 'b', + }, + } ); + } ); + + it( 'should replace the mapping in state', () => { + const originalState = { + 1: { + c: 'd', + }, + 2: { + e: 'f', + }, + }; + const newState = mapping( originalState, { + type: 'SET_MENU_ITEM_TO_CLIENT_ID_MAPPING', + postId: 1, + mapping: { g: 'h' }, + } ); + expect( newState ).toEqual( { + 1: { + g: 'h', + }, + 2: { + e: 'f', + }, + } ); + } ); +} ); + From b994b1a336ad5386a1c3bd0539c8a0f07aa2cd3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 18 Jun 2020 17:08:14 +0200 Subject: [PATCH 18/30] Add unit tests for processingQueue reducer --- packages/edit-navigation/src/store/actions.js | 6 + packages/edit-navigation/src/store/reducer.js | 17 ++- .../edit-navigation/src/store/test/reducer.js | 129 +++++++++++++++++- 3 files changed, 147 insertions(+), 5 deletions(-) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 5020cb2fd54c34..2e2848d9d9c035 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -238,6 +238,12 @@ function serializeProcessing( callback ) { return { status: 'pending' }; } + yield { + type: 'POP_PENDING_ACTION', + postId, + action: callback, + }; + yield { type: 'START_PROCESSING_POST', postId, diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index d308231b6b7859..8105c018fac268 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -26,14 +26,23 @@ export function processingQueue( state, { type, postId, ...rest } ) { return { ...state, [ postId ]: { + ...state[ postId ], inProgress: false, - pendingActions: - state[ postId ]?.pendingActions?.filter( - ( item ) => item !== rest.action - ) || [], }, }; + case 'POP_PENDING_ACTION': + const postState = state[ postId ] || {}; + if ( 'pendingActions' in postState ) { + postState.pendingActions = postState.pendingActions?.filter( + ( item ) => item !== rest.action + ); + } + return { + ...state, + [ postId ]: postState, + }; + case 'ENQUEUE_AFTER_PROCESSING': const pendingActions = state[ postId ]?.pendingActions || []; if ( ! pendingActions.includes( rest.action ) ) { diff --git a/packages/edit-navigation/src/store/test/reducer.js b/packages/edit-navigation/src/store/test/reducer.js index 06aaebf8a28072..4d7700b96ca0a6 100644 --- a/packages/edit-navigation/src/store/test/reducer.js +++ b/packages/edit-navigation/src/store/test/reducer.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { mapping } from '../reducer'; +import { mapping, processingQueue } from '../reducer'; describe( 'mapping', () => { it( 'should initialize empty mapping when there is no original state', () => { @@ -48,3 +48,130 @@ describe( 'mapping', () => { } ); } ); +describe( 'processingQueue', () => { + it( 'should initialize empty mapping when there is no original state', () => { + expect( processingQueue( null, {} ) ).toEqual( {} ); + } ); + + it( 'ENQUEUE_AFTER_PROCESSING should add an action to pendingActions', () => { + const originalState = {}; + const newState = processingQueue( originalState, { + type: 'ENQUEUE_AFTER_PROCESSING', + postId: 1, + action: 'some action', + } ); + expect( newState ).toEqual( { + 1: { + pendingActions: [ 'some action' ], + }, + } ); + } ); + it( 'ENQUEUE_AFTER_PROCESSING should not add the same action to pendingActions twice', () => { + const state1 = {}; + const state2 = processingQueue( state1, { + type: 'ENQUEUE_AFTER_PROCESSING', + postId: 1, + action: 'some action', + } ); + const state3 = processingQueue( state2, { + type: 'ENQUEUE_AFTER_PROCESSING', + postId: 1, + action: 'some action', + } ); + expect( state3 ).toEqual( { + 1: { + pendingActions: [ 'some action' ], + }, + } ); + const state4 = processingQueue( state3, { + type: 'ENQUEUE_AFTER_PROCESSING', + postId: 1, + action: 'another action', + } ); + expect( state4 ).toEqual( { + 1: { + pendingActions: [ 'some action', 'another action' ], + }, + } ); + } ); + + it( 'START_PROCESSING_POST should mark post as in progress', () => { + const originalState = {}; + const newState = processingQueue( originalState, { + type: 'START_PROCESSING_POST', + postId: 1, + } ); + expect( newState ).not.toBe( originalState ); + expect( newState ).toEqual( { + 1: { + inProgress: true, + }, + } ); + } ); + + it( 'FINISH_PROCESSING_POST should mark post as not in progress', () => { + const originalState = { + 1: { + inProgress: true, + }, + }; + const newState = processingQueue( originalState, { + type: 'FINISH_PROCESSING_POST', + postId: 1, + } ); + expect( newState ).not.toBe( originalState ); + expect( newState ).toEqual( { + 1: { + inProgress: false, + }, + } ); + } ); + + it( 'FINISH_PROCESSING_POST should preserve other state data', () => { + const originalState = { + 1: { + inProgress: true, + a: 1, + }, + 2: { + b: 2, + }, + }; + const newState = processingQueue( originalState, { + type: 'FINISH_PROCESSING_POST', + postId: 1, + } ); + expect( newState ).not.toBe( originalState ); + expect( newState ).toEqual( { + 1: { + inProgress: false, + a: 1, + }, + 2: { + b: 2, + }, + } ); + } ); + + it( 'POP_PENDING_ACTION should remove the action from pendingActions', () => { + const originalState = { + 1: { + pendingActions: [ + 'first action', + 'some action', + 'another action', + ], + }, + }; + const newState = processingQueue( originalState, { + type: 'POP_PENDING_ACTION', + postId: 1, + action: 'some action', + } ); + expect( newState ).toEqual( { + 1: { + pendingActions: [ 'first action', 'another action' ], + }, + } ); + } ); +} ); From 204d354f46cdefba84b61e8e9956af94256e0b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:15:26 +0200 Subject: [PATCH 19/30] Update comment on getNavigationPost selector --- packages/edit-navigation/src/store/actions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 2e2848d9d9c035..743e656e32195c 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -237,7 +237,6 @@ function serializeProcessing( callback ) { }; return { status: 'pending' }; } - yield { type: 'POP_PENDING_ACTION', postId, From 66139baeb94b9c9d87ce5ccf0f89a4090c8634b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:20:03 +0200 Subject: [PATCH 20/30] Update comment on getNavigationPost --- packages/edit-navigation/src/store/selectors.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 0a31065f306d52..295f7331a61b6f 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -14,7 +14,14 @@ import { createRegistrySelector } from '@wordpress/data'; import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; /** - * @see resolvers.js + * Returns a "stub" navigation post reflecting the contents of menu with id=menuId. The + * post is meant as a convenient to only exists in runtime and should never be saved. It + * enables a convenient way of editing the navigation by using a regular post editor. + * + * Related resolver fetches all menu items, converts them into blocks, and hydrates a new post with them. + * + * @param {string} menuId The id of menu to create a post from. + * @return {null|Object} Post once the resolver fetches it, otherwise null */ export const getNavigationPost = createRegistrySelector( ( select ) => ( state, menuId ) => { From 4464c7e1ede6516990ed4fea9ca0df0296108ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:20:38 +0200 Subject: [PATCH 21/30] Ensure POP_PENDING_ACTION action don't mutate the state --- packages/edit-navigation/src/store/reducer.js | 2 +- packages/edit-navigation/src/store/test/reducer.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index 8105c018fac268..83233c221bbd6d 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -32,7 +32,7 @@ export function processingQueue( state, { type, postId, ...rest } ) { }; case 'POP_PENDING_ACTION': - const postState = state[ postId ] || {}; + const postState = { ...state[ postId ] }; if ( 'pendingActions' in postState ) { postState.pendingActions = postState.pendingActions?.filter( ( item ) => item !== rest.action diff --git a/packages/edit-navigation/src/store/test/reducer.js b/packages/edit-navigation/src/store/test/reducer.js index 4d7700b96ca0a6..7eac08dae705c8 100644 --- a/packages/edit-navigation/src/store/test/reducer.js +++ b/packages/edit-navigation/src/store/test/reducer.js @@ -168,6 +168,7 @@ describe( 'processingQueue', () => { postId: 1, action: 'some action', } ); + expect( newState ).not.toBe( originalState ); expect( newState ).toEqual( { 1: { pendingActions: [ 'first action', 'another action' ], From bfb5e03208711149f30b6dab673ef95ad936fc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:29:56 +0200 Subject: [PATCH 22/30] Rename getNavigationPost to getNavigationPostForMenu --- .../edit-navigation/src/components/navigation-editor/index.js | 2 +- packages/edit-navigation/src/store/actions.js | 4 ++-- packages/edit-navigation/src/store/controls.js | 4 ++-- packages/edit-navigation/src/store/resolvers.js | 2 +- packages/edit-navigation/src/store/selectors.js | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/edit-navigation/src/components/navigation-editor/index.js b/packages/edit-navigation/src/components/navigation-editor/index.js index 183f60c72108bb..28ae6cc324b160 100644 --- a/packages/edit-navigation/src/components/navigation-editor/index.js +++ b/packages/edit-navigation/src/components/navigation-editor/index.js @@ -23,7 +23,7 @@ export default function NavigationEditorWrapper( { onDeleteMenu, } ) { const { post, hasResolved } = useSelect( ( select ) => ( { - post: select( 'core/edit-navigation' ).getNavigationPost( menuId + '' ), + post: select( 'core/edit-navigation' ).getNavigationPostForMenu( menuId + '' ), hasResolved: select( 'core/edit-navigation' ).hasResolvedNavigationPost( menuId ), diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 743e656e32195c..0a9230c3a8ba33 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -13,7 +13,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { - getNavigationPost, + getNavigationPostForMenu, getPendingActions, isProcessingPost, getMenuItemToClientIdMapping, @@ -265,7 +265,7 @@ function serializeProcessing( callback ) { // re-fetch the post as running the callback() likely updated it yield* serializedCallback( - yield getNavigationPost( post.meta.menuId ) + yield getNavigationPostForMenu( post.meta.menuId ) ); } } diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index 57f2eb3a0ec8c3..b59d46016e6fa1 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -43,11 +43,11 @@ export function getMenuItemToClientIdMapping( postId ) { }; } -export function getNavigationPost( menuId ) { +export function getNavigationPostForMenu( menuId ) { return { type: 'SELECT', registryName: 'core/edit-navigation', - selectorName: 'getNavigationPost', + selectorName: 'getNavigationPostForMenu', args: [ menuId ], }; } diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index c595b8b4656cbf..605f87b79935ac 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -24,7 +24,7 @@ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; * @param {string} menuId The id of menu to create a post from * @return {void} */ -export function* getNavigationPost( menuId ) { +export function* getNavigationPostForMenu( menuId ) { const stubPost = createStubPost( menuId ); // Persist an empty post to warm up the state yield persistPost( stubPost ); diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 295f7331a61b6f..b29aefb2eefa84 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -23,10 +23,10 @@ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; * @param {string} menuId The id of menu to create a post from. * @return {null|Object} Post once the resolver fetches it, otherwise null */ -export const getNavigationPost = createRegistrySelector( +export const getNavigationPostForMenu = createRegistrySelector( ( select ) => ( state, menuId ) => { // When the record is unavailable, calling getEditedEntityRecord triggers a http - // request via it's related resolver. Let's return nothing until getNavigationPost + // request via it's related resolver. Let's return nothing until getNavigationPostForMenu // resolver marks the record as resolved. if ( ! hasResolvedNavigationPost( state, menuId ) ) { return null; From b99b8aa8acc33b322c40e0ec838930751757464d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:42:07 +0200 Subject: [PATCH 23/30] Handle empty nonce in batchSave --- packages/edit-navigation/src/store/actions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 0a9230c3a8ba33..d17f03845b558f 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -135,6 +135,9 @@ function* batchSave( menuId, menuItemsByClientId, navigationBlock ) { const { nonce, stylesheet } = yield apiFetch( { path: '/__experimental/customizer-nonces/get-save-nonce', } ); + if ( ! nonce ) { + throw new Error(); + } // eslint-disable-next-line no-undef const body = new FormData(); From 7cdc116ced4da0a4442fe0a98bf7236336708779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:52:21 +0200 Subject: [PATCH 24/30] Don't export storeConfig --- packages/edit-navigation/src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/edit-navigation/src/index.js b/packages/edit-navigation/src/index.js index 7d29ac35e8c1fa..b3a9d6a9512421 100644 --- a/packages/edit-navigation/src/index.js +++ b/packages/edit-navigation/src/index.js @@ -21,6 +21,7 @@ import { decodeEntities } from '@wordpress/html-entities'; * Internal dependencies */ import Layout from './components/layout'; +import './store'; /** * Fetches link suggestions from the API. This function is an exact copy of a function found at: @@ -63,5 +64,3 @@ export function initialize( id, settings ) { document.getElementById( id ) ); } - -export { storeConfig } from './store'; From 6b49ee0e6835ef93f757f67b5eb1462388c14de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:53:22 +0200 Subject: [PATCH 25/30] Simplify useCallback --- .../navigation-editor/use-navigation-block-editor.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js b/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js index 4c8672171b870b..7c97d50b8381ff 100644 --- a/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js +++ b/packages/edit-navigation/src/components/navigation-editor/use-navigation-block-editor.js @@ -19,12 +19,9 @@ export default function useNavigationBlockEditor( post ) { { id: post.id } ); const onChange = useCallback( - ( updatedBlocks ) => { - async function handle() { - await _onChange( updatedBlocks ); - createMissingMenuItems( post ); - } - handle(); + async ( updatedBlocks ) => { + await _onChange( updatedBlocks ); + createMissingMenuItems( post ); }, [ blocks, onChange ] ); From 1021624e0126a5fc699df24b1181a3c6ceb2ae7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:54:29 +0200 Subject: [PATCH 26/30] Rename NavigationEditorWrapper to NavigationEditor --- .../src/components/navigation-editor/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/edit-navigation/src/components/navigation-editor/index.js b/packages/edit-navigation/src/components/navigation-editor/index.js index 28ae6cc324b160..15893c06b44539 100644 --- a/packages/edit-navigation/src/components/navigation-editor/index.js +++ b/packages/edit-navigation/src/components/navigation-editor/index.js @@ -17,13 +17,15 @@ import NavigationStructureArea from './navigation-structure-area'; import useNavigationBlockEditor from './use-navigation-block-editor'; import { useDispatch, useSelect } from '@wordpress/data'; -export default function NavigationEditorWrapper( { +export default function NavigationEditor( { menuId, blockEditorSettings, onDeleteMenu, } ) { const { post, hasResolved } = useSelect( ( select ) => ( { - post: select( 'core/edit-navigation' ).getNavigationPostForMenu( menuId + '' ), + post: select( 'core/edit-navigation' ).getNavigationPostForMenu( + menuId + '' + ), hasResolved: select( 'core/edit-navigation' ).hasResolvedNavigationPost( menuId ), From 170df1fbda10a6ffa07bf22552ec1088ce55a60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 10:56:49 +0200 Subject: [PATCH 27/30] Ensure menuId is a number --- packages/edit-navigation/src/components/menus-editor/index.js | 2 +- .../edit-navigation/src/components/navigation-editor/index.js | 2 +- packages/edit-navigation/src/store/resolvers.js | 2 +- packages/edit-navigation/src/store/selectors.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/edit-navigation/src/components/menus-editor/index.js b/packages/edit-navigation/src/components/menus-editor/index.js index a602ef60cb1957..2f2b5c2407c803 100644 --- a/packages/edit-navigation/src/components/menus-editor/index.js +++ b/packages/edit-navigation/src/components/menus-editor/index.js @@ -80,7 +80,7 @@ export default function MenusEditor( { blockEditorSettings } ) { label: menu.name, } ) ) } onChange={ ( selectedMenuId ) => - setMenuId( selectedMenuId ) + setMenuId( Number( selectedMenuId ) ) } value={ menuId } /> diff --git a/packages/edit-navigation/src/components/navigation-editor/index.js b/packages/edit-navigation/src/components/navigation-editor/index.js index 15893c06b44539..0996d28dbe5f7f 100644 --- a/packages/edit-navigation/src/components/navigation-editor/index.js +++ b/packages/edit-navigation/src/components/navigation-editor/index.js @@ -24,7 +24,7 @@ export default function NavigationEditor( { } ) { const { post, hasResolved } = useSelect( ( select ) => ( { post: select( 'core/edit-navigation' ).getNavigationPostForMenu( - menuId + '' + menuId ), hasResolved: select( 'core/edit-navigation' ).hasResolvedNavigationPost( menuId diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index 605f87b79935ac..bc828105a34d51 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -21,7 +21,7 @@ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; * * Fetches all menu items, converts them into blocks, and hydrates a new post with them. * - * @param {string} menuId The id of menu to create a post from + * @param {number} menuId The id of menu to create a post from * @return {void} */ export function* getNavigationPostForMenu( menuId ) { diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index b29aefb2eefa84..37d7892c5de13f 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -20,7 +20,7 @@ import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; * * Related resolver fetches all menu items, converts them into blocks, and hydrates a new post with them. * - * @param {string} menuId The id of menu to create a post from. + * @param {number} menuId The id of menu to create a post from. * @return {null|Object} Post once the resolver fetches it, otherwise null */ export const getNavigationPostForMenu = createRegistrySelector( From c82b617331f71949f435b1a8764f0b1ac39e52ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 11:05:52 +0200 Subject: [PATCH 28/30] Add comments for all exported members of store/**/*.js --- .../edit-navigation/src/store/controls.js | 32 +++++++++++++++++++ packages/edit-navigation/src/store/reducer.js | 25 +++++++++++++-- .../edit-navigation/src/store/selectors.js | 6 ++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index b59d46016e6fa1..8a534c9e35b79e 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -22,6 +22,12 @@ export function apiFetch( request ) { }; } +/** + * Returns a list of pending actions for given post id. + * + * @param {number} postId Post ID. + * @return {Array} List of pending actions. + */ export function getPendingActions( postId ) { return { type: 'GET_PENDING_ACTIONS', @@ -29,6 +35,13 @@ export function getPendingActions( postId ) { }; } +/** + * Returns boolean indicating whether or not an action processing specified + * post is currently running. + * + * @param {number} postId Post ID. + * @return {Object} Action. + */ export function isProcessingPost( postId ) { return { type: 'IS_PROCESSING_POST', @@ -36,6 +49,12 @@ export function isProcessingPost( postId ) { }; } +/** + * Selects menuItemId -> clientId mapping (necessary for saving the navigation). + * + * @param {number} postId Navigation post ID. + * @return {Object} Action. + */ export function getMenuItemToClientIdMapping( postId ) { return { type: 'GET_MENU_ITEM_TO_CLIENT_ID_MAPPING', @@ -43,6 +62,13 @@ export function getMenuItemToClientIdMapping( postId ) { }; } +/** + * Resolves navigation post for given menuId. + * + * @see selectors.js + * @param {number} menuId Menu ID. + * @return {Object} Action. + */ export function getNavigationPostForMenu( menuId ) { return { type: 'SELECT', @@ -52,6 +78,12 @@ export function getNavigationPostForMenu( menuId ) { }; } +/** + * Resolves menu items for given menu id. + * + * @param {number} menuId Menu ID. + * @return {Object} Action. + */ export function resolveMenuItems( menuId ) { return { type: 'RESOLVE_MENU_ITEMS', diff --git a/packages/edit-navigation/src/store/reducer.js b/packages/edit-navigation/src/store/reducer.js index 83233c221bbd6d..544857b159af27 100644 --- a/packages/edit-navigation/src/store/reducer.js +++ b/packages/edit-navigation/src/store/reducer.js @@ -3,7 +3,17 @@ */ import { combineReducers } from '@wordpress/data'; -export function mapping( state, { type, postId, ...rest } ) { +/** + * Internal to edit-navigation package. + * + * Stores menuItemId -> clientId mapping which is necessary for saving the navigation. + * + * @param {Object} state Redux state + * @param {Object} action Redux action + * @return {Object} Updated state + */ +export function mapping( state, action ) { + const { type, postId, ...rest } = action; if ( type === 'SET_MENU_ITEM_TO_CLIENT_ID_MAPPING' ) { return { ...state, [ postId ]: rest.mapping }; } @@ -11,7 +21,18 @@ export function mapping( state, { type, postId, ...rest } ) { return state || {}; } -export function processingQueue( state, { type, postId, ...rest } ) { +/** + * Internal to edit-navigation package. + * + * Enables serializeProcessing action wrapper by storing the underlying execution + * state and any pending actions. + * + * @param {Object} state Redux state + * @param {Object} action Redux action + * @return {Object} Updated state + */ +export function processingQueue( state, action ) { + const { type, postId, ...rest } = action; switch ( type ) { case 'START_PROCESSING_POST': return { diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 37d7892c5de13f..2891e1ac3046dd 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -39,6 +39,12 @@ export const getNavigationPostForMenu = createRegistrySelector( } ); +/** + * Returns true if the navigation post related to menuId was already resolved. + * + * @param {number} menuId The id of menu. + * @return {boolean} True if the navigation post related to menuId was already resolved, false otherwise. + */ export const hasResolvedNavigationPost = createRegistrySelector( ( select ) => ( state, menuId ) => { return select( 'core' ).hasFinishedResolution( 'getEntityRecord', [ From 27eac24e4e6796d0668ae38156ebe8d1fd51273e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 19 Jun 2020 11:07:51 +0200 Subject: [PATCH 29/30] Add comments to exported members of utils.js --- packages/edit-navigation/src/store/utils.js | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/edit-navigation/src/store/utils.js b/packages/edit-navigation/src/store/utils.js index e61e87e851ddf2..26fb6974903c40 100644 --- a/packages/edit-navigation/src/store/utils.js +++ b/packages/edit-navigation/src/store/utils.js @@ -1,8 +1,32 @@ +/** + * "Kind" of the navigation post. + * + * @type {string} + */ export const KIND = 'root'; + +/** + * "post type" of the navigation post. + * + * @type {string} + */ export const POST_TYPE = 'postType'; + +/** + * Builds an ID for a new navigation post. + * + * @param {number} menuId Menu id. + * @return {string} An ID. + */ export const buildNavigationPostId = ( menuId ) => `navigation-post-${ menuId }`; +/** + * Builds a query to resolve menu items. + * + * @param {number} menuId Menu id. + * @return {Object} Query. + */ export function menuItemsQuery( menuId ) { return { menus: menuId, per_page: -1 }; } From 7bf7e7c88f21e177099cec707df27bbdf99aae37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 22 Jun 2020 09:33:25 +0200 Subject: [PATCH 30/30] Update package-lock.json --- package-lock.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 04aab70e819de9..b29d5fd65d69f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10788,6 +10788,7 @@ "@wordpress/blocks": "file:packages/blocks", "@wordpress/components": "file:packages/components", "@wordpress/compose": "file:packages/compose", + "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", "@wordpress/data-controls": "file:packages/data-controls", "@wordpress/dom-ready": "file:packages/dom-ready", @@ -10801,7 +10802,8 @@ "@wordpress/url": "file:packages/url", "classnames": "^2.2.5", "lodash": "^4.17.15", - "rememo": "^3.0.0" + "rememo": "^3.0.0", + "uuid": "^7.0.2" } }, "@wordpress/edit-post": {