diff --git a/src/wp-admin/js/customize-nav-menus.js b/src/wp-admin/js/customize-nav-menus.js index 628ffa8c11..b4e7be5ff4 100644 --- a/src/wp-admin/js/customize-nav-menus.js +++ b/src/wp-admin/js/customize-nav-menus.js @@ -1250,16 +1250,21 @@ return; } - var titleEl = control.container.find( '.menu-item-title' ); + var titleEl = control.container.find( '.menu-item-title' ), + titleText = item.title || api.Menus.data.l10n.untitled; + + if ( item._invalid ) { + titleText = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', titleText ); + } // Don't update to an empty title. if ( item.title ) { titleEl - .text( item.title ) + .text( titleText ) .removeClass( 'no-title' ); } else { titleEl - .text( api.Menus.data.l10n.untitled ) + .text( titleText ) .addClass( 'no-title' ); } } ); @@ -1303,9 +1308,9 @@ 'menu-item-edit-inactive' ]; - if ( settingValue.invalid ) { - containerClasses.push( 'invalid' ); - control.params.title = api.Menus.data.invalidTitleTpl.replace( '%s', control.params.title ); + if ( settingValue._invalid ) { + containerClasses.push( 'menu-item-invalid' ); + control.params.title = api.Menus.data.l10n.invalidTitleTpl.replace( '%s', control.params.title ); } else if ( 'draft' === settingValue.status ) { containerClasses.push( 'pending' ); control.params.title = api.Menus.data.pendingTitleTpl.replace( '%s', control.params.title ); diff --git a/src/wp-includes/class-wp-customize-setting.php b/src/wp-includes/class-wp-customize-setting.php index 01b84c8e8b..65fa2e2234 100644 --- a/src/wp-includes/class-wp-customize-setting.php +++ b/src/wp-includes/class-wp-customize-setting.php @@ -885,7 +885,7 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 'status' => 'publish', 'original_title' => '', 'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item(). - // @todo also expose invalid? + '_invalid' => false, ); /** @@ -1144,6 +1144,14 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { } } + if ( ! isset( $this->value['_invalid'] ) ) { + $this->value['_invalid'] = ( + ( 'post_type' === $this->value['type'] && ! post_type_exists( $this->value['object'] ) ) + || + ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) ) + ); + } + // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value. $irrelevant_properties = array( 'ID', @@ -1246,6 +1254,8 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { $should_remove = ( false === $this_item || + true === $this_item['_invalid'] + || ( $this->original_nav_menu_term_id === $menu->term_id && @@ -1417,6 +1427,7 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { 'status' => 'publish', 'original_title' => '', 'nav_menu_term_id' => 0, + '_invalid' => false, ); $menu_item_value = array_merge( $default, $menu_item_value ); $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) ); @@ -1449,6 +1460,8 @@ class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting { $menu_item_value['status'] = 'publish'; } + $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid']; + /** This filter is documented in wp-includes/class-wp-customize-setting.php */ return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this ); } diff --git a/tests/phpunit/tests/customize/nav-menu-item-setting.php b/tests/phpunit/tests/customize/nav-menu-item-setting.php index ad2e7d2494..367d94744a 100644 --- a/tests/phpunit/tests/customize/nav-menu-item-setting.php +++ b/tests/phpunit/tests/customize/nav-menu-item-setting.php @@ -92,6 +92,7 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { 'status' => 'publish', 'original_title' => '', 'nav_menu_term_id' => 0, + '_invalid' => false, ); $this->assertEquals( $default, $setting->default ); @@ -458,6 +459,7 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { 'status' => 'forbidden', 'original_title' => 'Hi', 'nav_menu_term_id' => 'heilo', + '_invalid' => false, ); $sanitized = $setting->sanitize( $unsanitized ); @@ -664,4 +666,47 @@ class Test_WP_Customize_Nav_Menu_Item_Setting extends WP_UnitTestCase { $this->assertEquals( 'deleted', $update_result['status'] ); } + /** + * @ticket 33665 + */ + function test_invalid_nav_menu_item() { + $menu_id = wp_create_nav_menu( 'Primary' ); + register_post_type( 'poem', array( + 'public' => true, + ) ); + + $post_id = self::factory()->post->create( array( 'post_type' => 'poem', 'post_title' => 'Code is poetry.' ) ); + $post = get_post( $post_id ); + $item_id = wp_update_nav_menu_item( $menu_id, 0, array( + 'menu-item-type' => 'post_type', + 'menu-item-object' => 'poem', + 'menu-item-object-id' => $post_id, + 'menu-item-title' => $post->post_title, + 'menu-item-status' => 'publish', + 'menu-item-position' => 1, + ) ); + $setting_id = "nav_menu_item[$item_id]"; + + do_action( 'customize_register', $this->wp_customize ); + $setting = $this->wp_customize->get_setting( $setting_id ); + $this->assertNotEmpty( $setting ); + $value = $setting->value(); + $this->assertFalse( $value['_invalid'] ); + $value_object = $setting->value_as_wp_post_nav_menu_item(); + $this->assertFalse( $value_object->_invalid ); + + $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id ); + $value = $setting->value(); + $this->assertFalse( $value['_invalid'] ); + $value_object = $setting->value_as_wp_post_nav_menu_item(); + $this->assertFalse( $value_object->_invalid ); + + _unregister_post_type( 'poem' ); + $setting = new WP_Customize_Nav_Menu_Item_Setting( $this->wp_customize, $setting_id ); + $value = $setting->value(); + $this->assertTrue( $value['_invalid'] ); + $value_object = $setting->value_as_wp_post_nav_menu_item(); + $this->assertTrue( $value_object->_invalid ); + } + }