diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 25cc3f5e58ac9..7b15d775f281a 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -39,6 +39,7 @@ import { Spinner, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies @@ -55,6 +56,11 @@ import UnsavedInnerBlocks from './unsaved-inner-blocks'; import NavigationMenuDeleteControl from './navigation-menu-delete-control'; import useNavigationNotice from './use-navigation-notice'; import OverlayMenuIcon from './overlay-menu-icon'; +import useConvertClassicToBlockMenu, { + CLASSIC_MENU_CONVERSION_ERROR, + CLASSIC_MENU_CONVERSION_PENDING, + CLASSIC_MENU_CONVERSION_SUCCESS, +} from './use-convert-classic-menu-to-block-menu'; const EMPTY_ARRAY = []; @@ -231,23 +237,40 @@ function Navigation( { clientId ); + const { + convert, + status: classicMenuConversionStatus, + error: classicMenuConversionError, + value: classicMenuConversionResult, + } = useConvertClassicToBlockMenu( clientId ); + + const isConvertingClassicMenu = + classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING; + // The standard HTML5 tag for the block wrapper. const TagName = 'nav'; // "placeholder" shown if: // - we don't have a ref attribute pointing to a Navigation Post. + // - we are not running a menu conversion process. // - we don't have uncontrolled blocks. // - (legacy) we have a Navigation Area without a ref attribute pointing to a Navigation Post. const isPlaceholder = - ! ref && ( ! hasUncontrolledInnerBlocks || isWithinUnassignedArea ); + ! ref && + ! isConvertingClassicMenu && + ( ! hasUncontrolledInnerBlocks || isWithinUnassignedArea ); const isEntityAvailable = ! isNavigationMenuMissing && isNavigationMenuResolved; // "loading" state: + // - we are running the Classic Menu conversion process. + // OR // - there is a ref attribute pointing to a Navigation Post // - the Navigation Post isn't available (hasn't resolved) yet. - const isLoading = !! ( ref && ! isEntityAvailable ); + const isLoading = + isConvertingClassicMenu || + !! ( ref && ! isEntityAvailable && ! isConvertingClassicMenu ); const blockProps = useBlockProps( { ref: navRef, @@ -310,6 +333,42 @@ function Navigation( { ] = useState(); const [ detectedOverlayColor, setDetectedOverlayColor ] = useState(); + const [ + showClassicMenuConversionErrorNotice, + hideClassicMenuConversionErrorNotice, + ] = useNavigationNotice( { + name: 'block-library/core/navigation/classic-menu-conversion/error', + } ); + + function handleUpdateMenu( menuId ) { + setRef( menuId ); + selectBlock( clientId ); + } + + useEffect( () => { + if ( classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING ) { + speak( __( 'Classic menu importing.' ) ); + } + + if ( + classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_SUCCESS && + classicMenuConversionResult + ) { + handleUpdateMenu( classicMenuConversionResult?.id ); + hideClassicMenuConversionErrorNotice(); + speak( __( 'Classic menu imported successfully.' ) ); + } + + if ( classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_ERROR ) { + showClassicMenuConversionErrorNotice( classicMenuConversionError ); + speak( __( 'Classic menu import failed.' ) ); + } + }, [ + classicMenuConversionStatus, + classicMenuConversionResult, + classicMenuConversionError, + ] ); + // Spacer block needs orientation from context. This is a patch until // https://github.com/WordPress/gutenberg/issues/36197 is addressed. useEffect( () => { @@ -388,6 +447,25 @@ function Navigation( { ref, ] ); + const handleSelectNavigation = useCallback( + ( navPostOrClassicMenu ) => { + if ( ! navPostOrClassicMenu ) { + return; + } + + const isClassicMenu = navPostOrClassicMenu.hasOwnProperty( + 'auto_add' + ); + + if ( isClassicMenu ) { + convert( navPostOrClassicMenu.id, navPostOrClassicMenu.name ); + } else { + handleUpdateMenu( navPostOrClassicMenu.id ); + } + }, + [ convert, handleUpdateMenu ] + ); + const startWithEmptyMenu = useCallback( () => { registry.batch( () => { if ( navigationArea ) { @@ -490,12 +568,7 @@ function Navigation( { isResolvingCanUserCreateNavigationMenu={ isResolvingCanUserCreateNavigationMenu } - onFinish={ ( post ) => { - if ( post ) { - setRef( post.id ); - } - selectBlock( clientId ); - } } + onFinish={ handleSelectNavigation } /> ); @@ -510,9 +583,7 @@ function Navigation( { { - setRef( id ); - } } + onSelect={ handleSelectNavigation } onCreateNew={ startWithEmptyMenu } /* translators: %s: The name of a menu. */ actionLabel={ __( "Switch to '%s'" ) } diff --git a/packages/block-library/src/navigation/edit/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/navigation-menu-selector.js index 0993a8159b1f3..2a0f6fa60206d 100644 --- a/packages/block-library/src/navigation/edit/navigation-menu-selector.js +++ b/packages/block-library/src/navigation/edit/navigation-menu-selector.js @@ -17,12 +17,9 @@ import { useCallback, useMemo } from '@wordpress/element'; */ import useNavigationMenu from '../use-navigation-menu'; import useNavigationEntities from '../use-navigation-entities'; -import useConvertClassicMenu from '../use-convert-classic-menu'; -import useCreateNavigationMenu from './use-create-navigation-menu'; export default function NavigationMenuSelector( { currentMenuId, - clientId, onSelect, onCreateNew, showManageActions = false, @@ -43,27 +40,6 @@ export default function NavigationMenuSelector( { canSwitchNavigationMenu, } = useNavigationMenu(); - const createNavigationMenu = useCreateNavigationMenu( clientId ); - - const onFinishMenuCreation = async ( - blocks, - navigationMenuTitle = null - ) => { - if ( ! canUserCreateNavigationMenu ) { - return; - } - - const navigationMenu = await createNavigationMenu( - navigationMenuTitle, - blocks - ); - onSelect( navigationMenu ); - }; - - const convertClassicMenuToBlocks = useConvertClassicMenu( - onFinishMenuCreation - ); - const handleSelect = useCallback( ( _onClose ) => ( selectedId ) => { _onClose(); @@ -74,6 +50,14 @@ export default function NavigationMenuSelector( { [ navigationMenus ] ); + const handleSelectClassic = useCallback( + ( _onClose, menu ) => () => { + _onClose(); + onSelect( menu ); + }, + [] + ); + const menuChoices = useMemo( () => { return ( navigationMenus?.map( ( { id, title } ) => { @@ -130,13 +114,10 @@ export default function NavigationMenuSelector( { const label = decodeEntities( menu.name ); return ( { - onClose(); - convertClassicMenuToBlocks( - menu.id, - menu.name - ); - } } + onClick={ handleSelectClassic( + onClose, + menu + ) } key={ menu.id } aria-label={ sprintf( createActionLabel, diff --git a/packages/block-library/src/navigation/edit/use-convert-classic-menu-to-block-menu.js b/packages/block-library/src/navigation/edit/use-convert-classic-menu-to-block-menu.js new file mode 100644 index 0000000000000..475d232b4735f --- /dev/null +++ b/packages/block-library/src/navigation/edit/use-convert-classic-menu-to-block-menu.js @@ -0,0 +1,135 @@ +/** + * WordPress dependencies + */ +import { useRegistry } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useState, useCallback } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import useCreateNavigationMenu from './use-create-navigation-menu'; +import menuItemsToBlocks from '../menu-items-to-blocks'; + +export const CLASSIC_MENU_CONVERSION_SUCCESS = 'success'; +export const CLASSIC_MENU_CONVERSION_ERROR = 'error'; +export const CLASSIC_MENU_CONVERSION_PENDING = 'pending'; +export const CLASSIC_MENU_CONVERSION_IDLE = 'idle'; + +function useConvertClassicToBlockMenu( clientId ) { + const createNavigationMenu = useCreateNavigationMenu( clientId ); + const registry = useRegistry(); + + const [ status, setStatus ] = useState( CLASSIC_MENU_CONVERSION_IDLE ); + const [ value, setValue ] = useState( null ); + const [ error, setError ] = useState( null ); + + async function convertClassicMenuToBlockMenu( menuId, menuName ) { + let navigationMenu; + let classicMenuItems; + + // 1. Fetch the classic Menu items. + try { + classicMenuItems = await registry + .resolveSelect( coreStore ) + .getMenuItems( { + menus: menuId, + per_page: -1, + context: 'view', + } ); + } catch ( err ) { + throw new Error( + sprintf( + // translators: %s: the name of a menu (e.g. Header navigation). + __( `Unable to fetch classic menu "%s" from API.` ), + menuName + ), + { + cause: err, + } + ); + } + + // Handle offline response which resolves to `null`. + if ( classicMenuItems === null ) { + throw new Error( + sprintf( + // translators: %s: the name of a menu (e.g. Header navigation). + __( `Unable to fetch classic menu "%s" from API.` ), + menuName + ) + ); + } + + // 2. Convert the classic items into blocks. + const { innerBlocks } = menuItemsToBlocks( classicMenuItems ); + + // 3. Create the `wp_navigation` Post with the blocks. + try { + navigationMenu = await createNavigationMenu( + menuName, + innerBlocks + ); + } catch ( err ) { + throw new Error( + sprintf( + // translators: %s: the name of a menu (e.g. Header navigation). + __( `Unable to create Navigation Menu "%s".` ), + menuName + ), + { + cause: err, + } + ); + } + + return navigationMenu; + } + + const convert = useCallback( + ( menuId, menuName ) => { + if ( ! menuId || ! menuName ) { + setError( 'Unable to convert menu. Missing menu details.' ); + setStatus( CLASSIC_MENU_CONVERSION_ERROR ); + return; + } + + setStatus( CLASSIC_MENU_CONVERSION_PENDING ); + setValue( null ); + setError( null ); + + convertClassicMenuToBlockMenu( menuId, menuName ) + .then( ( navMenu ) => { + setValue( navMenu ); + setStatus( CLASSIC_MENU_CONVERSION_SUCCESS ); + } ) + .catch( ( err ) => { + setError( err?.message ); + setStatus( CLASSIC_MENU_CONVERSION_ERROR ); + + // Rethrow error for debugging. + throw new Error( + sprintf( + // translators: %s: the name of a menu (e.g. Header navigation). + __( `Unable to create Navigation Menu "%s".` ), + menuName + ), + { + cause: err, + } + ); + } ); + }, + [ clientId ] + ); + + return { + convert, + status, + value, + error, + }; +} + +export default useConvertClassicToBlockMenu; diff --git a/packages/block-library/src/navigation/edit/use-navigation-notice.js b/packages/block-library/src/navigation/edit/use-navigation-notice.js index 2466c79c2e7d5..ad0c6d33587c9 100644 --- a/packages/block-library/src/navigation/edit/use-navigation-notice.js +++ b/packages/block-library/src/navigation/edit/use-navigation-notice.js @@ -5,19 +5,19 @@ import { useRef } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; import { store as noticeStore } from '@wordpress/notices'; -function useNavigationNotice( { name, message } = {} ) { +function useNavigationNotice( { name, message = '' } = {} ) { const noticeRef = useRef(); const { createWarningNotice, removeNotice } = useDispatch( noticeStore ); - const showNotice = () => { + const showNotice = ( customMsg ) => { if ( noticeRef.current ) { return; } noticeRef.current = name; - createWarningNotice( message, { + createWarningNotice( customMsg || message, { id: noticeRef.current, type: 'snackbar', } ); diff --git a/packages/block-library/src/navigation/use-convert-classic-menu.js b/packages/block-library/src/navigation/use-convert-classic-menu.js deleted file mode 100644 index c857e9c97cb0d..0000000000000 --- a/packages/block-library/src/navigation/use-convert-classic-menu.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * WordPress dependencies - */ -import { useCallback, useState, useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import useNavigationEntities from './use-navigation-entities'; -import menuItemsToBlocks from './menu-items-to-blocks'; - -export default function useConvertClassicMenu( onFinish ) { - const [ selectedMenu, setSelectedMenu ] = useState(); - const [ - isAwaitingMenuItemResolution, - setIsAwaitingMenuItemResolution, - ] = useState( false ); - const [ menuName, setMenuName ] = useState( '' ); - - const { menuItems, hasResolvedMenuItems } = useNavigationEntities( - selectedMenu - ); - - const createFromMenu = useCallback( - ( name ) => { - const { innerBlocks: blocks } = menuItemsToBlocks( menuItems ); - onFinish( blocks, name ); - }, - [ menuItems, menuItemsToBlocks, onFinish ] - ); - - useEffect( () => { - // If the user selected a menu but we had to wait for menu items to - // finish resolving, then create the block once resolution finishes. - if ( isAwaitingMenuItemResolution && hasResolvedMenuItems ) { - createFromMenu( menuName ); - setIsAwaitingMenuItemResolution( false ); - } - }, [ isAwaitingMenuItemResolution, hasResolvedMenuItems, menuName ] ); - - return useCallback( - ( id, name ) => { - setSelectedMenu( id ); - - // If we have menu items, create the block right away. - if ( hasResolvedMenuItems ) { - createFromMenu( name ); - return; - } - - // Otherwise, create the block when resolution finishes. - setIsAwaitingMenuItemResolution( true ); - // Store the name to use later. - setMenuName( name ); - }, - [ hasResolvedMenuItems, createFromMenu ] - ); -}