Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate Navigation fallbacks logic between Editor and Front of Site via REST API #48698

Merged
merged 110 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
f8a66b8
Add Navigation REST class with fallback endpoint
getdave Mar 2, 2023
c7aa87f
Scaffold out tests
getdave Mar 22, 2023
1115c7b
Add fallback creation method and tests
getdave Mar 22, 2023
84a1af9
Add tests for returning existing Navigation posts
getdave Mar 22, 2023
c7d168f
Use Gutenberg versions of block functions
getdave Mar 22, 2023
5ea9a22
Add classic meny and concurrent request assertions
getdave Mar 22, 2023
24cfd94
Test does not create from classic if Navigation menu exists
getdave Mar 22, 2023
13d33df
Test for page list being unavailable
getdave Mar 22, 2023
13bb243
Extract common Nav fallback code into dedicated class
getdave Mar 22, 2023
b3d51a6
Remove changes to load
getdave Mar 22, 2023
0ecd828
Move get recent navigation into common class
getdave Mar 22, 2023
80ff0d9
Migrate functions to class and deprecate originals
getdave Mar 22, 2023
c1dd6f5
Refactor method to get fallback to exit early style
getdave Mar 22, 2023
e9f021d
Use new class in render_callback
getdave Mar 23, 2023
e85b9c8
Add example of dedicated class tests over REST endpoint tests
getdave Mar 23, 2023
9befce9
Add example of refactoring editor code to use new API endpoint
getdave Mar 23, 2023
38f3712
Update docblock
getdave Mar 23, 2023
0576ade
Add permissions check and test
getdave Mar 23, 2023
47ea683
Improve comments
getdave Mar 23, 2023
b0f19ce
Adjust test user
getdave Mar 23, 2023
0686177
Revert removal of deprecated function bodies
getdave Mar 23, 2023
7c39ed8
Reinstate deprecations
getdave Mar 23, 2023
1a8cc32
Update editor notice
getdave Mar 23, 2023
aaada54
Update endpoint to singular form (fallback)
getdave Mar 23, 2023
4649e99
Rename class to be specifically related to fallbacks and non generic
getdave Mar 23, 2023
8668dec
Also update file names
getdave Mar 23, 2023
d10cdd0
Avoid returning unecessary WP_Error
getdave Mar 23, 2023
6132be9
Avoid get_post and use performance internal method
getdave Mar 23, 2023
4c152e4
Normalize functions to standardise signatures
getdave Mar 23, 2023
457d390
Extract Menu parsing logic into separate class
getdave Mar 24, 2023
4734199
Refactor classic menu fallback logic
getdave Mar 24, 2023
1b750d5
Add test for Classic Menu in Primary Location
getdave Mar 24, 2023
e1cbab0
Add test for classic menu fallback to “primary” slug
getdave Mar 24, 2023
ad48992
Add further tests for classic menu fallback mechanic
getdave Mar 24, 2023
fd79656
Tidy
getdave Mar 24, 2023
f433cf2
Controller docblocks
getdave Mar 24, 2023
d5b54c4
Docblocks for WP_Navigation_Fallbacks_Gutenberg
getdave Mar 24, 2023
8aa039f
Simplfy fallbacks sequence
getdave Mar 24, 2023
4cf43ab
Tweak error slug
getdave Mar 24, 2023
7037a38
Use array() syntax
getdave Mar 24, 2023
2669c52
Improve function naming for clarity
getdave Mar 24, 2023
4b3c1c9
Try to fix PHPCS errors and warnings
getdave Mar 24, 2023
be54021
Rename class
getdave Mar 30, 2023
5f5341a
Correct return type
getdave Mar 30, 2023
11d1b1b
Update naming for consistency
getdave Mar 30, 2023
f4ee89d
Extract function for default registered blocks
getdave Mar 30, 2023
29a20c9
Corrects spelling in comment
getdave Mar 30, 2023
321db0a
Use more appropriate assertion
getdave Mar 30, 2023
2a9ffcf
Remove code references that no longer apply
getdave Mar 30, 2023
6001581
Remove PHP based ordering.
getdave Mar 31, 2023
1bba273
Rename class to match file
getdave Mar 31, 2023
cc075a9
Move to standalone block editor REST endpoint
getdave Apr 17, 2023
9603ce8
Migrate all tests to the Domain class and remove from REST endpoint t…
getdave Apr 17, 2023
7143798
Add smoke test to REST endpoint
getdave Apr 17, 2023
f8ef276
Augment smoke test to check for number of menus created
getdave Apr 17, 2023
eba41ce
Add schema and associated test
getdave Apr 18, 2023
f6082f9
Add useful comment
getdave Apr 18, 2023
577b32d
Update editor logic to expect an ID from the REST endpoint
getdave Apr 18, 2023
3479818
Correct return type
getdave Apr 18, 2023
953a515
Add default domain to i18n
getdave Apr 18, 2023
c1657dc
Try to get the file docblock comment correct
getdave Apr 18, 2023
449e2ee
Another file comment update
getdave Apr 18, 2023
38a3a50
Update lib/experimental/class-wp-rest-navigation-fallbacks-controller…
scruffian Apr 18, 2023
59c69c2
Update REST method naming to use `get_item`
getdave Apr 18, 2023
00b83e4
Move method inline with main permissions check
getdave Apr 18, 2023
e8a6356
Ensure rest response for endpoint
getdave Apr 18, 2023
0d29d95
Simplify perrmissions checks
getdave Apr 18, 2023
b1660fa
Add and translate error messages
getdave Apr 18, 2023
5a53b92
Remove erroneous comment
getdave Apr 18, 2023
1b18d69
Use static function
getdave Apr 18, 2023
12ecaba
Add @covers notation
getdave Apr 18, 2023
b631518
Add messages to assertions
getdave Apr 18, 2023
b00ac6b
Add messages to all assertions
getdave Apr 18, 2023
6037f45
Remove unused method
getdave Apr 18, 2023
de35df2
Update correct return type
getdave Apr 18, 2023
2590732
Implement prepare_item_for_response
getdave Apr 18, 2023
1f201d6
Revert to returning object in REST response
getdave Apr 18, 2023
2d09c18
Adjust tests to assert on new return type
getdave Apr 18, 2023
b69b3f1
Add response context filtering
getdave Apr 18, 2023
6ad35cc
Revert to `array` accidentally introduced in refactor
getdave Apr 18, 2023
6277397
add an extra comment to keep the linter happy, maybe
scruffian Apr 18, 2023
6f85821
Check fields before included them in response
getdave Apr 19, 2023
fa1b395
Improve test name
getdave Apr 19, 2023
f228b7e
Update e2e test to expect Page List fallback Menu to be auto-created
getdave Apr 19, 2023
8b718cc
Revert fallbacks invocation to simplified method
getdave Apr 19, 2023
c9f3c05
Correct privacy on members of WP_Navigation_Fallbacks_Gutenberg
getdave Apr 19, 2023
2c6bb93
Rename test and remove associated @doesNotPerformAssertions
getdave Apr 19, 2023
d5d6833
Remove unnecessary test for WP_Error
getdave Apr 19, 2023
b919057
Update comment
getdave Apr 19, 2023
734d3e4
Minor comment rewording.
getdave Apr 19, 2023
8a61e15
Remove unnecessary comments
getdave Apr 19, 2023
eed97ff
Reword comment to align with block terminology
getdave Apr 19, 2023
ccd235b
Improve comment wording.
getdave Apr 19, 2023
2164b79
Remove some comments
getdave Apr 19, 2023
1f715df
Rename classes with singular “fallback”
getdave Apr 19, 2023
7794109
Extract conditional to var for clarity
getdave Apr 19, 2023
9f213d5
Rename converter class
getdave Apr 19, 2023
638f7e2
Fix comments in JS
getdave Apr 19, 2023
a2a8e3c
Add comments to improve clarity around fallbacks logic
getdave Apr 19, 2023
308571f
Shorten method name to reduce verbosity
getdave Apr 19, 2023
eaceebe
Remove unnecessary comments from self documenting code
getdave Apr 19, 2023
d3b85da
Rename variables for clarity
getdave Apr 19, 2023
a3bf3be
Fix PHPCS
getdave Apr 20, 2023
3238826
Make converter class static
getdave Apr 20, 2023
191dd14
Improve var naming
getdave Apr 20, 2023
09e1681
Ensure all assertions have messages
getdave Apr 20, 2023
780bc12
Add @covers annotation
getdave Apr 20, 2023
56f181b
Add links to the response
getdave Apr 21, 2023
da4cdb9
Add test for inclusion of links in Response
getdave Apr 21, 2023
db39cdb
Fix PHPCS
getdave Apr 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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