From e8c6106726eda2680a032ecbea27409ab285c5df Mon Sep 17 00:00:00 2001 From: Grzegorz Date: Mon, 8 Mar 2021 13:25:06 +0100 Subject: [PATCH] Navigation Editor: Allow menu renaming (#29012) * feature to change navigation menu name * name editor component added * add missing styles| * remove doubled hooks * remove ; * navigation post variable updated * use-menu-locations hook moved to hooks directory * console error fixed * name edition moved to the sidebar. In toolbar manu name changed to button, when clicked name editor is focused * move NameEditor to InspectorControls in InspectorAdditions in order to preserve the same order of items in navbar * use TextControl component in name-editor * changes according to CR * add missing file * changes according to CR: removed aria-label from name-display/index.js and added aria-label in /name-editor/index.js * Dispatch save requests sequentially instead of in parallel * explicitly set () => setIsMenuNameEditFocused( true ) * add translation to the file * resolved conflicts with trunk * resolved conflicts with trunk 2 * sprintf added * Update unit test to take into account the call to saveEditedEntityRecord Co-authored-by: grzegorz_marzencki Co-authored-by: Daniel Richards --- .../src/components/header/manage-locations.js | 2 +- .../components/inspector-additions/index.js | 10 +- .../src/components/layout/index.js | 113 ++++++++----- .../src/components/name-display/index.js | 37 ++++ .../src/components/name-editor/index.js | 44 +++++ .../src/components/name-editor/style.scss | 5 + .../src/filters/add-menu-name-editor.js | 31 ++++ ...disable-inserting-non-navigation-blocks.js | 22 +++ packages/edit-navigation/src/filters/index.js | 18 ++ .../remove-edit-unsupported-features.js | 30 ++++ .../remove-settings-unsupported-features.js | 28 +++ packages/edit-navigation/src/hooks/index.js | 16 ++ .../src/hooks/use-menu-entity.js | 32 ++++ .../header => hooks}/use-menu-locations.js | 0 .../use-menu-notifications.js | 12 +- .../use-navigation-block-editor.js | 21 ++- .../layout => hooks}/use-navigation-editor.js | 40 ++--- .../src/hooks/use-selected-menu-data.js | 22 +++ packages/edit-navigation/src/index.js | 160 +----------------- packages/edit-navigation/src/store/actions.js | 16 +- .../edit-navigation/src/store/resolvers.js | 16 +- .../edit-navigation/src/store/selectors.js | 15 +- .../edit-navigation/src/store/test/actions.js | 8 + .../src/store/test/resolvers.js | 23 ++- .../src/store/test/selectors.js | 17 +- packages/edit-navigation/src/store/utils.js | 14 -- .../edit-navigation/src/utils/constants.js | 27 +++ .../src/utils/fetch-link-suggestions.js | 101 +++++++++++ 28 files changed, 606 insertions(+), 274 deletions(-) create mode 100644 packages/edit-navigation/src/components/name-display/index.js create mode 100644 packages/edit-navigation/src/components/name-editor/index.js create mode 100644 packages/edit-navigation/src/components/name-editor/style.scss create mode 100644 packages/edit-navigation/src/filters/add-menu-name-editor.js create mode 100644 packages/edit-navigation/src/filters/disable-inserting-non-navigation-blocks.js create mode 100644 packages/edit-navigation/src/filters/index.js create mode 100644 packages/edit-navigation/src/filters/remove-edit-unsupported-features.js create mode 100644 packages/edit-navigation/src/filters/remove-settings-unsupported-features.js create mode 100644 packages/edit-navigation/src/hooks/index.js create mode 100644 packages/edit-navigation/src/hooks/use-menu-entity.js rename packages/edit-navigation/src/{components/header => hooks}/use-menu-locations.js (100%) rename packages/edit-navigation/src/{components/layout => hooks}/use-menu-notifications.js (86%) rename packages/edit-navigation/src/{components/layout => hooks}/use-navigation-block-editor.js (58%) rename packages/edit-navigation/src/{components/layout => hooks}/use-navigation-editor.js (74%) create mode 100644 packages/edit-navigation/src/hooks/use-selected-menu-data.js create mode 100644 packages/edit-navigation/src/utils/constants.js create mode 100644 packages/edit-navigation/src/utils/fetch-link-suggestions.js diff --git a/packages/edit-navigation/src/components/header/manage-locations.js b/packages/edit-navigation/src/components/header/manage-locations.js index 480917f901924..f9d2e31f5b080 100644 --- a/packages/edit-navigation/src/components/header/manage-locations.js +++ b/packages/edit-navigation/src/components/header/manage-locations.js @@ -8,7 +8,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import useMenuLocations from './use-menu-locations'; +import useMenuLocations from '../../hooks/use-menu-locations'; export default function ManageLocations() { const menus = useSelect( ( select ) => select( 'core' ).getMenus(), [] ); diff --git a/packages/edit-navigation/src/components/inspector-additions/index.js b/packages/edit-navigation/src/components/inspector-additions/index.js index e9cc696bd7c1f..50fddb1987785 100644 --- a/packages/edit-navigation/src/components/inspector-additions/index.js +++ b/packages/edit-navigation/src/components/inspector-additions/index.js @@ -9,6 +9,9 @@ import { InspectorControls } from '@wordpress/block-editor'; */ import AutoAddPagesPanel from './auto-add-pages-panel'; import DeleteMenuPanel from './delete-menu-panel'; +import { NameEditor } from '../name-editor'; +import { PanelBody } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; export default function InspectorAdditions( { menuId, onDeleteMenu } ) { const selectedBlock = useSelect( @@ -22,8 +25,11 @@ export default function InspectorAdditions( { menuId, onDeleteMenu } ) { return ( - - + + + + + ); } diff --git a/packages/edit-navigation/src/components/layout/index.js b/packages/edit-navigation/src/components/layout/index.js index d531ed458656e..be6c0cd4f1a1c 100644 --- a/packages/edit-navigation/src/components/layout/index.js +++ b/packages/edit-navigation/src/components/layout/index.js @@ -19,14 +19,19 @@ import { BlockInspector, __unstableUseBlockSelectionClearer as useBlockSelectionClearer, } from '@wordpress/block-editor'; +import { useMemo, useState } from '@wordpress/element'; /** * Internal dependencies */ import EmptyState from './empty-state'; -import useNavigationEditor from './use-navigation-editor'; -import useNavigationBlockEditor from './use-navigation-block-editor'; -import useMenuNotifications from './use-menu-notifications'; +import { + IsMenuNameControlFocusedContext, + MenuIdContext, + useNavigationEditor, + useNavigationBlockEditor, + useMenuNotifications, +} from '../../hooks'; import ErrorBoundary from '../error-boundary'; import NavigationEditorShortcuts from './shortcuts'; import Header from '../header'; @@ -37,7 +42,9 @@ import { store as editNavigationStore } from '../../store'; export default function Layout( { blockEditorSettings } ) { const canvasRef = useBlockSelectionClearer(); - + const [ isMenuNameControlFocused, setIsMenuNameControlFocused ] = useState( + false + ); const { saveNavigationPost } = useDispatch( editNavigationStore ); const savePost = () => saveNavigationPost( navigationPost ); @@ -74,51 +81,65 @@ export default function Layout( { blockEditorSettings } ) { 'has-block-inspector': isBlockEditorReady, } ) } > -
+ + [ + isMenuNameControlFocused, + setIsMenuNameControlFocused, + ], + [ isMenuNameControlFocused ] + ) } + > +
- { ! hasFinishedInitialLoad && } + { ! hasFinishedInitialLoad && } - { hasFinishedInitialLoad && ! hasMenus && ( - - ) } + { hasFinishedInitialLoad && ! hasMenus && ( + + ) } - { isBlockEditorReady && ( - - - -
- -
- - -
- ) } + { isBlockEditorReady && ( + + + +
+ +
+ + +
+ ) } + + diff --git a/packages/edit-navigation/src/components/name-display/index.js b/packages/edit-navigation/src/components/name-display/index.js new file mode 100644 index 0000000000000..1778f21651e86 --- /dev/null +++ b/packages/edit-navigation/src/components/name-display/index.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; +import { BlockControls } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import { + useSelectedMenuData, + IsMenuNameControlFocusedContext, +} from '../../hooks'; +import { useContext } from '@wordpress/element'; + +import { sprintf, __ } from '@wordpress/i18n'; +export default function NameDisplay() { + const { menuName } = useSelectedMenuData(); + const [ , setIsMenuNameEditFocused ] = useContext( + IsMenuNameControlFocusedContext + ); + return ( + + + setIsMenuNameEditFocused( true ) } + > + { menuName } + + + + ); +} diff --git a/packages/edit-navigation/src/components/name-editor/index.js b/packages/edit-navigation/src/components/name-editor/index.js new file mode 100644 index 0000000000000..b948ee893d23a --- /dev/null +++ b/packages/edit-navigation/src/components/name-editor/index.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useEffect, useRef, useContext } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { TextControl } from '@wordpress/components'; +import { + IsMenuNameControlFocusedContext, + useMenuEntity, + useSelectedMenuData, +} from '../../hooks'; + +export function NameEditor() { + const [ isMenuNameEditFocused, setIsMenuNameEditFocused ] = useContext( + IsMenuNameControlFocusedContext + ); + + const { menuId } = useSelectedMenuData(); + const { editMenuName, editedMenuName } = useMenuEntity( menuId ); + const inputRef = useRef(); + useEffect( () => { + if ( isMenuNameEditFocused ) inputRef.current.focus(); + }, [ isMenuNameEditFocused ] ); + return ( + <> + setIsMenuNameEditFocused( false ) } + className="edit-navigation-name-editor__text-control" + value={ editedMenuName } + onChange={ ( value ) => { + editMenuName( value ); + } } + /> + + ); +} diff --git a/packages/edit-navigation/src/components/name-editor/style.scss b/packages/edit-navigation/src/components/name-editor/style.scss new file mode 100644 index 0000000000000..206c60f92fb8b --- /dev/null +++ b/packages/edit-navigation/src/components/name-editor/style.scss @@ -0,0 +1,5 @@ +.edit-navigation-name-editor__text-control { + .components-base-control { + border: none; + } +} diff --git a/packages/edit-navigation/src/filters/add-menu-name-editor.js b/packages/edit-navigation/src/filters/add-menu-name-editor.js new file mode 100644 index 0000000000000..e7c0e4210aeb6 --- /dev/null +++ b/packages/edit-navigation/src/filters/add-menu-name-editor.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +/** + * Internal dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import NameDisplay from '../components/name-display'; + +const addMenuNameEditor = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + if ( props.name !== 'core/navigation' ) { + return ; + } + return ( + <> + + + + ); + }, + 'withMenuName' +); + +export default () => + addFilter( + 'editor.BlockEdit', + 'core/edit-navigation/with-menu-name', + addMenuNameEditor + ); diff --git a/packages/edit-navigation/src/filters/disable-inserting-non-navigation-blocks.js b/packages/edit-navigation/src/filters/disable-inserting-non-navigation-blocks.js new file mode 100644 index 0000000000000..c9a34fe168b0f --- /dev/null +++ b/packages/edit-navigation/src/filters/disable-inserting-non-navigation-blocks.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +/** + * External dependencies + */ +import { set } from 'lodash'; + +function disableInsertingNonNavigationBlocks( settings, name ) { + if ( ! [ 'core/navigation', 'core/navigation-link' ].includes( name ) ) { + set( settings, [ 'supports', 'inserter' ], false ); + } + return settings; +} + +export default () => + addFilter( + 'blocks.registerBlockType', + 'core/edit-navigation/disable-inserting-non-navigation-blocks', + disableInsertingNonNavigationBlocks + ); diff --git a/packages/edit-navigation/src/filters/index.js b/packages/edit-navigation/src/filters/index.js new file mode 100644 index 0000000000000..96315b46b7eb8 --- /dev/null +++ b/packages/edit-navigation/src/filters/index.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import addMenuNameEditor from './add-menu-name-editor'; +import disableInsertingNonNavigationBlocks from './disable-inserting-non-navigation-blocks'; +import removeEditUnsupportedFeatures from './remove-edit-unsupported-features'; +import removeSettingsUnsupportedFeatures from './remove-settings-unsupported-features'; + +export const addFilters = ( + shouldAddDisableInsertingNonNavigationBlocksFilter +) => { + addMenuNameEditor(); + if ( shouldAddDisableInsertingNonNavigationBlocksFilter ) { + disableInsertingNonNavigationBlocks(); + } + removeEditUnsupportedFeatures(); + removeSettingsUnsupportedFeatures(); +}; diff --git a/packages/edit-navigation/src/filters/remove-edit-unsupported-features.js b/packages/edit-navigation/src/filters/remove-edit-unsupported-features.js new file mode 100644 index 0000000000000..a2074e2216501 --- /dev/null +++ b/packages/edit-navigation/src/filters/remove-edit-unsupported-features.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { createHigherOrderComponent } from '@wordpress/compose'; + +const removeNavigationBlockEditUnsupportedFeatures = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + if ( props.name !== 'core/navigation' ) { + return ; + } + + return ( + + ); + }, + 'removeNavigationBlockEditUnsupportedFeatures' +); + +export default () => + addFilter( + 'editor.BlockEdit', + 'core/edit-navigation/remove-navigation-block-edit-unsupported-features', + removeNavigationBlockEditUnsupportedFeatures + ); diff --git a/packages/edit-navigation/src/filters/remove-settings-unsupported-features.js b/packages/edit-navigation/src/filters/remove-settings-unsupported-features.js new file mode 100644 index 0000000000000..26d62f9ec6448 --- /dev/null +++ b/packages/edit-navigation/src/filters/remove-settings-unsupported-features.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; + +function removeNavigationBlockSettingsUnsupportedFeatures( settings, name ) { + if ( name !== 'core/navigation' ) { + return settings; + } + + return { + ...settings, + supports: { + customClassName: false, + html: false, + inserter: true, + }, + // Remove any block variations. + variations: undefined, + }; +} + +export default () => + addFilter( + 'blocks.registerBlockType', + 'core/edit-navigation/remove-navigation-block-settings-unsupported-features', + removeNavigationBlockSettingsUnsupportedFeatures + ); diff --git a/packages/edit-navigation/src/hooks/index.js b/packages/edit-navigation/src/hooks/index.js new file mode 100644 index 0000000000000..5d2c4f9474565 --- /dev/null +++ b/packages/edit-navigation/src/hooks/index.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { createContext } from '@wordpress/element'; + +export const untitledMenu = __( '(untitled menu)' ); +export const MenuIdContext = createContext(); +export const IsMenuNameControlFocusedContext = createContext(); + +export { default as useMenuEntity } from './use-menu-entity'; +export { default as useNavigationEditor } from './use-navigation-editor'; +export { default as useNavigationBlockEditor } from './use-navigation-block-editor'; +export { default as useMenuNotifications } from './use-menu-notifications'; +export { default as useSelectedMenuData } from './use-selected-menu-data'; +export { default as useMenuLocations } from './use-menu-locations'; diff --git a/packages/edit-navigation/src/hooks/use-menu-entity.js b/packages/edit-navigation/src/hooks/use-menu-entity.js new file mode 100644 index 0000000000000..0b092f62a5d83 --- /dev/null +++ b/packages/edit-navigation/src/hooks/use-menu-entity.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { MENU_KIND, MENU_POST_TYPE } from '../utils/constants'; + +import { untitledMenu } from './index'; + +export default function useMenuEntity( menuId ) { + const { editEntityRecord } = useDispatch( 'core' ); + + const menuEntityData = [ MENU_KIND, MENU_POST_TYPE, menuId ]; + const editedMenu = useSelect( + ( select ) => + menuId && + select( 'core' ).getEditedEntityRecord( ...menuEntityData ), + [ menuId ] + ); + + const editedMenuName = menuId && editedMenu.name; + + const editMenuName = ( name = untitledMenu ) => + editEntityRecord( ...menuEntityData, { name } ); + + return { + editedMenuName, + editMenuName, + }; +} diff --git a/packages/edit-navigation/src/components/header/use-menu-locations.js b/packages/edit-navigation/src/hooks/use-menu-locations.js similarity index 100% rename from packages/edit-navigation/src/components/header/use-menu-locations.js rename to packages/edit-navigation/src/hooks/use-menu-locations.js diff --git a/packages/edit-navigation/src/components/layout/use-menu-notifications.js b/packages/edit-navigation/src/hooks/use-menu-notifications.js similarity index 86% rename from packages/edit-navigation/src/components/layout/use-menu-notifications.js rename to packages/edit-navigation/src/hooks/use-menu-notifications.js index 16e7076c0011d..e85f33f164fdd 100644 --- a/packages/edit-navigation/src/components/layout/use-menu-notifications.js +++ b/packages/edit-navigation/src/hooks/use-menu-notifications.js @@ -4,17 +4,21 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; +/** + * Internal dependencies + */ +import { MENU_POST_TYPE, MENU_KIND } from '../utils/constants'; export default function useMenuNotifications( menuId ) { const { lastSaveError, lastDeleteError } = useSelect( ( select ) => ( { lastSaveError: select( 'core' ).getLastEntitySaveError( - 'root', - 'menu' + MENU_KIND, + MENU_POST_TYPE ), lastDeleteError: select( 'core' ).getLastEntityDeleteError( - 'root', - 'menu', + MENU_KIND, + MENU_POST_TYPE, menuId ), } ), diff --git a/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js b/packages/edit-navigation/src/hooks/use-navigation-block-editor.js similarity index 58% rename from packages/edit-navigation/src/components/layout/use-navigation-block-editor.js rename to packages/edit-navigation/src/hooks/use-navigation-block-editor.js index 902e77ac22bbd..855bd92b75870 100644 --- a/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js +++ b/packages/edit-navigation/src/hooks/use-navigation-block-editor.js @@ -8,24 +8,29 @@ import { useEntityBlockEditor } from '@wordpress/core-data'; /** * Internal dependencies */ -import { KIND, POST_TYPE } from '../../store/utils'; -import { store as editNavigationStore } from '../../store'; +import { + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, +} from '../utils/constants'; +import { store as editNavigationStore } from '../store'; export default function useNavigationBlockEditor( post ) { const { createMissingMenuItems } = useDispatch( editNavigationStore ); - const [ blocks, onInput, _onChange ] = useEntityBlockEditor( - KIND, - POST_TYPE, - { id: post?.id } + const [ blocks, onInput, onEntityChange ] = useEntityBlockEditor( + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, + { + id: post?.id, + } ); const onChange = useCallback( async ( ...args ) => { - await _onChange( ...args ); + await onEntityChange( ...args ); createMissingMenuItems( post ); }, - [ _onChange, post ] + [ onEntityChange, post ] ); return [ blocks, onInput, onChange ]; diff --git a/packages/edit-navigation/src/components/layout/use-navigation-editor.js b/packages/edit-navigation/src/hooks/use-navigation-editor.js similarity index 74% rename from packages/edit-navigation/src/components/layout/use-navigation-editor.js rename to packages/edit-navigation/src/hooks/use-navigation-editor.js index e6439e804062b..3af9a2b100386 100644 --- a/packages/edit-navigation/src/components/layout/use-navigation-editor.js +++ b/packages/edit-navigation/src/hooks/use-navigation-editor.js @@ -3,35 +3,35 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; - /** * Internal dependencies */ -import { store as editNavigationStore } from '../../store'; +import { store as editNavigationStore } from '../store'; +const getMenusData = ( select ) => { + const selectors = select( 'core' ); + const params = { per_page: -1 }; + return { + menus: selectors.getMenus( params ), + hasLoadedMenus: selectors.hasFinishedResolution( 'getMenus', [ + params, + ] ), + }; +}; export default function useNavigationEditor() { + const { deleteMenu: _deleteMenu } = useDispatch( 'core' ); + const [ selectedMenuId, setSelectedMenuId ] = useState( null ); const [ hasFinishedInitialLoad, setHasFinishedInitialLoad ] = useState( false ); - const { menus, hasLoadedMenus } = useSelect( ( select ) => { - const selectors = select( 'core' ); - const params = { per_page: -1 }; - return { - menus: selectors.getMenus( params ), - hasLoadedMenus: selectors.hasFinishedResolution( 'getMenus', [ - params, - ] ), - }; - }, [] ); + const { menus, hasLoadedMenus } = useSelect( getMenusData, [] ); useEffect( () => { if ( hasLoadedMenus ) { setHasFinishedInitialLoad( true ); } }, [ hasLoadedMenus ] ); - const [ selectedMenuId, setSelectedMenuId ] = useState( null ); - useEffect( () => { if ( ! selectedMenuId && menus?.length ) { setSelectedMenuId( menus[ 0 ].id ); @@ -50,12 +50,6 @@ export default function useNavigationEditor() { [ selectedMenuId ] ); - const selectMenu = ( menuId ) => { - setSelectedMenuId( menuId ); - }; - - const { deleteMenu: _deleteMenu } = useDispatch( 'core' ); - const deleteMenu = async () => { const didDeleteMenu = await _deleteMenu( selectedMenuId, { force: true, @@ -67,11 +61,11 @@ export default function useNavigationEditor() { return { menus, - hasLoadedMenus, - hasFinishedInitialLoad, selectedMenuId, navigationPost, - selectMenu, + selectMenu: setSelectedMenuId, deleteMenu, + hasFinishedInitialLoad, + hasLoadedMenus, }; } diff --git a/packages/edit-navigation/src/hooks/use-selected-menu-data.js b/packages/edit-navigation/src/hooks/use-selected-menu-data.js new file mode 100644 index 0000000000000..7947bd4969965 --- /dev/null +++ b/packages/edit-navigation/src/hooks/use-selected-menu-data.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useContext } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { MenuIdContext, untitledMenu } from './index'; + +export default function useSelectedMenuData() { + const menuId = useContext( MenuIdContext ); + const menu = useSelect( ( select ) => select( 'core' ).getMenu( menuId ), [ + menuId, + ] ); + const menuName = menu?.name ?? untitledMenu; + return { + menuId, + menu, + menuName, + }; +} diff --git a/packages/edit-navigation/src/index.js b/packages/edit-navigation/src/index.js index ef67e940a6759..4744f8b29d6f5 100644 --- a/packages/edit-navigation/src/index.js +++ b/packages/edit-navigation/src/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { map, set, flatten, partialRight } from 'lodash'; - /** * WordPress dependencies */ @@ -11,167 +6,28 @@ import { __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; import { render } from '@wordpress/element'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; -import { decodeEntities } from '@wordpress/html-entities'; -import { addFilter } from '@wordpress/hooks'; - /** * Internal dependencies */ -import Layout from './components/layout'; -import './store'; - -function disableInsertingNonNavigationBlocks( settings, name ) { - if ( ! [ 'core/navigation', 'core/navigation-link' ].includes( name ) ) { - set( settings, [ 'supports', 'inserter' ], false ); - } - return settings; -} - -function removeNavigationBlockSettingsUnsupportedFeatures( settings, name ) { - if ( name !== 'core/navigation' ) { - return settings; - } - - return { - ...settings, - supports: { - customClassName: false, - html: false, - inserter: true, - }, - // Remove any block variations. - variations: undefined, - }; -} - -const removeNavigationBlockEditUnsupportedFeatures = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - if ( props.name !== 'core/navigation' ) { - return ; - } - - return ( - - ); - }, - 'removeNavigationBlockEditUnsupportedFeatures' -); +import { addFilters } from './filters'; +import fetchLinkSuggestions from './utils/fetch-link-suggestions'; /** - * Fetches link suggestions from the API. This function is an exact copy of a function found at: - * - * packages/editor/src/components/provider/index.js - * - * It seems like there is no suitable package to import this from. Ideally it would be either part of core-data. - * Until we refactor it, just copying the code is the simplest solution. - * - * @param {string} search - * @param {Object} [searchArguments] - * @param {number} [searchArguments.isInitialSuggestions] - * @param {number} [searchArguments.type] - * @param {number} [searchArguments.subtype] - * @param {Object} [editorSettings] - * @param {boolean} [editorSettings.disablePostFormats=false] - * @return {Promise} List of suggestions + * Internal dependencies */ -const fetchLinkSuggestions = ( - search, - { isInitialSuggestions, type, subtype } = {}, - { disablePostFormats = false } = {} -) => { - const perPage = isInitialSuggestions ? 3 : 20; - - const queries = []; - - if ( ! type || type === 'post' ) { - queries.push( - apiFetch( { - path: addQueryArgs( '/wp/v2/search', { - search, - per_page: perPage, - type: 'post', - subtype, - } ), - } ).catch( () => [] ) // fail by returning no results - ); - } - - if ( ! type || type === 'term' ) { - queries.push( - apiFetch( { - path: addQueryArgs( '/wp/v2/search', { - search, - per_page: perPage, - type: 'term', - subtype, - } ), - } ).catch( () => [] ) - ); - } - - if ( ! disablePostFormats && ( ! type || type === 'post-format' ) ) { - queries.push( - apiFetch( { - path: addQueryArgs( '/wp/v2/search', { - search, - per_page: perPage, - type: 'post-format', - subtype, - } ), - } ).catch( () => [] ) - ); - } - - return Promise.all( queries ).then( ( results ) => { - return map( flatten( results ).slice( 0, perPage ), ( result ) => ( { - id: result.id, - url: result.url, - title: decodeEntities( result.title ) || __( '(no title)' ), - type: result.subtype || result.type, - } ) ); - } ); -}; +import Layout from './components/layout'; +import './store'; export function initialize( id, settings ) { - if ( ! settings.blockNavMenus ) { - addFilter( - 'blocks.registerBlockType', - 'core/edit-navigation/disable-inserting-non-navigation-blocks', - disableInsertingNonNavigationBlocks - ); - } - - addFilter( - 'blocks.registerBlockType', - 'core/edit-navigation/remove-navigation-block-settings-unsupported-features', - removeNavigationBlockSettingsUnsupportedFeatures - ); - - addFilter( - 'editor.BlockEdit', - 'core/edit-navigation/remove-navigation-block-edit-unsupported-features', - removeNavigationBlockEditUnsupportedFeatures - ); - + addFilters( ! settings.blockNavMenus ); registerCoreBlocks(); if ( process.env.GUTENBERG_PHASE === 2 ) { __experimentalRegisterExperimentalCoreBlocks(); } - settings.__experimentalFetchLinkSuggestions = partialRight( - fetchLinkSuggestions, - settings - ); + settings.__experimentalFetchLinkSuggestions = () => + fetchLinkSuggestions( settings ); render( , diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 1b95b2950d37b..90dcf5aa19227 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -88,14 +88,26 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) { ); try { - const response = yield* batchSave( + // Save edits to the menu, like the menu name. + const menuResponse = yield dispatch( + 'core', + 'saveEditedEntityRecord', + 'root', + 'menu', + menuId + ); + + // Save blocks as menu items. + const batchSaveResponse = yield* batchSave( menuId, menuItemsByClientId, post.blocks[ 0 ] ); - if ( ! response.success ) { + + if ( ! batchSaveResponse.success || ! menuResponse ) { throw new Error(); } + yield dispatch( noticesStore, 'createSuccessNotice', diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index 5a5a06438b371..aaecaec940c1e 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -11,8 +11,12 @@ import { parse, createBlock } from '@wordpress/blocks'; /** * Internal dependencies */ +import { + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, +} from '../utils/constants'; import { resolveMenuItems, dispatch } from './controls'; -import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; +import { buildNavigationPostId } from './utils'; /** * Creates a "stub" navigation post reflecting the contents of menu with id=menuId. The @@ -35,7 +39,11 @@ export function* getNavigationPostForMenu( menuId ) { // 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 ]; + const args = [ + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, + stubPost.id, + ]; yield dispatch( 'core', 'startResolution', 'getEntityRecord', args ); // Now let's create a proper one hydrated using actual menu items @@ -73,8 +81,8 @@ const persistPost = ( post ) => dispatch( 'core', 'receiveEntityRecords', - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, post, { id: post.id }, false diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 2891e1ac3046d..4826183802816 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -11,7 +11,12 @@ import { createRegistrySelector } from '@wordpress/data'; /** * Internal dependencies */ -import { KIND, POST_TYPE, buildNavigationPostId } from './utils'; +import { + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, +} from '../utils/constants'; + +import { buildNavigationPostId } from './utils'; /** * Returns a "stub" navigation post reflecting the contents of menu with id=menuId. The @@ -32,8 +37,8 @@ export const getNavigationPostForMenu = createRegistrySelector( return null; } return select( 'core' ).getEditedEntityRecord( - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, buildNavigationPostId( menuId ) ); } @@ -48,8 +53,8 @@ export const getNavigationPostForMenu = createRegistrySelector( export const hasResolvedNavigationPost = createRegistrySelector( ( select ) => ( state, menuId ) => { return select( 'core' ).hasFinishedResolution( 'getEntityRecord', [ - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, buildNavigationPostId( menuId ), ] ); } diff --git a/packages/edit-navigation/src/store/test/actions.js b/packages/edit-navigation/src/store/test/actions.js index 0a2acb9560c15..9f24671057fea 100644 --- a/packages/edit-navigation/src/store/test/actions.js +++ b/packages/edit-navigation/src/store/test/actions.js @@ -324,6 +324,10 @@ describe( 'saveNavigationPost', () => { ); expect( action.next( mapping ).value ).toEqual( + dispatch( 'core', 'saveEditedEntityRecord', 'root', 'menu', 1 ) + ); + + expect( action.next( { id: 1 } ).value ).toEqual( apiFetch( { path: '/__experimental/customizer-nonces/get-save-nonce', } ) @@ -454,6 +458,10 @@ describe( 'saveNavigationPost', () => { ); expect( action.next( mapping ).value ).toEqual( + dispatch( 'core', 'saveEditedEntityRecord', 'root', 'menu', 1 ) + ); + + expect( action.next().value ).toEqual( apiFetch( { path: '/__experimental/customizer-nonces/get-save-nonce', } ) diff --git a/packages/edit-navigation/src/store/test/resolvers.js b/packages/edit-navigation/src/store/test/resolvers.js index da79100c4458c..af3e0ef2b6cd1 100644 --- a/packages/edit-navigation/src/store/test/resolvers.js +++ b/packages/edit-navigation/src/store/test/resolvers.js @@ -3,7 +3,12 @@ */ import { getNavigationPostForMenu } from '../resolvers'; import { resolveMenuItems, dispatch } from '../controls'; -import { KIND, POST_TYPE, buildNavigationPostId } from '../utils'; +import { + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, +} from '../../utils/constants'; + +import { buildNavigationPostId } from '../utils'; // Mock createBlock to avoid creating block in test environment jest.mock( '@wordpress/blocks', () => { @@ -52,8 +57,8 @@ describe( 'getNavigationPostForMenu', () => { dispatch( 'core', 'receiveEntityRecords', - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, stubPost, { id: stubPost.id }, false @@ -63,8 +68,8 @@ describe( 'getNavigationPostForMenu', () => { // Dispatch startResolution. expect( generator.next().value ).toEqual( dispatch( 'core', 'startResolution', 'getEntityRecord', [ - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, stubPost.id, ] ) ); @@ -164,8 +169,8 @@ describe( 'getNavigationPostForMenu', () => { dispatch( 'core', 'receiveEntityRecords', - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, navigationBlockStubPost, { id: navigationBlockStubPost.id }, false @@ -174,8 +179,8 @@ describe( 'getNavigationPostForMenu', () => { expect( generator.next().value ).toEqual( dispatch( 'core', 'finishResolution', 'getEntityRecord', [ - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, stubPost.id, ] ) ); diff --git a/packages/edit-navigation/src/store/test/selectors.js b/packages/edit-navigation/src/store/test/selectors.js index 450283f2661d2..67b7a25f932cb 100644 --- a/packages/edit-navigation/src/store/test/selectors.js +++ b/packages/edit-navigation/src/store/test/selectors.js @@ -6,7 +6,12 @@ import { hasResolvedNavigationPost, getMenuItemForClientId, } from '../selectors'; -import { KIND, POST_TYPE, buildNavigationPostId } from '../utils'; +import { + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, +} from '../../utils/constants'; + +import { buildNavigationPostId } from '../utils'; describe( 'getNavigationPostForMenu', () => { it( 'gets navigation post for menu', () => { @@ -29,8 +34,8 @@ describe( 'getNavigationPostForMenu', () => { expect( registry.select ).toHaveBeenCalledWith( 'core' ); expect( getEditedEntityRecord ).toHaveBeenCalledWith( - KIND, - POST_TYPE, + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, buildNavigationPostId( menuId ) ); @@ -83,7 +88,11 @@ describe( 'hasResolvedNavigationPost', () => { expect( registry.select ).toHaveBeenCalledWith( 'core' ); expect( hasFinishedResolution ).toHaveBeenCalledWith( 'getEntityRecord', - [ KIND, POST_TYPE, buildNavigationPostId( menuId ) ] + [ + NAVIGATION_POST_KIND, + NAVIGATION_POST_POST_TYPE, + buildNavigationPostId( menuId ), + ] ); hasResolvedNavigationPost.registry = defaultRegistry; diff --git a/packages/edit-navigation/src/store/utils.js b/packages/edit-navigation/src/store/utils.js index 64bbab6d69a18..fe28772fadeb4 100644 --- a/packages/edit-navigation/src/store/utils.js +++ b/packages/edit-navigation/src/store/utils.js @@ -17,20 +17,6 @@ import { isProcessingPost, } from './controls'; -/** - * "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. * diff --git a/packages/edit-navigation/src/utils/constants.js b/packages/edit-navigation/src/utils/constants.js new file mode 100644 index 0000000000000..9074d2f5e0a59 --- /dev/null +++ b/packages/edit-navigation/src/utils/constants.js @@ -0,0 +1,27 @@ +/** + * "Kind" of the menu post. + * + * @type {string} + */ +export const MENU_KIND = 'root'; + +/** + * "post type" of the menu post. + * + * @type {string} + */ +export const MENU_POST_TYPE = 'menu'; + +/** + * "Kind" of the navigation post. + * + * @type {string} + */ +export const NAVIGATION_POST_KIND = 'root'; + +/** + * "post type" of the navigation post. + * + * @type {string} + */ +export const NAVIGATION_POST_POST_TYPE = 'postType'; diff --git a/packages/edit-navigation/src/utils/fetch-link-suggestions.js b/packages/edit-navigation/src/utils/fetch-link-suggestions.js new file mode 100644 index 0000000000000..94a4e1587b15d --- /dev/null +++ b/packages/edit-navigation/src/utils/fetch-link-suggestions.js @@ -0,0 +1,101 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +/** + * External dependencies + */ +import { flatten, map } from 'lodash'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __ } from '@wordpress/i18n'; + +/** + * Fetches link suggestions from the API. This function is an exact copy of a function found at: + * + * packages/editor/src/components/provider/index.js + * + * It seems like there is no suitable package to import this from. Ideally it would be either part of core-data. + * Until we refactor it, just copying the code is the simplest solution. + * + * @param {string} search + * @param {Object} [searchArguments] + * @param {number} [searchArguments.isInitialSuggestions] + * @param {number} [searchArguments.type] + * @param {number} [searchArguments.subtype] + * @param {Object} [editorSettings] + * @param {boolean} [editorSettings.disablePostFormats=false] + * @return {Promise} List of suggestions + */ + +export default function fetchLinkSuggestions( + search, + { isInitialSuggestions, type, subtype } = {}, + { disablePostFormats = false } = {} +) { + const perPage = isInitialSuggestions ? 3 : 20; + + const linkTypes = [ 'post', 'term', 'post-format' ]; + + linkTypes.forEach( ( linkType ) => { + if ( ! type || type === linkType ) { + apiFetch( { + path: addQueryArgs( '/wp/v2/search', { + search, + per_page: perPage, + type: 'post', + subtype, + } ), + } ).catch( () => [] ); // fail by returning no results + } + } ); + const queries = []; + + if ( ! type || type === 'post' ) { + queries.push( + apiFetch( { + path: addQueryArgs( '/wp/v2/search', { + search, + per_page: perPage, + type: 'post', + subtype, + } ), + } ).catch( () => [] ) // fail by returning no results + ); + } + + if ( ! type || type === 'term' ) { + queries.push( + apiFetch( { + path: addQueryArgs( '/wp/v2/search', { + search, + per_page: perPage, + type: 'term', + subtype, + } ), + } ).catch( () => [] ) + ); + } + + if ( ! disablePostFormats && ( ! type || type === 'post-format' ) ) { + queries.push( + apiFetch( { + path: addQueryArgs( '/wp/v2/search', { + search, + per_page: perPage, + type: 'post-format', + subtype, + } ), + } ).catch( () => [] ) + ); + } + + return Promise.all( queries ).then( ( results ) => { + return map( flatten( results ).slice( 0, perPage ), ( result ) => ( { + id: result.id, + url: result.url, + title: decodeEntities( result.title ) || __( '(no title)' ), + type: result.subtype || result.type, + } ) ); + } ); +}