diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 027b3b87a0a8a7..10055770dcf856 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -41,7 +41,6 @@ import { } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; -import { createBlock, getBlockType } from '@wordpress/blocks'; import { close, Icon } from '@wordpress/icons'; /** @@ -308,71 +307,6 @@ function Navigation( { // that automatically saves the menu as an entity when changes are made to the inner blocks. const hasUnsavedBlocks = hasUncontrolledInnerBlocks && ! isEntityAvailable; - useEffect( () => { - if ( - ref || - ! hasResolvedNavigationMenus || - isConvertingClassicMenu || - fallbackNavigationMenus?.length > 0 || - hasUnsavedBlocks - ) { - return; - } - - // If there's non fallback navigation menus and - // a classic menu with a `primary` location or slug, - // then create a new navigation menu based on it. - // Otherwise, use the most recently created classic menu. - if ( classicMenus?.length ) { - const primaryMenus = classicMenus.filter( - ( classicMenu ) => - classicMenu.locations.includes( 'primary' ) || - classicMenu.slug === 'primary' - ); - - if ( primaryMenus.length ) { - convertClassicMenu( - primaryMenus[ 0 ].id, - primaryMenus[ 0 ].name, - 'publish' - ); - } else { - classicMenus.sort( ( a, b ) => { - return b.id - a.id; - } ); - convertClassicMenu( - classicMenus[ 0 ].id, - classicMenus[ 0 ].name, - 'publish' - ); - } - } else { - // If there are no fallback navigation menus and no classic menus, - // then create a new navigation menu. - - // Check that we have a page-list block type. - let defaultBlocks = []; - if ( getBlockType( 'core/page-list' ) ) { - defaultBlocks = [ createBlock( 'core/page-list' ) ]; - } - - createNavigationMenu( - 'Navigation', // TODO - use the template slug in future - defaultBlocks, - 'publish' - ); - } - }, [ - hasResolvedNavigationMenus, - hasUnsavedBlocks, - classicMenus, - convertClassicMenu, - createNavigationMenu, - fallbackNavigationMenus?.length, - isConvertingClassicMenu, - ref, - ] ); - const navRef = useRef(); // The standard HTML5 tag for the block wrapper. diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 5ec43046d6618f..f6facb0f72c71a 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -325,26 +325,31 @@ function block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_me } /** - * If there's a the classic menu then use it as a fallback. + * Checks for a Classic Menu and attempts to convert into a block-based + * Navigation menu. * - * @return array the normalized parsed blocks. + * @return int|WP_Error The Navigation menu post ID on success. A WP_Error on failure. */ -function block_core_navigation_maybe_use_classic_menu_fallback() { - // See if we have a classic menu. +function block_core_navigation_create_classic_menu_fallback() { + // See if we have a Classic menu. $classic_nav_menu = block_core_navigation_get_classic_menu_fallback(); + // Exit early if one does not exist. if ( ! $classic_nav_menu ) { - return; + return new WP_Error( 'no_classic_menu', __( 'No classic menu exists to convert to a block.' ) ); } - // If we have a classic menu then convert it to blocks. + // 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 it's empty then exit early. if ( empty( $classic_nav_menu_blocks ) ) { - return; + return new WP_Error( 'no_classic_menu', __( 'Classic menu is empty or could not be converted to blocks.' ) ); } - // Create a new navigation menu from the classic menu. + $return_errors = true; + + // Create a new Navigation menu from the Classic menu blocks. $wp_insert_post_result = wp_insert_post( array( 'post_content' => $classic_nav_menu_blocks, @@ -353,15 +358,10 @@ function block_core_navigation_maybe_use_classic_menu_fallback() { 'post_status' => 'publish', 'post_type' => 'wp_navigation', ), - true // So that we can check whether the result is an error. + $return_errors // 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(); + return $wp_insert_post_result; } /** @@ -432,11 +432,12 @@ function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { } /** - * Create and returns a navigation menu containing a page-list as a fallback. + * Creates a navigation menu containing default fallback content. + * (a page-list if registered). * - * @return array the newly created navigation menu. + * @return int|WP_Error The post ID on success. A WP_Error on failure. */ -function block_core_navigation_get_default_pages_fallback() { +function block_core_navigation_create_default_fallback() { $registry = WP_Block_Type_Registry::get_instance(); // If `core/page-list` is not registered then use empty blocks. @@ -454,13 +455,59 @@ function block_core_navigation_get_default_pages_fallback() { true // So that we can check whether the result is an error. ); - if ( is_wp_error( $wp_insert_post_result ) ) { + return $wp_insert_post_result; +} + +/** + * Creates an appropriate fallback Navigation Menu (`wp_navigation` Post) + * to be used by the Navigation block. + * + * Behaviour: + * 1. If there is a Navigation menu already then use it. + * 2. If there is a Classic menu then convert it to a Navigation menu. + * 3. If there is no Navigation menu and no Classic menu then create a default menu. + * + * The fallback menu is only created if the theme is a block theme. + * + * @return void + */ +function block_core_navigation_create_fallback() { + $should_skip = apply_filters( 'block_core_navigation_skip_fallback', false ); + + if ( $should_skip ) { return; } - // Fetch the most recently published navigation which will be the default one created above. - return block_core_navigation_get_most_recently_published_navigation(); + // Don't create a fallback if the theme is not a block theme. + if ( ! wp_is_block_theme() ) { + return; + } + + // Get the most recently published Navigation menu. + $existing_navigation_menu = block_core_navigation_get_most_recently_published_navigation(); + + // If there is already a Navigation menu then exit. + if ( $existing_navigation_menu ) { + return; + } + + // If there are no Navigation menus then try to find a Classic menu + // and attempt to convert it into a Navigation menu. + $converted_classic_menu_id = block_core_navigation_create_classic_menu_fallback(); + + // If the conversion + creation was successful then exit. + if ( ! is_wp_error( $converted_classic_menu_id ) ) { + return; + } + + // If the Classic menu process did not result in a post + // then default to creating a default fallback Navigaiton menu. + block_core_navigation_create_default_fallback(); } +// Run on switching Theme and when installing WP for the first time. +add_action( 'switch_theme', 'block_core_navigation_create_fallback' ); +add_action( 'wp_install', 'block_core_navigation_create_fallback' ); + /** * Retrieves the appropriate fallback to be used on the front of the @@ -475,17 +522,6 @@ function block_core_navigation_get_fallback_blocks() { // Get the most recently published Navigation post. $navigation_post = block_core_navigation_get_most_recently_published_navigation(); - // 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(); - } - - // If there are no navigation posts then default to a list of Pages. - if ( ! $navigation_post ) { - $navigation_post = block_core_navigation_get_default_pages_fallback(); - } - // Use the first non-empty Navigation as fallback, there should always be one. if ( $navigation_post ) { $parsed_blocks = parse_blocks( $navigation_post->post_content ); diff --git a/phpunit/class-block-library-navigation-fallbacks-test.php b/phpunit/class-block-library-navigation-fallbacks-test.php new file mode 100644 index 00000000000000..8568ac967daf65 --- /dev/null +++ b/phpunit/class-block-library-navigation-fallbacks-test.php @@ -0,0 +1,377 @@ + 'wp_navigation', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + return $navs_in_db->posts ? $navs_in_db->posts : array(); + } + + private function mock_wp_install() { + $user = get_current_user(); + do_action( 'wp_install', $user ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_should_auto_create_navigation_menu_on_wp_install() { + $this->mock_wp_install(); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 1, $navs_in_db, 'No Navigation Menu was found.' ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_should_auto_create_navigation_menu_on_theme_switch() { + // Remove any existing disabling filters. + remove_filter( 'block_core_navigation_skip_fallback', '__return_true' ); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 0, $navs_in_db, 'Navigation Menu should not exist before switching Theme.' ); + + // Should trigger creation of Navigation Menu if one does not already exist. + switch_theme( 'emptytheme' ); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 1, $navs_in_db, 'No Navigation Menu was found.' ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_should_not_auto_create_navigation_menu_on_theme_switch_to_classic_theme() { + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 0, $navs_in_db, 'Navigation Menu should not exist before switching Theme.' ); + + // Should trigger creation of Navigation Menu if one does not already exist. + switch_theme( 'twentytwenty' ); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 0, $navs_in_db, 'A Navigation Menu should not exist.' ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_should_not_auto_create_navigation_menu_on_theme_switch_if_one_already_exists() { + + // Pre-add a Navigation Menu to simulate when a user already has a menu. + self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu', + 'post_content' => '', + ) + ); + + // Remove any existing disabling filters. + remove_filter( 'block_core_navigation_skip_fallback', '__return_true' ); + + // Should trigger creation of Navigation Menu if one does not already exist. + switch_theme( 'emptytheme' ); + + $navs_in_db = $this->get_navigations_in_database(); + + // There should still only be one Navigation Menu. + $this->assertCount( 1, $navs_in_db, 'A single Navigation Menu should exist.' ); + + // The existing Navigation Menu should be unchanged. + $this->assertEquals( 'Existing Navigation Menu', $navs_in_db[0]->post_title, 'The title of the Navigation Menu should match the existing Navigation Menu' ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_creates_fallback_navigation_with_existing_navigation_menu_if_found() { + + self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu 1', + 'post_content' => '', + ) + ); + + $most_recently_published_nav = self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu 2', + 'post_content' => '', + ) + ); + + gutenberg_block_core_navigation_create_fallback(); + + $fallback = $this->get_navigations_in_database()[0]; + + $this->assertEquals( $fallback->post_title, $most_recently_published_nav->post_title, 'The title of the fallback Navigation Menu should match the title of the most recently published Navigation Menu.' ); + $this->assertEquals( $fallback->post_type, $most_recently_published_nav->post_type, 'The post type of the fallback Navigation Menu should match the post type of the most recently published Navigation Menu.' ); + $this->assertEquals( $fallback->post_content, $most_recently_published_nav->post_content, 'The contents of the fallback Navigation Menu should match the contents of the most recently published Navigation Menu.' ); + $this->assertEquals( $fallback->post_status, $most_recently_published_nav->post_status, 'The status of the fallback Navigation Menu should match the status of the most recently published Navigation Menu.' ); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 2, $navs_in_db, '2 Navigation Menus should exist.' ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_creates_fallback_navigation_with_existing_classic_menu_if_found() { + + $menu_id = wp_create_nav_menu( 'Existing Classic Menu' ); + + wp_update_nav_menu_item( + $menu_id, + 0, + array( + 'menu-item-title' => 'Classic Menu Item 1', + 'menu-item-url' => '/classic-menu-item-1', + 'menu-item-status' => 'publish', + ) + ); + + gutenberg_block_core_navigation_create_fallback(); + + $fallback = $this->get_navigations_in_database()[0]; + + $this->assertEquals( 'Existing Classic Menu', $fallback->post_title, 'The title of the fallback Navigation Menu should match the name of the Classic menu.' ); + + // Assert that the fallback contains a navigation-link block. + $this->assertStringContainsString( '', $fallback->post_content, 'The fallback Navigation Menu should contain a Page List block.' ); + $this->assertEquals( 'publish', $fallback->post_status, 'The fallback Navigation Menu should be published.' ); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 1, $navs_in_db, 'A single Navigation Menu should exist.' ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_creates_fallback_from_existing_navigation_menu_even_if_classic_menu_exists() { + + // Create a Navigation Post. + $navigation_post = self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu', + 'post_content' => '', + ) + ); + + // Also create a Classic Menu - this should be ignored. + $menu_id = wp_create_nav_menu( 'Existing Classic Menu' ); + + gutenberg_block_core_navigation_create_fallback(); + + $fallback = $this->get_navigations_in_database()[0]; + + $this->assertEquals( $fallback->post_title, $navigation_post->post_title, 'The title of the fallback Navigation Menu should match that of the existing Navigation Menu.' ); + $this->assertEquals( $fallback->post_type, $navigation_post->post_type, 'The post type of the fallback Navigation Menu should match that of the existing Navigation Menu.' ); + $this->assertEquals( $fallback->post_content, $navigation_post->post_content, 'The contents of the fallback Navigation Menu should match that of the existing Navigation Menu.' ); + $this->assertEquals( $fallback->post_status, $navigation_post->post_status, 'The status of the fallback Navigation Menu should match that of the existing Navigation Menu.' ); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 1, $navs_in_db, 'A single Navigation Menu should exist.' ); + + // Cleanup. + wp_delete_nav_menu( $menu_id ); + } + + /** + * @covers ::block_core_navigation_create_fallback + */ + public function test_should_skip_if_filter_returns_truthy() { + add_filter( 'block_core_navigation_skip_fallback', '__return_true' ); + + gutenberg_block_core_navigation_create_fallback(); + + $navs_in_db = $this->get_navigations_in_database(); + $this->assertCount( 0, $navs_in_db, 'No Navigation Menus should have been created.' ); + + remove_filter( 'block_core_navigation_skip_fallback', '__return_true' ); + } + + + /** + * @covers ::gutenberg_block_core_navigation_get_fallback_blocks + */ + public function test_should_get_fallback_blocks_when_no_navigations_exist() { + $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); + + $this->assertSame( array(), $fallback_blocks, 'Fallback blocks should be an empty array.' ); + } + + /** + * @covers ::gutenberg_block_core_navigation_get_fallback_blocks + */ + public function test_should_get_blocks_from_most_recently_created_navigation() { + + // Create a fallback navigation. + self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu 1', + 'post_content' => '', + ) + ); + + // Create another fallback navigation. + self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu 2', + 'post_content' => '', + ) + ); + + $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); + + $block = $fallback_blocks[0]; + + // Check blocks match most recently Navigation Post data. + $this->assertEquals( $block['blockName'], 'core/navigation-link', '1st fallback block should match expected type.' ); + $this->assertEquals( $block['attrs']['label'], 'Hello world', '1st fallback block\'s label should match.' ); + $this->assertEquals( $block['attrs']['url'], '/hello-world', '1st fallback block\'s url should match.' ); + + } + + /** + * @covers ::gutenberg_block_core_navigation_get_fallback_blocks + */ + public function test_should_get_empty_array_if_most_recently_created_navigation_is_empty() { + + self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu', + 'post_content' => '', // empty. + ) + ); + + $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); + + $this->assertIsArray( $fallback_blocks, 'Fallback blocks should be an array.' ); + $this->assertEmpty( $fallback_blocks, 'Fallback blocks should be empty.' ); + } + + /** + * @covers ::gutenberg_block_core_navigation_get_fallback_blocks + */ + public function test_should_filter_out_empty_blocks_from_fallbacks() { + + // Create a fallback navigation. + self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu', + 'post_content' => ' ', + ) + ); + + $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); + + $first_block = $fallback_blocks[0]; + + // There should only be a single page list block and no "null" blocks. + $this->assertCount( 1, $fallback_blocks, 'Fallback blocks should contain a single block.' ); + $this->assertEquals( $first_block['blockName'], 'core/page-list', 'Fallback block should be a page list.' ); + + // Check that no "empty" blocks exist with a blockName of 'null'. + // If the block parser encounters whitespace it will return a block with a blockName of null. + // This is an intentional feature, but is undesirable for our fallbacks. + $null_blocks = array_filter( + $fallback_blocks, + function( $block ) { + return null === $block['blockName']; + } + ); + $this->assertEmpty( $null_blocks, 'Fallback blocks should not contain any null blocks.' ); + } + + /** + * @covers ::gutenberg_block_core_navigation_get_fallback_blocks + */ + public function test_should_get_filtered_blocks_if_fallback_is_filtered() { + + function use_site_logo() { + return parse_blocks( '' ); + } + + add_filter( 'block_core_navigation_render_fallback', 'use_site_logo' ); + + self::factory()->post->create_and_get( + array( + 'post_type' => 'wp_navigation', + 'post_title' => 'Existing Navigation Menu', + 'post_content' => '', + ) + ); + + $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); + + $block = $fallback_blocks[0]; + + // Check blocks match most recently Navigation Post data. + $this->assertEquals( $block['blockName'], 'core/site-logo', '1st fallback block should match expected type.' ); + + remove_filter( 'block_core_navigation_render_fallback', 'use_site_logo' ); + } +} + +