Skip to content

Commit

Permalink
Improve create new UI feedback in Nav block (#39219)
Browse files Browse the repository at this point in the history
* Refactor hook to async pattern with state

* Extract createNavigation to top level edit and provide UI feedback

* Extract createNavigation to top level edit and provide UI feedback

* Handle create empty in upper component

* Expose errors to improve debugging

* Implement notices for menu creation

* Ensure fallback title by waiting on default title generation

* Guard against empty Navigation title

* Remove unused hook introduced via rebase

* Export and use status constants for create menu hook

* Rename function name to better communicate intent

* Fix convert classic menu hook to conform to new create hook API

* Use status constants

* Fix test by ensuring reference to Nav block is current when unfocused.

* Add test for loading indicator whilst creating Navigation

* Add test for notice upon creation of empty Navigation Menu
  • Loading branch information
getdave authored Mar 9, 2022
1 parent 9ffe470 commit a0e4559
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 45 deletions.
73 changes: 64 additions & 9 deletions packages/block-library/src/navigation/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ import useConvertClassicToBlockMenu, {
CLASSIC_MENU_CONVERSION_PENDING,
CLASSIC_MENU_CONVERSION_SUCCESS,
} from './use-convert-classic-menu-to-block-menu';
import useCreateNavigationMenu, {
CREATE_NAVIGATION_MENU_ERROR,
CREATE_NAVIGATION_MENU_PENDING,
CREATE_NAVIGATION_MENU_SUCCESS,
} from './use-create-navigation-menu';

const EMPTY_ARRAY = [];

Expand Down Expand Up @@ -162,6 +167,51 @@ function Navigation( {
// the Select Menu dropdown.
useNavigationEntities();

const [
showNavigationMenuCreateNotice,
hideNavigationMenuCreateNotice,
] = useNavigationNotice( {
name: 'block-library/core/navigation/create',
} );

const {
create: createNavigationMenu,
status: createNavigationMenuStatus,
error: createNavigationMenuError,
value: createNavigationMenuPost,
} = useCreateNavigationMenu( clientId );

const isCreatingNavigationMenu =
createNavigationMenuStatus === CREATE_NAVIGATION_MENU_PENDING;

useEffect( () => {
hideNavigationMenuCreateNotice();

if ( createNavigationMenuStatus === CREATE_NAVIGATION_MENU_PENDING ) {
speak( __( `Creating Navigation Menu.` ) );
}

if ( createNavigationMenuStatus === CREATE_NAVIGATION_MENU_SUCCESS ) {
setRef( createNavigationMenuPost.id );
selectBlock( clientId );

showNavigationMenuCreateNotice(
__( `Navigation Menu successfully created.` )
);
}

if ( createNavigationMenuStatus === CREATE_NAVIGATION_MENU_ERROR ) {
showNavigationMenuCreateNotice(
__( 'Failed to create Navigation Menu.' )
);
}
}, [
createNavigationMenu,
createNavigationMenuStatus,
createNavigationMenuError,
createNavigationMenuPost,
] );

const {
hasUncontrolledInnerBlocks,
uncontrolledInnerBlocks,
Expand Down Expand Up @@ -251,24 +301,28 @@ function Navigation( {
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.
// - there is no ref attribute pointing to a Navigation Post.
// - there is no classic menu conversion process in progress.
// - there is no menu creation process in progress.
// - there are no uncontrolled blocks.
// - (legacy) there is a Navigation Area without a ref attribute pointing to a Navigation Post.
const isPlaceholder =
! ref &&
! isCreatingNavigationMenu &&
! isConvertingClassicMenu &&
( ! hasUncontrolledInnerBlocks || isWithinUnassignedArea );

const isEntityAvailable =
! isNavigationMenuMissing && isNavigationMenuResolved;

// "loading" state:
// - we are running the Classic Menu conversion process.
// - there is a menu creation process in progress.
// - there is a classic menu conversion process in progress.
// OR
// - there is a ref attribute pointing to a Navigation Post
// - the Navigation Post isn't available (hasn't resolved) yet.
const isLoading =
isCreatingNavigationMenu ||
isConvertingClassicMenu ||
!! ( ref && ! isEntityAvailable && ! isConvertingClassicMenu );

Expand Down Expand Up @@ -466,7 +520,7 @@ function Navigation( {
[ convert, handleUpdateMenu ]
);

const startWithEmptyMenu = useCallback( () => {
const resetToEmptyBlock = useCallback( () => {
registry.batch( () => {
if ( navigationArea ) {
setAreaMenu( 0 );
Expand Down Expand Up @@ -528,7 +582,7 @@ function Navigation( {
{ __(
'Navigation menu has been deleted or is unavailable. '
) }
<Button onClick={ startWithEmptyMenu } variant="link">
<Button onClick={ resetToEmptyBlock } variant="link">
{ __( 'Create a new menu?' ) }
</Button>
</Warning>
Expand Down Expand Up @@ -569,6 +623,7 @@ function Navigation( {
isResolvingCanUserCreateNavigationMenu
}
onFinish={ handleSelectNavigation }
onCreateEmpty={ () => createNavigationMenu( '', [] ) }
/>
</TagName>
);
Expand All @@ -584,7 +639,7 @@ function Navigation( {
currentMenuId={ ref }
clientId={ clientId }
onSelect={ handleSelectNavigation }
onCreateNew={ startWithEmptyMenu }
onCreateNew={ resetToEmptyBlock }
/* translators: %s: The name of a menu. */
actionLabel={ __( "Switch to '%s'" ) }
showManageActions
Expand Down Expand Up @@ -739,7 +794,7 @@ function Navigation( {
{ hasResolvedCanUserDeleteNavigationMenu &&
canUserDeleteNavigationMenu && (
<NavigationMenuDeleteControl
onDelete={ startWithEmptyMenu }
onDelete={ resetToEmptyBlock }
/>
) }
</InspectorControls>
Expand Down
21 changes: 2 additions & 19 deletions packages/block-library/src/navigation/edit/placeholder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { useEffect } from '@wordpress/element';
*/
import useNavigationEntities from '../../use-navigation-entities';
import PlaceholderPreview from './placeholder-preview';
import useCreateNavigationMenu from '../use-create-navigation-menu';
import NavigationMenuSelector from '../navigation-menu-selector';

export default function NavigationPlaceholder( {
Expand All @@ -22,26 +21,10 @@ export default function NavigationPlaceholder( {
canUserCreateNavigationMenu = false,
isResolvingCanUserCreateNavigationMenu,
onFinish,
onCreateEmpty,
} ) {
const createNavigationMenu = useCreateNavigationMenu( clientId );

const onFinishMenuCreation = async (
blocks,
navigationMenuTitle = null
) => {
const navigationMenu = await createNavigationMenu(
navigationMenuTitle,
blocks
);
onFinish( navigationMenu, blocks );
};

const { isResolvingMenus, hasResolvedMenus } = useNavigationEntities();

const onCreateEmptyMenu = () => {
onFinishMenuCreation( [] );
};

useEffect( () => {
if ( ! isSelected ) {
return;
Expand Down Expand Up @@ -98,7 +81,7 @@ export default function NavigationPlaceholder( {
{ canUserCreateNavigationMenu && (
<Button
variant="tertiary"
onClick={ onCreateEmptyMenu }
onClick={ onCreateEmpty }
>
{ __( 'Start empty' ) }
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export default function UnsavedInnerBlocks( {

const { hasResolvedNavigationMenus, navigationMenus } = useNavigationMenu();

const createNavigationMenu = useCreateNavigationMenu( clientId );
const { create: createNavigationMenu } = useCreateNavigationMenu(
clientId
);

// Automatically save the uncontrolled blocks.
useEffect( async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export const CLASSIC_MENU_CONVERSION_PENDING = 'pending';
export const CLASSIC_MENU_CONVERSION_IDLE = 'idle';

function useConvertClassicToBlockMenu( clientId ) {
const createNavigationMenu = useCreateNavigationMenu( clientId );
const { create: createNavigationMenu } = useCreateNavigationMenu(
clientId
);
const registry = useRegistry();

const [ status, setStatus ] = useState( CLASSIC_MENU_CONVERSION_IDLE );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,87 @@
import { serialize } from '@wordpress/blocks';
import { store as coreStore } from '@wordpress/core-data';
import { useDispatch } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { useState, useCallback } from '@wordpress/element';

/**
* Internal dependencies
*/
import useGenerateDefaultNavigationTitle from './use-generate-default-navigation-title';

export const CREATE_NAVIGATION_MENU_SUCCESS = 'success';
export const CREATE_NAVIGATION_MENU_ERROR = 'error';
export const CREATE_NAVIGATION_MENU_PENDING = 'pending';
export const CREATE_NAVIGATION_MENU_IDLE = 'idle';

export default function useCreateNavigationMenu( clientId ) {
const [ status, setStatus ] = useState( CREATE_NAVIGATION_MENU_IDLE );
const [ value, setValue ] = useState( null );
const [ error, setError ] = useState( null );

const { saveEntityRecord } = useDispatch( coreStore );
const generateDefaultTitle = useGenerateDefaultNavigationTitle( clientId );

// This callback uses data from the two placeholder steps and only creates
// a new navigation menu when the user completes the final step.
return useCallback(
const create = useCallback(
async ( title = null, blocks = [] ) => {
// Guard against creating Navigations without a title.
// Note you can pass no title, but if one is passed it must be
// a string otherwise the title may end up being empty.
if ( title && typeof title !== 'string' ) {
setError(
'Invalid title supplied when creating Navigation Menu.'
);
setStatus( CREATE_NAVIGATION_MENU_ERROR );
throw new Error(
`Value of supplied title argument was not a string.`
);
}

setStatus( CREATE_NAVIGATION_MENU_PENDING );
setValue( null );
setError( null );

if ( ! title ) {
title = await generateDefaultTitle();
title = await generateDefaultTitle().catch( ( err ) => {
setError( err?.message );
setStatus( CREATE_NAVIGATION_MENU_ERROR );
throw new Error(
'Failed to create title when saving new Navigation Menu.',
{
cause: err,
}
);
} );
}
const record = {
title,
content: serialize( blocks ),
status: 'publish',
};

return await saveEntityRecord(
'postType',
'wp_navigation',
record
);
// Return affords ability to await on this function directly
return saveEntityRecord( 'postType', 'wp_navigation', record )
.then( ( response ) => {
setValue( response );
setStatus( CREATE_NAVIGATION_MENU_SUCCESS );
return response;
} )
.catch( ( err ) => {
setError( err?.message );
setStatus( CREATE_NAVIGATION_MENU_ERROR );
throw new Error( 'Unable to save new Navigation Menu', {
cause: err,
} );
} );
},
[ serialize, saveEntityRecord ]
);

return {
create,
status,
value,
error,
};
}
Loading

0 comments on commit a0e4559

Please sign in to comment.