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

Copy navigation area infrastructure from Gutenberg #1865

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions src/wp-admin/includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -2447,3 +2447,63 @@ function the_block_editor_meta_box_post_form_hidden_fields( $post ) {
*/
do_action( 'block_editor_meta_box_hidden_fields', $post );
}

/**
* Disable block editor for wp_navigation type posts so they can be managed via the UI.
*
* @since 5.9.0
* @access private
*
* @param bool $value Whether the CPT supports block editor or not.
* @param string $post_type Post type.
*
* @return bool
*/
function _disable_block_editor_for_navigation_post_type( $value, $post_type ) {
if ( 'wp_navigation' === $post_type ) {
return false;
}

return $value;
}

/**
* This callback disables the content editor for wp_navigation type posts.
* Content editor cannot handle wp_navigation type posts correctly.
* We cannot disable the "editor" feature in the wp_navigation's CPT definition
* because it disables the ability to save navigation blocks via REST API.
*
* @since 5.9.0
* @access private
*
* @param WP_Post $post An instance of WP_Post class.
*/
function _disable_content_editor_for_navigation_post_type( $post ) {
$post_type = get_post_type( $post );
if ( 'wp_navigation' !== $post_type ) {
return;
}

remove_post_type_support( $post_type, 'editor' );
}

/**
* This callback enables content editor for wp_navigation type posts.
* We need to enable it back because we disable it to hide
* the content editor for wp_navigation type posts.
*
* @since 5.9.0
* @access private
*
* @see _disable_content_editor_for_navigation_post_type
*
* @param WP_Post $post An instance of WP_Post class.
*/
function _enable_content_editor_for_navigation_post_type( $post ) {
$post_type = get_post_type( $post );
if ( 'wp_navigation' !== $post_type ) {
return;
}

add_post_type_support( $post_type, 'editor' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we doing this? Are not just not register support for editor, until we are ready?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We temporarily disable it in _disable_content_editor_for_navigation_post_type so that the editor doesn't appear when you edit the CPT. This re-enables it.

}
10 changes: 10 additions & 0 deletions src/wp-admin/site-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,17 @@ static function( $classes ) {
'/wp/v2/global-styles/' . $active_global_styles_id . '?context=edit',
'/wp/v2/global-styles/' . $active_global_styles_id,
'/wp/v2/themes/' . $active_theme . '/global-styles',
'/wp/v2/block-navigation-areas?context=edit',
);

$areas = get_option( 'fse_navigation_areas', array() );
$active_areas = array_intersect_key( $areas, get_navigation_areas() );
foreach ( $active_areas as $post_id ) {
if ( $post_id ) {
$preload_paths[] = add_query_args( 'context', 'edit', rest_get_route_for_post( $post_id ) );
}
}

block_editor_rest_api_preload( $preload_paths, $block_editor_context );

$editor_settings = get_block_editor_settings(
Expand Down
14 changes: 13 additions & 1 deletion src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,17 @@
add_action( 'admin_footer-post-new.php', 'wp_add_iframed_editor_assets_html' );
add_action( 'admin_footer-widgets.php', 'wp_add_iframed_editor_assets_html' );

add_action( 'use_block_editor_for_post_type', '_disable_block_editor_for_navigation_post_type', 10, 2 );
add_action( 'edit_form_after_title', '_disable_content_editor_for_navigation_post_type' );
add_action( 'edit_form_after_editor', '_enable_content_editor_for_navigation_post_type' );

/*
* Disable "Post Attributes" for wp_navigation post type. The attributes are
* also conditionally enabled when a site has custom templates. Block Theme
* templates can be available for every post type.
*/
add_filter( 'theme_wp_navigation_templates', '__return_empty_array' );

// Taxonomy.
add_action( 'init', 'create_initial_taxonomies', 0 ); // Highest priority.
add_action( 'change_locale', 'create_initial_taxonomies' );
Expand Down Expand Up @@ -670,6 +681,7 @@
add_action( 'setup_theme', 'wp_enable_block_templates' );

// Navigation areas.
add_action( 'setup_theme', '_register_default_navigation_areas' );
add_action( 'setup_theme', '_wp_register_default_navigation_areas' );
add_action( 'switch_theme', '_wp_migrate_menu_to_navigation_post', 99, 3 );

unset( $filter, $action );
207 changes: 206 additions & 1 deletion src/wp-includes/navigation-areas.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function register_navigation_areas( $new_areas ) {
* @since 5.9.0
* @access private
*/
function _register_default_navigation_areas() {
function _wp_register_default_navigation_areas() {
register_navigation_areas(
array(
'primary' => _x( 'Primary', 'navigation area' ),
Expand All @@ -50,3 +50,208 @@ function get_navigation_areas() {
global $navigation_areas;
return $navigation_areas;
}

/**
* Migrates classic menus to a block-based navigation post on theme switch.
* Assigns the created navigation post to the corresponding navigation area.
*
* @since 5.9.0
* @access private
*
* @param string $new_name Name of the new theme.
* @param WP_Theme $new_theme New theme.
* @param WP_Theme $old_theme Old theme.
*/
function _wp_migrate_menu_to_navigation_post( $new_name, WP_Theme $new_theme, WP_Theme $old_theme ) {
// Do nothing when switching to a theme that does not support site editor.
if ( ! wp_is_block_template_theme() ) {
return;
}

// get_nav_menu_locations() calls get_theme_mod() which depends on the stylesheet option.
// At the same time, switch_theme runs only after the stylesheet option was updated to $new_theme.
// To retrieve theme mods of the old theme, the getter is hooked to get_option( 'stylesheet' ) so that we
// get the old theme, which causes the get_nav_menu_locations to get the locations of the old theme.
$get_old_theme_stylesheet = static function() use ( $old_theme ) {
return $old_theme->get_stylesheet();
};
add_filter( 'option_stylesheet', $get_old_theme_stylesheet );

$locations = get_nav_menu_locations();
$area_mapping = get_option( 'fse_navigation_areas', array() );
noisysocks marked this conversation as resolved.
Show resolved Hide resolved

foreach ( $locations as $location_name => $menu_id ) {
// Get the menu from the location, skipping if there is no
// menu or there was an error.
$menu = wp_get_nav_menu_object( $menu_id );
if ( ! $menu || is_wp_error( $menu ) ) {
continue;
}

$menu_items = _wp_get_menu_items_at_location( $location_name );
if ( empty( $menu_items ) ) {
continue;
}

$post_name = 'classic_menu_' . $menu_id;

// Get or create to avoid creating too many wp_navigation posts.
$query = new WP_Query;
$matching_posts = $query->query(
array(
'name' => $post_name,
'post_status' => 'publish',
'post_type' => 'wp_navigation',
'posts_per_page' => 1,
'fields' => 'ids',
)
);

if ( ! empty( $matching_posts ) ) {
$navigation_post_id = $matching_posts[0]->ID;
} else {
$menu_items_by_parent_id = _wp_sort_menu_items_by_parent_id( $menu_items );
$parsed_blocks = _wp_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id );
$post_data = array(
'post_type' => 'wp_navigation',
'post_title' => sprintf(
/* translators: %s: the name of the menu, e.g. "Main Menu". */
__( 'Classic menu: %s' ),
$menu->name
),
'post_name' => $post_name,
'post_content' => serialize_blocks( $parsed_blocks ),
'post_status' => 'publish',
);
$navigation_post_id = wp_insert_post( $post_data );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There NEEDs to be some error handling here. What is the database falls over and this returns a WP_Error object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should happen in this case? @talldan @adamziel

Copy link
Contributor

@adamziel adamziel Nov 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning error here and then Error 500 from the API endpoint sounds like a sensible thing to do

}

$area_mapping[ $location_name ] = $navigation_post_id;
}
remove_filter( 'option_stylesheet', $get_old_theme_stylesheet );

update_option( 'fse_navigation_areas', $area_mapping );
}

/**
* Returns the menu items for a WordPress menu location.
*
* @since 5.9.0
* @access private
*
* @param string $location The menu location.
* @return array Menu items for the location.
*/
function _wp_get_menu_items_at_location( $location ) {
if ( empty( $location ) ) {
return;
}

// Build menu data. The following approximates the code in `wp_nav_menu()`.

// Find the location in the list of locations, returning early if the
// location can't be found.
$locations = get_nav_menu_locations();
if ( ! isset( $locations[ $location ] ) ) {
return;
}

// Get the menu from the location, returning early if there is no
// menu or there was an error.
$menu = wp_get_nav_menu_object( $locations[ $location ] );
if ( ! $menu || is_wp_error( $menu ) ) {
return;
}

$menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
_wp_menu_item_classes_by_context( $menu_items );

return $menu_items;
}

/**
* Sorts a standard array of menu items into a nested structure keyed by the
* id of the parent menu.
*
* @since 5.9.0
* @access private
*
* @param array $menu_items Menu items to sort.
* @return array An array keyed by the id of the parent menu where each element
* is an array of menu items that belong to that parent.
*/
function _wp_sort_menu_items_by_parent_id( $menu_items ) {
$sorted_menu_items = array();
foreach ( $menu_items as $menu_item ) {
$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
}
unset( $menu_items, $menu_item );

$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;
}

return $menu_items_by_parent_id;
}

/**
* Turns menu item data into a nested array of parsed blocks
*
* @since 5.9.0
* @access private
*
* @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.
*/
function _wp_parse_blocks_from_menu_items( $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,
),
);

if ( isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ) {
$block['innerBlocks'] = _wp_parse_blocks_from_menu_items(
$menu_items_by_parent_id[ $menu_item->ID ],
$menu_items_by_parent_id
);
} else {
$block['innerBlocks'] = array();
}

$block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] );

$blocks[] = $block;
}

return $blocks;
}
7 changes: 3 additions & 4 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -480,29 +480,28 @@ function create_initial_post_types() {
'labels' => array(
'name' => __( 'Navigation Menus' ),
'singular_name' => __( 'Navigation Menu' ),
'menu_name' => _x( 'Navigation Menus', 'Admin Menu text' ),
'add_new' => _x( 'Add New', 'Navigation Menu' ),
'add_new_item' => __( 'Add New Navigation Menu' ),
'new_item' => __( 'New Navigation Menu' ),
'edit_item' => __( 'Edit Navigation Menu' ),
'view_item' => __( 'View Navigation Menu' ),
'all_items' => __( 'All Navigation Menus' ),
'all_items' => __( 'Navigation Menus' ),
'search_items' => __( 'Search Navigation Menus' ),
'parent_item_colon' => __( 'Parent Navigation Menu:' ),
'not_found' => __( 'No Navigation Menu found.' ),
'not_found_in_trash' => __( 'No Navigation Menu found in Trash.' ),
'archives' => __( 'Navigation Menu archives' ),
'insert_into_item' => __( 'Insert into Navigation Menu' ),
'uploaded_to_this_item' => __( 'Uploaded to this Navigation Menu' ),
// Some of these are a bit weird, what are they for?
'filter_items_list' => __( 'Filter Navigation Menu list' ),
'items_list_navigation' => __( 'Navigation Menus list navigation' ),
'items_list' => __( 'Navigation Menus list' ),
),
'description' => __( 'Navigation menus that can be inserted into your site.' ),
'public' => false,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'has_archive' => false,
'show_ui' => false,
'show_ui' => wp_is_block_template_theme(),
'show_in_menu' => 'themes.php',
'show_in_admin_bar' => false,
'show_in_rest' => true,
Expand Down
2 changes: 1 addition & 1 deletion tests/qunit/fixtures/wp-api-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -11445,7 +11445,7 @@ mockedApiResponse.TypesCollection = {
}
},
"wp_navigation": {
"description": "",
"description": "Navigation menus that can be inserted into your site.",
"hierarchical": false,
"name": "Navigation Menus",
"slug": "wp_navigation",
Expand Down