Skip to content

Commit

Permalink
Consolidate Navigation fallbacks logic between Editor and Front of Si…
Browse files Browse the repository at this point in the history
…te via REST API (#48698)

* Add Navigation REST class with fallback endpoint

* Scaffold out tests

* Add fallback creation method and tests

* Add tests for returning existing Navigation posts

* Use Gutenberg versions of block functions

* Add classic meny and concurrent request assertions

* Test does not create from classic if Navigation menu exists

* Test for page list being unavailable

* Extract common Nav fallback code into dedicated class

* Remove changes to load

* Move get recent navigation into common class

* Migrate functions to class and deprecate originals

* Refactor method to get fallback to exit early style

* Use new class in render_callback

* Add example of dedicated class tests over REST endpoint tests

* Add example of refactoring editor code to use new API endpoint

* Update docblock

* Add permissions check and test

* Improve comments

* Adjust test user

* Revert removal of deprecated function bodies

* Reinstate deprecations

* Update editor notice

* Update endpoint to singular form (fallback)

* Rename class to be specifically related to fallbacks and non generic

* Also update file names

* Avoid returning unecessary WP_Error

* Avoid get_post and use performance internal method

* Normalize functions to standardise signatures

* Extract Menu parsing logic into separate class

* Refactor classic menu fallback logic

* Add test for Classic Menu in Primary Location

* Add test for classic menu fallback to “primary” slug

* Add further tests for classic menu fallback mechanic

* Tidy

* Controller docblocks

* Docblocks for WP_Navigation_Fallbacks_Gutenberg

* Simplfy fallbacks sequence

* Tweak error slug

* Use array() syntax

* Improve function naming for clarity

* Try to fix PHPCS errors and warnings

* Rename class

Resolves https://github.com/WordPress/gutenberg/pull/48698/files#r1147427800

* Correct return type

* Update naming for consistency

Resolves https://github.com/WordPress/gutenberg/pull/48698/files#r1150229415

* Extract function for default registered blocks

* Corrects spelling in comment

* Use more appropriate assertion

* Remove code references that no longer apply

* Remove PHP based ordering.

wp_nav_menu applies ordering by default already and keying the menu items by order key seems to be pointless as the key is not used.

https://developer.wordpress.org/reference/functions/wp_get_nav_menu_items/#parameters

* Rename class to match file

This may change later as the Gutenberg suffix may be unnecessary

* Move to standalone block editor REST endpoint

No longer extends Posts Controller endpoint.

* Migrate all tests to the Domain class and remove from REST endpoint tests

* Add smoke test to REST endpoint

* Augment smoke test to check for number of menus created

* Add schema and associated test

* Add useful comment

* Update editor logic to expect an ID from the REST endpoint

* Correct return type

* Add default domain to i18n

* Try to get the file docblock comment correct

* Another file comment update

* Update lib/experimental/class-wp-rest-navigation-fallbacks-controller.php

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

* Update REST method naming to use `get_item`

* Move method inline with main permissions check

* Ensure rest response for endpoint

* Simplify perrmissions checks

* Add and translate error messages

* Remove erroneous comment

* Use static function

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

* Add @Covers notation

* Add messages to assertions

* Add messages to all assertions

* Remove unused method

* Update correct return type

* Implement prepare_item_for_response

* Revert to returning object in REST response

* Adjust tests to assert on new return type

* Add response context filtering

* Revert to `array` accidentally introduced in refactor

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

* add an extra comment to keep the linter happy, maybe

* Check fields before included them in response

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

* Improve test name

* Update e2e test to expect Page List fallback Menu to be auto-created

* Revert fallbacks invocation to simplified method

Addresses https://github.com/WordPress/gutenberg/pull/48698/files#r1147610071

* Correct privacy on members of WP_Navigation_Fallbacks_Gutenberg

* Rename test and remove associated @doesNotPerformAssertions

* Remove unnecessary test for WP_Error

* Update comment

Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>

* Minor comment rewording.

Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>

* Remove unnecessary comments

Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>

* Reword comment to align with block terminology

Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>

* Improve comment wording.

Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>

* Remove some comments

Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>

* Rename classes with singular “fallback”

* Extract conditional to var for clarity

Addresses https://github.com/WordPress/gutenberg/pull/48698/files#r1171571842

* Rename converter class

Addresses https://github.com/WordPress/gutenberg/pull/48698/files#r1171568403

* Fix comments in JS

* Add comments to improve clarity around fallbacks logic

Addresses #48698 (comment)

* Shorten method name to reduce verbosity

Addresses #48698 (comment)

* Remove unnecessary comments from self documenting code

Addresses #48698 (comment)

* Rename variables for clarity

Addresses #48698 (comment)

* Fix PHPCS

* Make converter class static

Follows from discussion at #48698 (comment)

* Improve var naming

* Ensure all assertions have messages

* Add @Covers annotation

* Add links to the response

* Add test for inclusion of links in Response

* Fix PHPCS

---------

Co-authored-by: Ben Dwyer <ben@scruffian.com>
Co-authored-by: Jonny Harris <spacedmonkey@users.noreply.github.com>
Co-authored-by: Andrei Draganescu <me@andreidraganescu.info>
  • Loading branch information
4 people authored Apr 21, 2023
1 parent 8c13771 commit 1e9cbb4
Show file tree
Hide file tree
Showing 10 changed files with 1,119 additions and 119 deletions.
111 changes: 111 additions & 0 deletions lib/experimental/class-wp-classic-to-block-menu-converter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
/**
* WP_Classic_To_Block_Menu_Converter class
*
* @package gutenberg
* @since 6.3.0
*/

/**
* Converts a Classic Menu to Block Menu blocks.
*
* @access public
*/
class WP_Classic_To_Block_Menu_Converter {

/**
* Converts a Classic Menu to blocks.
*
* @param WP_Term $menu The Menu term object of the menu to convert.
* @return string the serialized and normalized parsed blocks.
*/
public static function convert( $menu ) {
$menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );

// Set up the $menu_item variables.
// Adds the class property classes for the current context, if applicable.
_wp_menu_item_classes_by_context( $menu_items );

$menu_items_by_parent_id = static::group_by_parent_id( $menu_items );

$first_menu_item = isset( $menu_items_by_parent_id[0] )
? $menu_items_by_parent_id[0]
: array();

$inner_blocks = static::to_blocks(
$first_menu_item,
$menu_items_by_parent_id
);

return serialize_blocks( $inner_blocks );
}

/**
* Returns an array of menu items grouped by the id of the parent menu item.
*
* @param array $menu_items An array of menu items.
* @return array
*/
private static function group_by_parent_id( $menu_items ) {
$menu_items_by_parent_id = array();

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

return $menu_items_by_parent_id;
}

/**
* Turns menu item data into a nested array of parsed blocks
*
* @param array $menu_items An array of menu items that represent
* an individual level of a menu.
* @param array $menu_items_by_parent_id An array keyed by the id of the
* parent menu where each element is an
* array of menu items that belong to
* that parent.
* @return array An array of parsed block data.
*/
private static function to_blocks( $menu_items, $menu_items_by_parent_id ) {

if ( empty( $menu_items ) ) {
return array();
}

$blocks = array();

foreach ( $menu_items as $menu_item ) {
$class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null;
$id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null;
$opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target;
$rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null;
$kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom';

$block = array(
'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link',
'attrs' => array(
'className' => $class_name,
'description' => $menu_item->description,
'id' => $id,
'kind' => $kind,
'label' => $menu_item->title,
'opensInNewTab' => $opens_in_new_tab,
'rel' => $rel,
'title' => $menu_item->attr_title,
'type' => $menu_item->object,
'url' => $menu_item->url,
),
);

$block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] )
? static::to_blocks( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id )
: array();
$block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] );

$blocks[] = $block;
}

return $blocks;
}
}
233 changes: 233 additions & 0 deletions lib/experimental/class-wp-navigation-fallback-gutenberg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
<?php
/**
* WP_Navigation_Fallback_Gutenberg class
*
* Manages fallback behavior for Navigation menus.
*
* @package Gutenberg
* @subpackage Navigation
* @since 6.3.0
*/

/**
* Import dependencies.
*/
require __DIR__ . '/class-wp-classic-to-block-menu-converter.php';

/**
* Manages fallback behavior for Navigation menus.
*
* @access public
*/
class WP_Navigation_Fallback_Gutenberg {

/**
* Gets (and/or creates) an appropriate fallback Navigation Menu.
*
* @return WP_Post|null the fallback Navigation Post or null.
*/
public static function get_fallback() {

$fallback = static::get_most_recently_published_navigation();

if ( $fallback ) {
return $fallback;
}

$fallback = static::create_classic_menu_fallback();

if ( $fallback && ! is_wp_error( $fallback ) ) {
// Return the newly created fallback post object which will now be the most recently created navigation menu.
return $fallback instanceof WP_Post ? $fallback : static::get_most_recently_published_navigation();
}

$fallback = static::create_default_fallback();

if ( $fallback && ! is_wp_error( $fallback ) ) {
// Return the newly created fallback post object which will now be the most recently created navigation menu.
return $fallback instanceof WP_Post ? $fallback : static::get_most_recently_published_navigation();
}

return null;
}

/**
* Finds the most recently published `wp_navigation` post type.
*
* @return WP_Post|null the first non-empty Navigation or null.
*/
private static function get_most_recently_published_navigation() {

$parsed_args = array(
'post_type' => 'wp_navigation',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'order' => 'DESC',
'orderby' => 'date',
'post_status' => 'publish',
'posts_per_page' => 1,
);

$navigation_post = new WP_Query( $parsed_args );

if ( count( $navigation_post->posts ) > 0 ) {
return $navigation_post->posts[0];
}

return null;
}

/**
* Creates a Navigation Menu post from a Classic Menu.
*
* @return int|WP_Error The post ID of the default fallback menu or a WP_Error object.
*/
private static function create_classic_menu_fallback() {
// See if we have a classic menu.
$classic_nav_menu = static::get_fallback_classic_menu();

if ( ! $classic_nav_menu ) {
return new WP_Error( 'no_classic_menus', __( 'No Classic Menus found.', 'gutenberg' ) );
}

// If there is a classic menu then convert it to blocks.
$classic_nav_menu_blocks = WP_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu );

if ( empty( $classic_nav_menu_blocks ) ) {
return new WP_Error( 'cannot_convert_classic_menu', __( 'Unable to convert Classic Menu to blocks.', 'gutenberg' ) );
}

// Create a new navigation menu from the classic menu.
$classic_menu_fallback = wp_insert_post(
array(
'post_content' => $classic_nav_menu_blocks,
'post_title' => $classic_nav_menu->name,
'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.
);

return $classic_menu_fallback;
}

/**
* Determine the most appropriate classic navigation menu to use as a fallback.
*
* @return WP_Term|null The most appropriate classic navigation menu to use as a fallback.
*/
private static function get_fallback_classic_menu() {
$classic_nav_menus = wp_get_nav_menus();

if ( ! $classic_nav_menus || is_wp_error( $classic_nav_menus ) ) {
return null;
}

$nav_menu = static::get_nav_menu_at_primary_location();

if ( $nav_menu ) {
return $nav_menu;
}

$nav_menu = static::get_nav_menu_with_primary_slug( $classic_nav_menus );

if ( $nav_menu ) {
return $nav_menu;
}

return static::get_most_recently_created_nav_menu( $classic_nav_menus );
}


/**
* Sorts the classic menus and returns the most recently created one.
*
* @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects.
* @return WP_Term The most recently created classic nav menu.
*/
private static function get_most_recently_created_nav_menu( $classic_nav_menus ) {
usort(
$classic_nav_menus,
static function( $a, $b ) {
return $b->term_id - $a->term_id;
}
);

return $classic_nav_menus[0];
}

/**
* Returns the classic menu with the slug `primary` if it exists.
*
* @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects.
* @return WP_Term|null The classic nav menu with the slug `primary` or null.
*/
private static function get_nav_menu_with_primary_slug( $classic_nav_menus ) {
foreach ( $classic_nav_menus as $classic_nav_menu ) {
if ( 'primary' === $classic_nav_menu->slug ) {
return $classic_nav_menu;
}
}

return null;
}


/**
* Gets the classic menu assigned to the `primary` navigation menu location
* if it exists.
*
* @return WP_Term|null The classic nav menu assigned to the `primary` location or null.
*/
private static function get_nav_menu_at_primary_location() {
$locations = get_nav_menu_locations();

if ( isset( $locations['primary'] ) ) {
$primary_menu = wp_get_nav_menu_object( $locations['primary'] );

if ( $primary_menu ) {
return $primary_menu;
}
}

return null;
}

/**
* Creates a default Navigation Block Menu fallback.
*
* @return int|WP_Error The post ID of the default fallback menu or a WP_Error object.
*/
private static function create_default_fallback() {

$default_blocks = static::get_default_fallback_blocks();

// Create a new navigation menu from the fallback blocks.
$default_fallback = wp_insert_post(
array(
'post_content' => $default_blocks,
'post_title' => _x( 'Navigation', 'Title of a Navigation menu', 'gutenberg' ),
'post_name' => 'navigation',
'post_status' => 'publish',
'post_type' => 'wp_navigation',
),
true // So that we can check whether the result is an error.
);

return $default_fallback;
}

/**
* Gets the rendered markup for the default fallback blocks.
*
* @return string default blocks markup to use a the fallback.
*/
private static function get_default_fallback_blocks() {
$registry = WP_Block_Type_Registry::get_instance();

// If `core/page-list` is not registered then use empty blocks.
return $registry->is_registered( 'core/page-list' ) ? '<!-- wp:page-list /-->' : '';
}
}
Loading

0 comments on commit 1e9cbb4

Please sign in to comment.