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

Add: Ability to use creation patterns for other post types besides page #41791

Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<?php
Copy link
Member Author

Choose a reason for hiding this comment

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

Note to reviewers: this file is an exact copy of core with the following changes:

  • class name changed for the Gutenberg prefix.
  • post_types added to the description in public function get_item_schema().
  • 'postTypes' => 'post_types', added to $key in prepare_item_for_response.

There are no other changes to the core file.

/**
* REST API: Gutenberg_REST_Block_Patterns_Controller class
*
* @package Gutenberg
* @subpackage REST_API
*/

/**
* Core class used to access block patterns via the REST API.
*
* @since 6.0.0
*
* @see WP_REST_Controller
*/
class Gutenberg_REST_Block_Patterns_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.

We need to align with the existing controller in core. This means, I believe we need to move forwards this issue: #40902 and see what we'll need to update. Perhaps something like WP_Theme_JSON_6_1 extends WP_Theme_JSON_6_0 might be needed..

Noting that I didn't check the code here - just some thoughts..

Copy link
Member Author

Choose a reason for hiding this comment

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

Hi @ntsekouras In this PR I'm already aligining the endpoint with core (by accident) private $remote_patterns_loaded; is part of this PR. Basically, I copied this class from core, changed its name, and did the changes I needed. I'm using WordPress filters to overwrite the core class with this new Gutenberg one Following this approach we don't need any extends etc. We are free to apply the changes we need. When backporting the class to core we just copy this file back to the core, change its name, and that's it. I left a comment on @anton-vlasenko PR #40902 (review).


/**
* Defines whether remote patterns should be loaded.
*
* @since 6.0.0
* @var bool
*/
private $remote_patterns_loaded;

/**
* Constructs the controller.
*
* @since 6.0.0
*/
public function __construct() {
$this->namespace = 'wp/v2';
$this->rest_base = 'block-patterns/patterns';
}

/**
* Registers the routes for the objects of the controller.
*
* @since 6.0.0
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
),
true
);
}

/**
* Checks whether a given request has permission to read block patterns.
*
* @since 6.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) {
if ( current_user_can( 'edit_posts' ) ) {
return true;
}

foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
return true;
}
}

return new WP_Error(
'rest_cannot_view',
__( 'Sorry, you are not allowed to view the registered block patterns.' ),
array( 'status' => rest_authorization_required_code() )
);
}

/**
* Retrieves all block patterns.
*
* @since 6.0.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {
if ( ! $this->remote_patterns_loaded ) {
// Load block patterns from w.org.
_load_remote_block_patterns(); // Patterns with the `core` keyword.
_load_remote_featured_patterns(); // Patterns in the `featured` category.
_register_remote_theme_patterns(); // Patterns requested by current theme.

$this->remote_patterns_loaded = true;
}

$response = array();
$patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
foreach ( $patterns as $pattern ) {
$prepared_pattern = $this->prepare_item_for_response( $pattern, $request );
$response[] = $this->prepare_response_for_collection( $prepared_pattern );
}
return rest_ensure_response( $response );
}

/**
* Prepare a raw block pattern before it gets output in a REST API response.
*
* @since 6.0.0
*
* @param array $item Raw pattern as registered, before any changes.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function prepare_item_for_response( $item, $request ) {
$fields = $this->get_fields_for_response( $request );
$keys = array(
'name' => 'name',
'title' => 'title',
'description' => 'description',
'viewportWidth' => 'viewport_width',
'blockTypes' => 'block_types',
'postTypes' => 'post_types',
'categories' => 'categories',
'keywords' => 'keywords',
'content' => 'content',
'inserter' => 'inserter',
);
$data = array();
foreach ( $keys as $item_key => $rest_key ) {
if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) {
$data[ $rest_key ] = $item[ $item_key ];
}
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
return rest_ensure_response( $data );
}

/**
* Retrieves the block pattern schema, conforming to JSON Schema.
*
* @since 6.0.0
*
* @return array Item schema data.
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'block-pattern',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'The pattern name.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'title' => array(
'description' => __( 'The pattern title, in human readable format.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'description' => array(
'description' => __( 'The pattern detailed description.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'viewport_width' => array(
'description' => __( 'The pattern viewport width for inserter preview.' ),
'type' => 'number',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'block_types' => array(
'description' => __( 'Block types that the pattern is intended to be used with.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'post_types' => array(
'description' => __( 'Post types where the pattern is intended to be used as the starting content.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure what the best way to track this is, but we will also need to update the documentation of Core's register_block_pattern:

https://github.com/WordPress/wordpress-develop/blob/e7cafe6141631bc8125c3173404edab09c868961/src/wp-includes/class-wp-block-patterns-registry.php#L47-L72

Copy link
Member Author

Choose a reason for hiding this comment

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

I added a PR against core here WordPress/wordpress-develop#2858 so we don't miss the required change in core.

'categories' => array(
'description' => __( 'The pattern category slugs.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'keywords' => array(
'description' => __( 'The pattern keywords.' ),
'type' => 'array',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'content' => array(
'description' => __( 'The pattern content.' ),
'type' => 'string',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
'inserter' => array(
'description' => __( 'Determines whether the pattern is visible in inserter.' ),
'type' => 'boolean',
'readonly' => true,
'context' => array( 'view', 'edit', 'embed' ),
),
),
);

return $this->add_additional_fields_schema( $schema );
}
}
16 changes: 5 additions & 11 deletions lib/compat/wordpress-6.1/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,11 @@ function gutenberg_update_templates_template_parts_rest_controller( $args, $post
}
add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 );


/**
* Add the post type's `icon`(menu_icon) in the response.
* When we backport this change we will need to add the
* `icon` to WP_REST_Post_Types_Controller schema.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post_Type $post_type The original post type object.
* Registers the block patterns REST API routes.
*/
function gutenberg_update_post_types_rest_response( $response, $post_type ) {
$response->data['icon'] = $post_type->menu_icon;
return $response;
function gutenberg_register_gutenberg_rest_block_patterns() {
$block_patterns = new Gutenberg_REST_Block_Patterns_Controller();
$block_patterns->register_routes();
}
add_filter( 'rest_prepare_post_type', 'gutenberg_update_post_types_rest_response', 10, 2 );
add_action( 'rest_api_init', 'gutenberg_register_gutenberg_rest_block_patterns', 100 );
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/compat/wordpress-6.0/rest-api.php';

// WordPress 6.1 compat.
require_once __DIR__ . '/compat/wordpress-6.1/class-gutenberg-rest-block-patterns-controller.php';
require_once __DIR__ . '/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php';
require_once __DIR__ . '/compat/wordpress-6.1/rest-api.php';

Expand Down
2 changes: 2 additions & 0 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,8 @@ export const getBlockPatterns =
switch ( key ) {
case 'block_types':
return 'blockTypes';
case 'post_types':
Copy link
Member

Choose a reason for hiding this comment

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

Is there any documentation to update around this? cc @juanmaguitar

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we generalise the casing transformation?

// hello_world -> helloWorld
function snakeToCamel( string ) {
  return string.replace( /_([a-z])/, ( _, letter ) => letter.toUpperCase() );
}

mapKeys( pattern, ( _, phpKey ) => snakeToCamel( phpKey ) );

If not (why not?), then it would be nice to eventually be able to source the mapping from Core rather than keep a copy here.

Copy link
Member Author

Choose a reason for hiding this comment

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

I applied the feedback, using a function from lodash that is already used in other parts of the codebase.

return 'postTypes';
case 'viewport_width':
return 'viewportWidth';
default:
Expand Down
44 changes: 28 additions & 16 deletions packages/edit-post/src/components/start-page-options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import { Modal } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { useState, useEffect, useMemo } from '@wordpress/element';
import {
store as blockEditorStore,
__experimentalBlockPatternsList as BlockPatternsList,
Expand All @@ -17,15 +17,30 @@ import { store as editorStore } from '@wordpress/editor';
*/
import { store as editPostStore } from '../../store';

function PatternSelection( { onChoosePattern } ) {
const { blockPatterns } = useSelect( ( select ) => {
function useStartPatterns() {
const { blockPatterns, postType } = useSelect( ( select ) => {
const { __experimentalGetPatternsByBlockTypes } =
select( blockEditorStore );
const { getCurrentPostType } = select( editorStore );
return {
blockPatterns:
__experimentalGetPatternsByBlockTypes( 'core/post-content' ),
postType: getCurrentPostType(),
};
}, [] );
return useMemo( () => {
return blockPatterns.filter( ( pattern ) => {
return (
( postType === 'page' && ! pattern.postTypes ) ||
Copy link
Member

Choose a reason for hiding this comment

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

Can we include some comments here? The logic and its implications are worth stating since it's not obvious on its own.

Copy link
Member Author

Choose a reason for hiding this comment

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

I included some inline comments.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I'm a bit late. But what do you think if we make filtering by post type part of REST API request? There's no need to send a full response if we filter out results on the client-side.

P.S. We're already doing something similar for the templates - https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php#L747-L750

( Array.isArray( pattern.postTypes ) &&
pattern.postTypes.includes( postType ) )
);
} );
}, [ postType, blockPatterns ] );
}

function PatternSelection( { onChoosePattern } ) {
const blockPatterns = useStartPatterns();
const shownBlockPatterns = useAsyncList( blockPatterns );
const { resetEditorBlocks } = useDispatch( editorStore );
return (
Expand All @@ -50,31 +65,28 @@ export default function StartPageOptions() {
const [ modalState, setModalState ] = useState(
START_PAGE_MODAL_STATES.INITIAL
);
const blockPatterns = useStartPatterns();
const hasStartPattern = blockPatterns.length > 0;
const shouldOpenModel = useSelect(
( select ) => {
if ( modalState !== START_PAGE_MODAL_STATES.INITIAL ) {
if (
! hasStartPattern ||
modalState !== START_PAGE_MODAL_STATES.INITIAL
) {
return false;
}
const { __experimentalGetPatternsByBlockTypes } =
select( blockEditorStore );
const {
getCurrentPostType,
getEditedPostContent,
isEditedPostSaveable,
} = select( editorStore );
const { getEditedPostContent, isEditedPostSaveable } =
select( editorStore );
const { isEditingTemplate, isFeatureActive } =
select( editPostStore );
return (
getCurrentPostType() === 'page' &&
! isEditedPostSaveable() &&
'' === getEditedPostContent() &&
! isEditingTemplate() &&
! isFeatureActive( 'welcomeGuide' ) &&
__experimentalGetPatternsByBlockTypes( 'core/post-content' )
.length >= 1
! isFeatureActive( 'welcomeGuide' )
);
},
[ modalState ]
[ modalState, hasStartPattern ]
);

useEffect( () => {
Expand Down