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

Register Navigation Link block variations based on menu item types added in customizer filters #31584

Closed
wants to merge 8 commits into from
128 changes: 128 additions & 0 deletions lib/class-wp-rest-menu-custom-items-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php
Copy link
Member

Choose a reason for hiding this comment

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

Why can't think be part of the existing menu items controller?

Also why does this have to part of the REST API at all. Can't be loaded in via localize script or similar?

Copy link
Contributor

Choose a reason for hiding this comment

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

The results are shown in the search results within <LinkControl> are retrieved on demand from the REST API using this API within Gutenberg.

It feels like the custom items should also be retrieved using the same mechanism.

From the discussion in the PR thread it looks like there are a number of options being proposed so this may change direction.

/**
* WP_REST_Menu_Custom_Items_Controller class.
*
* @package gutenberg
*/

/**
* Class that returns the menu items added via the
* `customize_nav_menu_available_items` filter.
*/
class WP_REST_Menu_Custom_Items_Controller extends WP_REST_Controller {
Copy link
Contributor

Choose a reason for hiding this comment

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

You'll want to tag in someone from the REST API team such as @TimothyBJacobs to take a look at this one. They have a regular Core team office hours in WP Core Slack if you need to get some traction.

Copy link
Member

Choose a reason for hiding this comment

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

@getdave Can you provide context on what this endpoint does and why it is needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, although I would defer to @Aljullu who is the PR author.

The endpoint is introduced purely to retrieve any custom nav menu items that have been added by the developer via the customize_nav_menu_available_items filter. As you are probably aware, this filter already exists in Core.

The items are then displayed in the Nav Editor in the <LinkControl> search results which you can see in the screenshots attached to this PR.

@Aljullu also left a good explanation of the context behind this in the PR description and in the accompanying Issue.

As I mentioned in my review of this PR, I would hope that we will not actually need to introduce a new endpoint but rather the functionality proposed can be introduced to the main Menu Items REST API endpoint. I will however, defer to your greater experience working on these endpoints.

Copy link
Member

Choose a reason for hiding this comment

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

@Aljullu @getdave I think this code should live in the menu items endpoint. The fields map pretty well.

Maybe something along the line of.

/wp/v2/menu-items/custom.

@TimothyBJacobs WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

If this is the direction we are going, I think the REST API should be created in a new PR, with unit tests etc. Once it is merged, this PR can be rebased.

/**
* Constructor.
*/
public function __construct() {
$this->namespace = '__experimental';
$this->rest_base = 'menu-custom-items';
}

/**
* Registers the necessary REST API routes.
*
* @access public
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_menu_custom_items' ),
'permission_callback' => array( $this, 'permissions_check' ),
'args' => $this->get_collection_params(),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}

/**
* Checks if a given request has access to read menu items if they have access to edit them.
*
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function permissions_check() {
$post_type = get_post_type_object( 'nav_menu_item' );
if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to view menu items.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}

/**
* Retrieves the menu custom items' schema, conforming to JSON Schema.
*
* @return array Item schema data.
*/
public function get_item_schema() {
$this->schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'menu-custom-items',
'type' => 'object',
'properties' => array(

'id' => array(
'description' => __( 'Unique identifier for the menu item.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ),
),
'title' => array(
'description' => __( 'Human-readable name identifying the menu item.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ),
),
'type_label' => array(
'description' => __( 'Type of link.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ),
),
'url' => array(
'description' => __( 'URL of the link.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ),
),
),
);

return $this->add_additional_fields_schema( $this->schema );
}

/**
* Returns the menu items added via the
* `customize_nav_menu_available_item_types` filter.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Menu custom items, or WP_Error if type could not be found.
*/
public function get_menu_custom_items( $request ) {
$requested_type = $request->get_param( 'type' );
$item_types = apply_filters( 'customize_nav_menu_available_item_types', array() );

if ( is_array( $item_types ) ) {
foreach ( $item_types as $item_type ) {
if ( $item_type['type'] === $requested_type ) {
return $this->prepare_item_for_response( $item_type, $request );
}
}
}

return new WP_Error( 'rest_invalid_menu_item_type', __( 'This item type could not be found.', 'gutenberg' ), array( 'status' => 404 ) );
}

/**
* Prepares a menu list of items for serialization.
*
* @param stdClass $item_type Item type data.
* @param WP_REST_Request $request Full details about the request.
*
* @return WP_REST_Response List of menu items.
*/
public function prepare_item_for_response( $item_type, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
return rest_ensure_response(
apply_filters( 'customize_nav_menu_available_items', array(), $item_type['type'], $item_type['object'], 0 )
);
}
}
2 changes: 1 addition & 1 deletion lib/class-wp-rest-menus-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ public function update_item( $request ) {

$prepared_term = $this->prepare_item_for_database( $request );

// Only update the term if we haz something to update.
Copy link
Contributor

Choose a reason for hiding this comment

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

😆

// Only update the term if we have something to update.
if ( ! empty( $prepared_term ) ) {
$update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) );

Expand Down
3 changes: 3 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ function gutenberg_is_experiment_enabled( $name ) {
if ( ! class_exists( 'WP_REST_Menus_Controller' ) ) {
require_once __DIR__ . '/class-wp-rest-menus-controller.php';
}
if ( ! class_exists( 'WP_REST_Menu_Custom_Items_Controller' ) ) {
require_once __DIR__ . '/class-wp-rest-menu-custom-items-controller.php';
}
if ( ! class_exists( 'WP_REST_Menu_Items_Controller' ) ) {
require_once __DIR__ . '/class-wp-rest-menu-items-controller.php';
}
Expand Down
9 changes: 9 additions & 0 deletions lib/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ function gutenberg_register_rest_customizer_nonces() {
}
add_action( 'rest_api_init', 'gutenberg_register_rest_customizer_nonces' );

/**
* Registers the custom menu items REST API route.
*/
function gutenberg_register_rest_menu_custom_items() {
$nav_menu_location = new WP_REST_Menu_Custom_Items_Controller();
$nav_menu_location->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_rest_menu_custom_items' );

/**
* Registers the Sidebars & Widgets REST API routes.
*/
Expand Down
7 changes: 7 additions & 0 deletions packages/block-editor/src/components/link-control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ Whether to present suggestions when typing the URL.

Whether to present initial suggestions immediately.

### noDirectEntry
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, that's unrelated to the other changes in that PR. It's a prop that was undocumented. I thought all props had to be in the docs, but happy to remove it from here if not.


- Type: `boolean`
- Required: No

Whether to allow turning a URL-like search query directly into a link.

### forceIsEditingLink

- Type: `boolean`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const LinkControlSearchInput = forwardRef(
renderSuggestions = ( props ) => (
<LinkControlSearchResults { ...props } />
),
fetchSuggestions = null,
allowDirectEntry = true,
showInitialSuggestions = false,
suggestionsQuery = {},
Expand All @@ -51,7 +50,7 @@ const LinkControlSearchInput = forwardRef(
withURLSuggestion
);
const searchHandler = showSuggestions
? fetchSuggestions || genericSearchHandler
? genericSearchHandler
: noopSearchHandler;

const instanceId = useInstanceId( LinkControlSearchInput );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const handleEntitySearch = async (
return isURLLike( val ) || ! withCreateSuggestion
? results
: results.concat( {
// the `id` prop is intentionally ommitted here because it
// the `id` prop is intentionally omitted here because it
// is never exposed as part of the component's public API.
// see: https://github.com/WordPress/gutenberg/pull/19775#discussion_r378931316.
title: val, // must match the existing `<input>`s text value
Expand Down
2 changes: 1 addition & 1 deletion packages/block-library/src/navigation-link/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function getSuggestionsQuery( type, kind ) {
if ( kind === 'post-type' ) {
return { type: 'post', subtype: type };
}
return {};
return { type };
}
}

Expand Down
19 changes: 18 additions & 1 deletion packages/block-library/src/navigation-link/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,28 @@ function register_block_core_navigation_link() {
}
}

// Some plugins use the `customize_nav_menu_available_item_types` filter to
// add new menu item types.
$item_types = apply_filters( 'customize_nav_menu_available_item_types', array() );
$filter_variations = array();
if ( is_array( $item_types ) ) {
foreach ( $item_types as $item_type ) {
$filter_variations[] = array(
'name' => $item_type['object'],
'title' => $item_type['title'],
'description' => '',
'attributes' => array(
'type' => $item_type['type'],
),
);
}
}

register_block_type_from_metadata(
__DIR__ . '/navigation-link',
array(
'render_callback' => 'render_block_core_navigation_link',
'variations' => array_merge( $built_ins, $variations ),
'variations' => array_merge( $built_ins, $variations, $filter_variations ),
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function render_block_core_post_navigation_link( $attributes, $content ) {
return '';
}

// Get the nagigation type to show the proper link. Available options are `next|previous`.
// Get the navigation type to show the proper link. Available options are `next|previous`.
$navigation_type = isset( $attributes['type'] ) ? $attributes['type'] : 'next';
// Allow only `next` and `previous` in `$navigation_type`.
if ( ! in_array( $navigation_type, array( 'next', 'previous' ), true ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,35 @@ const fetchLinkSuggestions = async (
);
}

if (
type &&
type !== 'post' &&
type !== 'term' &&
type !== 'post-format'
) {
queries.push(
apiFetch( {
path: addQueryArgs( '/__experimental/menu-custom-items', {
type,
} ),
} ).then( ( results ) => {
const customItems = results.filter(
( result ) =>
search === '' ||
result.title
.toLowerCase()
.includes( search.toLowerCase() )
);
return customItems.map( ( item ) => ( {
id: item.id,
title: item.title,
url: item.url,
type: 'URL',
} ) );
} )
);
}

return Promise.all( queries ).then( ( results ) => {
return results
.reduce(
Expand Down