Skip to content

Commit

Permalink
Navigation: Fallback to a classic menu if one is available (#44173)
Browse files Browse the repository at this point in the history
* Navigation: Fallback to a classic menu if one is availiable

* only use classic menus if there is one

* only import classic menus if there's only one of them

* fix gutenberg_convert_menu_items_to_blocks

* refactor

* only create navigation once, and create it published

* remove the code that makes the block dirty

* convert navigation with submenu items correctly

* php lint

* use a different function to convert classic menus to blocks

* add error handling

* Update packages/block-library/src/navigation/index.php

Co-authored-by: Dave Smith <getdavemail@gmail.com>

* Update packages/block-library/src/navigation/index.php

Co-authored-by: Dave Smith <getdavemail@gmail.com>

* Update packages/block-library/src/navigation/index.php

Co-authored-by: Jonny Harris <spacedmonkey@users.noreply.github.com>

* pass second param as true

* Use existing classic menu conversion state

* Update packages/block-library/src/navigation/edit/index.js

Co-authored-by: Dave Smith <getdavemail@gmail.com>

* rename function

* Fixes multiple menu creation on import. Also:

- refactores the effects for block menu fallback and classic menu fallback

Co-authored-by: Dave Smith <444434+getdave@users.noreply.github.com>
Co-authored-by: Ben Dwyer <275961+scruffian@users.noreply.github.com>

* return early when we won't continue

* a comment about where the duplicated code starts

* Update packages/block-library/src/navigation/index.php

Co-authored-by: Dave Smith <getdavemail@gmail.com>

* Update packages/block-library/src/navigation/index.php

Co-authored-by: Dave Smith <getdavemail@gmail.com>

* make the callback from the useEffect return a function not a promise

* remove await as this doesn't need to be async

* Prevent classic menus from importing twice'

* revert unnecessary changes

* lint fix

* Rename variable for clarity

* Fix var rename

* Reset the menu id on error

* Revert testing code

* serialize blocks in the function that gets the classic menu

Co-authored-by: Dave Smith <getdavemail@gmail.com>
Co-authored-by: Jonny Harris <spacedmonkey@users.noreply.github.com>
Co-authored-by: Andrei Draganescu <andrei.draganescu@automattic.com>
Co-authored-by: Dave Smith <444434+getdave@users.noreply.github.com>
Co-authored-by: Ben Dwyer <275961+scruffian@users.noreply.github.com>
  • Loading branch information
6 people authored Oct 6, 2022
1 parent 7f3993f commit 8e0515c
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 34 deletions.
67 changes: 45 additions & 22 deletions packages/block-library/src/navigation/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ function Navigation( {

// Preload classic menus, so that they don't suddenly pop-in when viewing
// the Select Menu dropdown.
useNavigationEntities();
const { menus: classicMenus } = useNavigationEntities();

const [ showNavigationMenuStatusNotice, hideNavigationMenuStatusNotice ] =
useNavigationNotice( {
Expand Down Expand Up @@ -216,6 +216,20 @@ function Navigation( {
const navMenuResolvedButMissing =
hasResolvedNavigationMenus && isNavigationMenuMissing;

const {
convert: convertClassicMenu,
status: classicMenuConversionStatus,
error: classicMenuConversionError,
} = useConvertClassicToBlockMenu( clientId );

const isConvertingClassicMenu =
classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING;

// Only autofallback to published menus.
const fallbackNavigationMenus = navigationMenus?.filter(
( menu ) => menu.status === 'publish'
);

// Attempt to retrieve and prioritize any existing navigation menu unless:
// - the are uncontrolled inner blocks already present in the block.
// - the user is creating a new menu.
Expand All @@ -228,23 +242,17 @@ function Navigation( {
hasUncontrolledInnerBlocks ||
isCreatingNavigationMenu ||
ref ||
! navigationMenus?.length
! fallbackNavigationMenus?.length
) {
return;
}

navigationMenus.sort( ( menuA, menuB ) => {
fallbackNavigationMenus.sort( ( menuA, menuB ) => {
const menuADate = new Date( menuA.date );
const menuBDate = new Date( menuB.date );
return menuADate.getTime() < menuBDate.getTime();
} );

// Only autofallback to published menus.
const fallbackNavigationMenus = navigationMenus.filter(
( menu ) => menu.status === 'publish'
);
if ( fallbackNavigationMenus.length === 0 ) return;

/**
* This fallback displays (both in editor and on front)
* a list of pages only if no menu (user assigned or
Expand All @@ -256,16 +264,26 @@ function Navigation( {
setRef( fallbackNavigationMenus[ 0 ].id );
}, [ navigationMenus ] );

const navRef = useRef();
useEffect( () => {
if (
! hasResolvedNavigationMenus ||
isConvertingClassicMenu ||
fallbackNavigationMenus?.length > 0 ||
classicMenus?.length !== 1
) {
return false;
}

const {
convert: convertClassicMenu,
status: classicMenuConversionStatus,
error: classicMenuConversionError,
} = useConvertClassicToBlockMenu( clientId );
// If there's non fallback navigation menus and
// only one classic menu then create a new navigation menu based on it.
convertClassicMenu(
classicMenus[ 0 ].id,
classicMenus[ 0 ].name,
'publish'
);
}, [ hasResolvedNavigationMenus ] );

const isConvertingClassicMenu =
classicMenuConversionStatus === CLASSIC_MENU_CONVERSION_PENDING;
const navRef = useRef();

// The standard HTML5 tag for the block wrapper.
const TagName = 'nav';
Expand All @@ -280,10 +298,11 @@ function Navigation( {
! isCreatingNavigationMenu &&
! isConvertingClassicMenu &&
hasResolvedNavigationMenus &&
classicMenus?.length === 0 &&
! hasUncontrolledInnerBlocks;

useEffect( () => {
if ( isPlaceholder && ! ref ) {
if ( isPlaceholder ) {
/**
* this fallback only displays (both in editor and on front)
* the list of pages block if no menu is available as a fallback.
Expand Down Expand Up @@ -642,7 +661,8 @@ function Navigation( {
onSelectClassicMenu={ async ( classicMenu ) => {
const navMenu = await convertClassicMenu(
classicMenu.id,
classicMenu.name
classicMenu.name,
'draft'
);
if ( navMenu ) {
handleUpdateMenu( navMenu.id, {
Expand Down Expand Up @@ -723,7 +743,8 @@ function Navigation( {
onSelectClassicMenu={ async ( classicMenu ) => {
const navMenu = await convertClassicMenu(
classicMenu.id,
classicMenu.name
classicMenu.name,
'draft'
);
if ( navMenu ) {
handleUpdateMenu( navMenu.id, {
Expand Down Expand Up @@ -808,7 +829,8 @@ function Navigation( {
onSelectClassicMenu={ async ( classicMenu ) => {
const navMenu = await convertClassicMenu(
classicMenu.id,
classicMenu.name
classicMenu.name,
'draft'
);
if ( navMenu ) {
handleUpdateMenu( navMenu.id, {
Expand Down Expand Up @@ -836,7 +858,8 @@ function Navigation( {
onSelectClassicMenu={ async ( classicMenu ) => {
const navMenu = await convertClassicMenu(
classicMenu.id,
classicMenu.name
classicMenu.name,
'draft'
);
if ( navMenu ) {
handleUpdateMenu( navMenu.id, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const CLASSIC_MENU_CONVERSION_ERROR = 'error';
export const CLASSIC_MENU_CONVERSION_PENDING = 'pending';
export const CLASSIC_MENU_CONVERSION_IDLE = 'idle';

// This is needed to ensure that multiple components using this hook
// do not import the same classic menu twice.
let classicMenuBeingConvertedId = null;

function useConvertClassicToBlockMenu( clientId ) {
/*
* The wp_navigation post is created as a draft so the changes on the frontend and
Expand All @@ -32,7 +36,11 @@ function useConvertClassicToBlockMenu( clientId ) {
const [ status, setStatus ] = useState( CLASSIC_MENU_CONVERSION_IDLE );
const [ error, setError ] = useState( null );

async function convertClassicMenuToBlockMenu( menuId, menuName ) {
async function convertClassicMenuToBlockMenu(
menuId,
menuName,
postStatus = 'publish'
) {
let navigationMenu;
let classicMenuItems;

Expand Down Expand Up @@ -76,7 +84,8 @@ function useConvertClassicToBlockMenu( clientId ) {
try {
navigationMenu = await createNavigationMenu(
menuName,
innerBlocks
innerBlocks,
postStatus
);

/**
Expand All @@ -91,7 +100,7 @@ function useConvertClassicToBlockMenu( clientId ) {
'wp_navigation',
navigationMenu.id,
{
status: 'publish',
status: postStatus,
},
{ throwOnError: true }
);
Expand All @@ -111,7 +120,15 @@ function useConvertClassicToBlockMenu( clientId ) {
return navigationMenu;
}

const convert = useCallback( async ( menuId, menuName ) => {
const convert = useCallback( async ( menuId, menuName, postStatus ) => {
// Check whether this classic menu is being imported already.
if ( classicMenuBeingConvertedId === menuId ) {
return;
}

// Set the ID for the currently importing classic menu.
classicMenuBeingConvertedId = menuId;

if ( ! menuId || ! menuName ) {
setError( 'Unable to convert menu. Missing menu details.' );
setStatus( CLASSIC_MENU_CONVERSION_ERROR );
Expand All @@ -121,15 +138,25 @@ function useConvertClassicToBlockMenu( clientId ) {
setStatus( CLASSIC_MENU_CONVERSION_PENDING );
setError( null );

return await convertClassicMenuToBlockMenu( menuId, menuName )
return await convertClassicMenuToBlockMenu(
menuId,
menuName,
postStatus
)
.then( ( navigationMenu ) => {
setStatus( CLASSIC_MENU_CONVERSION_SUCCESS );
// Reset the ID for the currently importing classic menu.
classicMenuBeingConvertedId = null;
return navigationMenu;
} )
.catch( ( err ) => {
setError( err?.message );
// Reset the ID for the currently importing classic menu.
setStatus( CLASSIC_MENU_CONVERSION_ERROR );

// Reset the ID for the currently importing classic menu.
classicMenuBeingConvertedId = null;

// Rethrow error for debugging.
throw new Error(
sprintf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ 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,
postStatus = 'publish'
) {
export default function useCreateNavigationMenu( clientId ) {
const [ status, setStatus ] = useState( CREATE_NAVIGATION_MENU_IDLE );
const [ value, setValue ] = useState( null );
const [ error, setError ] = useState( null );
Expand All @@ -30,7 +27,7 @@ export default function useCreateNavigationMenu(
// This callback uses data from the two placeholder steps and only creates
// a new navigation menu when the user completes the final step.
const create = useCallback(
async ( title = null, blocks = [] ) => {
async ( title = null, blocks = [], postStatus ) => {
// 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.
Expand Down
104 changes: 102 additions & 2 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,14 +248,108 @@ function block_core_navigation_render_submenu_icon() {
return '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" focusable="false"><path d="M1.50002 4L6.00002 8L10.5 4" stroke-width="1.5"></path></svg>';
}

/**
* Get the classic navigation menu to use as a fallback.
*
* @return object WP_Term The classic navigation.
*/
function block_core_navigation_get_classic_menu_fallback() {
$classic_nav_menus = wp_get_nav_menus();

// If menus exist.
if ( $classic_nav_menus && ! is_wp_error( $classic_nav_menus ) && count( $classic_nav_menus ) === 1 ) {
// Use the first classic menu only. Handles simple use case where user has a single
// classic menu and switches to a block theme. In future this maybe expanded to
// determine the most appropriate classic menu to be used based on location.
return $classic_nav_menus[0];
}
}

/**
* Converts a classic navigation to blocks.
*
* @param object $classic_nav_menu WP_Term The classic navigation object to convert.
* @return array the normalized parsed blocks.
*/
function block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ) {
// BEGIN: Code that already exists in wp_nav_menu().
$menu_items = wp_get_nav_menu_items( $classic_nav_menu->term_id, array( 'update_post_term_cache' => false ) );

// Set up the $menu_item variables.
_wp_menu_item_classes_by_context( $menu_items );

$sorted_menu_items = array();
foreach ( (array) $menu_items as $menu_item ) {
$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
}

unset( $menu_items, $menu_item );

// END: Code that already exists in wp_nav_menu().

$menu_items_by_parent_id = array();
foreach ( $sorted_menu_items as $menu_item ) {
$menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
}

$inner_blocks = block_core_navigation_parse_blocks_from_menu_items(
isset( $menu_items_by_parent_id[0] )
? $menu_items_by_parent_id[0]
: array(),
$menu_items_by_parent_id
);

return serialize_blocks( $inner_blocks );
}

/**
* If there's a the classic menu then use it as a fallback.
*
* @return array the normalized parsed blocks.
*/
function block_core_navigation_maybe_use_classic_menu_fallback() {
// See if we have a classic menu.
$classic_nav_menu = block_core_navigation_get_classic_menu_fallback();

if ( ! $classic_nav_menu ) {
return;
}

// If we have a classic menu then convert it to blocks.
$classic_nav_menu_blocks = block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu );

if ( empty( $classic_nav_menu_blocks ) ) {
return;
}

// Create a new navigation menu from the classic menu.
$wp_insert_post_result = wp_insert_post(
array(
'post_content' => $classic_nav_menu_blocks,
'post_title' => $classic_nav_menu->slug,
'post_name' => $classic_nav_menu->slug,
'post_status' => 'publish',
'post_type' => 'wp_navigation',
),
true // So that we can check whether the result is an error.
);

if ( is_wp_error( $wp_insert_post_result ) ) {
return;
}

// Fetch the most recently published navigation which will be the classic one created above.
return block_core_navigation_get_most_recently_published_navigation();
}

/**
* Finds the most recently published `wp_navigation` Post.
*
* @return WP_Post|null the first non-empty Navigation or null.
*/
function block_core_navigation_get_most_recently_published_navigation() {
// We default to the most recently created menu.

// Default to the most recently created menu.
$parsed_args = array(
'post_type' => 'wp_navigation',
'no_found_rows' => true,
Expand Down Expand Up @@ -319,7 +413,13 @@ function block_core_navigation_get_fallback_blocks() {

$navigation_post = block_core_navigation_get_most_recently_published_navigation();

// Prefer using the first non-empty Navigation as fallback if available.
// If there are no navigation posts then try to find a classic menu
// and convert it into a block based navigation menu.
if ( ! $navigation_post ) {
$navigation_post = block_core_navigation_maybe_use_classic_menu_fallback();
}

// Use the first non-empty Navigation as fallback if available.
if ( $navigation_post ) {
$maybe_fallback = block_core_navigation_filter_out_empty_blocks( parse_blocks( $navigation_post->post_content ) );

Expand Down

0 comments on commit 8e0515c

Please sign in to comment.