Skip to content

Commit cd0003c

Browse files
authored
Navigation: Try adding navigation link variants via server (#29095)
The navigation block allows insertion of Links, Post Links, Page Links, Tag Links and Category Links. Changes here add the ability to insert links for custom post types and taxonomies. If we install a Portfolio plugin for example, we should be able to add Portfolio Links. In trunk, the list of link variations is hardcoded. Changes here include registering navigation link variants using register_block_type_from_metadata and passing not only a render callback, but the variations array. One benefit of doing this is being able to fetch post types and taxonomies and filter by show_in_nav_menus which is not exposed via the REST API. It also has some flexibility in pulling other data from the server without needing to bloat the API response (values here are unlikely to be reused elsewhere).
1 parent 7ba0075 commit cd0003c

File tree

7 files changed

+240
-14
lines changed

7 files changed

+240
-14
lines changed

packages/block-library/src/navigation-link/block.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
},
2929
"title": {
3030
"type": "string"
31+
},
32+
"kind": {
33+
"type": "string"
3134
}
3235
},
3336
"usesContext": [

packages/block-library/src/navigation-link/edit.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@ const useIsDraggingWithin = ( elementRef ) => {
103103
* /wp/v2/search.
104104
*
105105
* @param {string} type Link block's type attribute.
106+
* @param {string} kind Link block's entity of kind (post-type|taxonomy)
106107
* @return {{ type?: string, subtype?: string }} Search query params.
107108
*/
108-
function getSuggestionsQuery( type ) {
109+
function getSuggestionsQuery( type, kind ) {
109110
switch ( type ) {
110111
case 'post':
111112
case 'page':
@@ -115,6 +116,12 @@ function getSuggestionsQuery( type ) {
115116
case 'tag':
116117
return { type: 'term', subtype: 'post_tag' };
117118
default:
119+
if ( kind === 'taxonomy' ) {
120+
return { type: 'term', subtype: type };
121+
}
122+
if ( kind === 'post-type' ) {
123+
return { type: 'post', subtype: type };
124+
}
118125
return {};
119126
}
120127
}
@@ -137,6 +144,7 @@ export default function NavigationLinkEdit( {
137144
description,
138145
rel,
139146
title,
147+
kind,
140148
} = attributes;
141149
const link = {
142150
url,
@@ -501,7 +509,10 @@ export default function NavigationLinkEdit( {
501509
} }
502510
noDirectEntry={ !! type }
503511
noURLSuggestion={ !! type }
504-
suggestionsQuery={ getSuggestionsQuery( type ) }
512+
suggestionsQuery={ getSuggestionsQuery(
513+
type,
514+
kind
515+
) }
505516
onChange={ ( {
506517
title: newTitle = '',
507518
url: newURL = '',

packages/block-library/src/navigation-link/variations.js renamed to packages/block-library/src/navigation-link/fallback-variations.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import {
88
postTitle as postIcon,
99
tag as tagIcon,
1010
} from '@wordpress/icons';
11-
const variations = [
11+
12+
// FALLBACK: this is only used when the server does not understand the variations property in the
13+
// register_block_type_from_metadata call. see navigation-link/index.php.
14+
// Delete this file when supported WP ranges understand the `variations` property when passed to
15+
// register_block_type_from_metadata in index.php
16+
const fallbackVariations = [
1217
{
1318
name: 'link',
1419
isDefault: true,
@@ -51,10 +56,10 @@ const variations = [
5156
* `isActive` function is used to find a variation match from a created
5257
* Block by providing its attributes.
5358
*/
54-
variations.forEach( ( variation ) => {
59+
fallbackVariations.forEach( ( variation ) => {
5560
if ( variation.isActive ) return;
5661
variation.isActive = ( blockAttributes, variationAttributes ) =>
5762
blockAttributes.type === variationAttributes.type;
5863
} );
5964

60-
export default variations;
65+
export default fallbackVariations;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { addFilter } from '@wordpress/hooks';
5+
import {
6+
category,
7+
page,
8+
postTitle,
9+
tag,
10+
customPostType,
11+
} from '@wordpress/icons';
12+
13+
/**
14+
* Internal dependencies
15+
*/
16+
import fallbackVariations from './fallback-variations';
17+
18+
function getIcon( variationName ) {
19+
switch ( variationName ) {
20+
case 'post':
21+
return postTitle;
22+
case 'page':
23+
return page;
24+
case 'tag':
25+
return tag;
26+
case 'category':
27+
return category;
28+
default:
29+
return customPostType;
30+
}
31+
}
32+
33+
function enhanceNavigationLinkVariations( settings, name ) {
34+
if ( name !== 'core/navigation-link' ) {
35+
return settings;
36+
}
37+
38+
// Fallback handling may be deleted after supported WP ranges understand the `variations`
39+
// property when passed to register_block_type_from_metadata in index.php
40+
if ( ! settings.variations ) {
41+
return {
42+
...settings,
43+
variations: fallbackVariations,
44+
};
45+
}
46+
47+
// Otherwise decorate server passed variations with an icon and isActive function
48+
if ( settings.variations ) {
49+
const isActive = ( blockAttributes, variationAttributes ) => {
50+
return blockAttributes.type === variationAttributes.type;
51+
};
52+
const variations = settings.variations.map( ( variation ) => {
53+
return {
54+
...variation,
55+
...( ! variation.icon && {
56+
icon: getIcon( variation.name ),
57+
} ),
58+
...( ! variation.isActive && {
59+
isActive,
60+
} ),
61+
};
62+
} );
63+
return {
64+
...settings,
65+
variations,
66+
};
67+
}
68+
return settings;
69+
}
70+
71+
addFilter(
72+
'blocks.registerBlockType',
73+
'core/navigation-link',
74+
enhanceNavigationLinkVariations
75+
);

packages/block-library/src/navigation-link/index.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { InnerBlocks } from '@wordpress/block-editor';
1111
import metadata from './block.json';
1212
import edit from './edit';
1313
import save from './save';
14-
import variations from './variations';
14+
import './hooks';
1515

1616
const { name } = metadata;
1717

@@ -24,8 +24,6 @@ export const settings = {
2424

2525
description: __( 'Add a page, link, or another item to your navigation.' ),
2626

27-
variations,
28-
2927
__experimentalLabel: ( { label } ) => label,
3028

3129
merge( leftAttributes, { label: rightLabel = '' } ) {
@@ -39,6 +37,13 @@ export const settings = {
3937

4038
save,
4139

40+
example: {
41+
attributes: {
42+
label: _x( 'Example Link', 'navigation link preview example' ),
43+
url: 'https://example.com',
44+
},
45+
},
46+
4247
deprecated: [
4348
{
4449
isEligible( attributes ) {

packages/block-library/src/navigation-link/index.php

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,12 @@ function block_core_navigation_link_render_submenu_icon() {
104104
* @return string Returns the post content with the legacy widget added.
105105
*/
106106
function render_block_core_navigation_link( $attributes, $content, $block ) {
107+
$navigation_link_has_id = isset( $attributes['id'] ) && is_numeric( $attributes['id'] );
108+
$is_post_type = isset( $attributes['kind'] ) && 'post-type' === $attributes['kind'];
109+
$is_post_type = $is_post_type || isset( $attributes['type'] ) && ( 'post' === $attributes['type'] || 'page' === $attributes['type'] );
110+
107111
// Don't render the block's subtree if it is a draft.
108-
if (
109-
isset( $attributes['id'] ) &&
110-
is_numeric( $attributes['id'] ) &&
111-
isset( $attributes['type'] ) &&
112-
( 'post' === $attributes['type'] || 'page' === $attributes['type'] )
113-
) {
112+
if ( $is_post_type && $navigation_link_has_id ) {
114113
$post = get_post( $attributes['id'] );
115114
if ( 'publish' !== $post->post_status ) {
116115
return '';
@@ -225,17 +224,77 @@ function render_block_core_navigation_link( $attributes, $content, $block ) {
225224
return $html;
226225
}
227226

227+
/**
228+
* Returns a navigation link variation
229+
*
230+
* @param WP_Taxonomy|WP_Post_Type $entity post type or taxonomy entity.
231+
* @param string $kind string of value 'taxonomy' or 'post-type'.
232+
*
233+
* @return array
234+
*/
235+
function build_variation_for_navigation_link( $entity, $kind ) {
236+
$name = 'post_tag' === $entity->name ? 'tag' : $entity->name;
237+
238+
$title = '';
239+
$description = '';
240+
241+
if ( property_exists( $entity->labels, 'item_link' ) ) {
242+
$title = $entity->labels->item_link;
243+
}
244+
if ( property_exists( $entity->labels, 'item_link_description' ) ) {
245+
$description = $entity->labels->item_link_description;
246+
}
247+
248+
return array(
249+
'name' => $name,
250+
'title' => $title,
251+
'description' => $description,
252+
'attributes' => array(
253+
'type' => $name,
254+
'kind' => $kind,
255+
),
256+
);
257+
}
258+
228259
/**
229260
* Register the navigation link block.
230261
*
231262
* @uses render_block_core_navigation()
232263
* @throws WP_Error An WP_Error exception parsing the block definition.
233264
*/
234265
function register_block_core_navigation_link() {
266+
267+
$post_types = get_post_types( array( 'show_in_nav_menus' => true ), 'objects' );
268+
$taxonomies = get_taxonomies( array( 'show_in_nav_menus' => true ), 'objects' );
269+
$built_ins = array();
270+
$variations = array();
271+
272+
if ( $post_types ) {
273+
foreach ( $post_types as $post_type ) {
274+
$variation = build_variation_for_navigation_link( $post_type, 'post-type' );
275+
if ( 'post' === $variation['name'] || 'page' === $variation['name'] ) {
276+
$built_ins[] = $variation;
277+
} else {
278+
$variations[] = $variation;
279+
}
280+
}
281+
}
282+
if ( $taxonomies ) {
283+
foreach ( $taxonomies as $taxonomy ) {
284+
$variation = build_variation_for_navigation_link( $taxonomy, 'taxonomy' );
285+
if ( 'category' === $variation['name'] || 'tag' === $variation['name'] ) {
286+
$built_ins[] = $variation;
287+
} else {
288+
$variations[] = $variation;
289+
}
290+
}
291+
}
292+
235293
register_block_type_from_metadata(
236294
__DIR__ . '/navigation-link',
237295
array(
238296
'render_callback' => 'render_block_core_navigation_link',
297+
'variations' => array_merge( $built_ins, $variations ),
239298
)
240299
);
241300
}

phpunit/class-block-library-navigation-link-test.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class Block_Library_Navigation_Link_Test extends WP_UnitTestCase {
1313
private static $category;
1414
private static $page;
1515
private static $draft;
16+
private static $custom_draft;
17+
private static $custom_post;
1618

1719
private static $pages;
1820
private static $terms;
@@ -31,6 +33,30 @@ public static function wpSetUpBeforeClass() {
3133
);
3234
self::$pages[] = self::$draft;
3335

36+
self::$custom_draft = self::factory()->post->create_and_get(
37+
array(
38+
'post_type' => 'cats',
39+
'post_status' => 'draft',
40+
'post_name' => 'metalcat',
41+
'post_title' => 'Metal Cat',
42+
'post_content' => 'Metal Cat content',
43+
'post_excerpt' => 'Metal Cat',
44+
)
45+
);
46+
self::$pages[] = self::$custom_draft;
47+
48+
self::$custom_post = self::factory()->post->create_and_get(
49+
array(
50+
'post_type' => 'dogs',
51+
'post_status' => 'publish',
52+
'post_name' => 'metaldog',
53+
'post_title' => 'Metal Dog',
54+
'post_content' => 'Metal Dog content',
55+
'post_excerpt' => 'Metal Dog',
56+
)
57+
);
58+
self::$pages[] = self::$custom_post;
59+
3460
self::$page = self::factory()->post->create_and_get(
3561
array(
3662
'post_type' => 'page',
@@ -166,4 +192,46 @@ function test_returns_link_for_plain_link() {
166192
) !== false
167193
);
168194
}
195+
196+
function test_returns_empty_when_custom_post_type_draft() {
197+
$page_id = self::$custom_draft->ID;
198+
199+
$parsed_blocks = parse_blocks(
200+
"<!-- wp:navigation-link {\"label\":\"Draft Custom Post Type\",\"type\":\"cats\",\"kind\":\"post-type\",\"id\":{$page_id},\"url\":\"http://localhost:8888/?page_id={$page_id}\"} /-->"
201+
);
202+
$this->assertEquals( 1, count( $parsed_blocks ) );
203+
204+
$navigation_link_block = new WP_Block( $parsed_blocks[0], array() );
205+
206+
$this->assertEquals(
207+
'',
208+
gutenberg_render_block_core_navigation_link(
209+
$navigation_link_block->attributes,
210+
array(),
211+
$navigation_link_block
212+
)
213+
);
214+
}
215+
216+
function test_returns_link_when_custom_post_is_published() {
217+
$page_id = self::$custom_post->ID;
218+
219+
$parsed_blocks = parse_blocks(
220+
"<!-- wp:navigation-link {\"label\":\"Metal Dogs\",\"type\":\"dogs\",\"kind\":\"post-type\",\"id\":{$page_id},\"url\":\"http://localhost:8888/?page_id={$page_id}\"} /-->"
221+
);
222+
$this->assertEquals( 1, count( $parsed_blocks ) );
223+
224+
$navigation_link_block = new WP_Block( $parsed_blocks[0], array() );
225+
$this->assertEquals(
226+
true,
227+
strpos(
228+
gutenberg_render_block_core_navigation_link(
229+
$navigation_link_block->attributes,
230+
array(),
231+
$navigation_link_block
232+
),
233+
'Metal Dogs'
234+
) !== false
235+
);
236+
}
169237
}

0 commit comments

Comments
 (0)