From b3436039f2b07fcc1110d77c9a5c76ff6c1cfadf Mon Sep 17 00:00:00 2001 From: Tonya Mork Date: Tue, 20 Sep 2022 21:19:10 +0000 Subject: [PATCH] Editor: Adds template types, `is_wp_suggestion`, and fallback template content. This commit improves site editor templates by: * Adds a post meta `is_wp_suggestion` to templates created from the site editor. Why? To differentiate the templates created from the post editor in the Template panel in inspector controls and the templates suggested in site editor. See [https://github.com/WordPress/gutenberg/pull/41387 Gutenberg PR 41387] for more details. * Expands the template types that can be added to the site editor to include single custom post type and specific posts templates. See [https://github.com/WordPress/gutenberg/pull/41189 Gutenberg PR 41189] for more details. * Adds fallback template content on creation in site editor: * Introduces `get_template_hierarchy()` to get the template hierarchy for a given template slug to be created. * Adds a `lookup` route to `WP_REST_Templates_Controller` to get the fallback template content. See [https://github.com/WordPress/gutenberg/pull/42520 Gutenberg PR 42520] for more details. * Fixes a typo in default category template's description within `get_default_block_template_types()`. See [https://github.com/WordPress/gutenberg/pull/42586 Gutenberg PR 42586] for more details. * Changes field checks from `in_array()` to `rest_is_field_included()` in `WP_REST_Post_Types_Controller`. * Adds an `icon` field to `WP_REST_Post_Types_Controller` Follow-up to [53129], [52331], [52275], [52062], [51962], [43087]. Props ntsekouras, spacedmonkey, mamaduka, mburridge, jameskoster, bernhard-reiter, mcsf, hellofromTonya. See #56467. git-svn-id: https://develop.svn.wordpress.org/trunk@54269 602fd350-edb4-49c9-b593-d223f7449a82 --- phpcs.xml.dist | 1 + src/wp-includes/block-template-utils.php | 86 +++++++++++- .../class-wp-rest-post-types-controller.php | 35 +++-- .../class-wp-rest-templates-controller.php | 53 ++++++++ tests/phpunit/tests/block-templates/base.php | 86 ++++++++++++ .../block-templates/getTemplateHierarchy.php | 124 ++++++++++++++++++ .../rest-api/rest-post-types-controller.php | 33 +++-- .../tests/rest-api/rest-schema-setup.php | 2 + .../rest-api/wpRestTemplatesController.php | 111 ++++++++++++++++ tests/qunit/fixtures/wp-api-generated.js | 83 ++++++++++++ 10 files changed, 583 insertions(+), 31 deletions(-) create mode 100644 tests/phpunit/tests/block-templates/base.php create mode 100644 tests/phpunit/tests/block-templates/getTemplateHierarchy.php diff --git a/phpcs.xml.dist b/phpcs.xml.dist index db1a76d17e721..32c6acc06d8b1 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -237,6 +237,7 @@ + diff --git a/src/wp-includes/block-template-utils.php b/src/wp-includes/block-template-utils.php index 54413d809bc78..a16fe2914c821 100644 --- a/src/wp-includes/block-template-utils.php +++ b/src/wp-includes/block-template-utils.php @@ -147,7 +147,7 @@ function get_default_block_template_types() { ), 'category' => array( 'title' => _x( 'Category', 'Template name' ), - 'description' => __( 'Displays latest posts in single post category.' ), + 'description' => __( 'Displays latest posts from a single post category.' ), ), 'taxonomy' => array( 'title' => _x( 'Taxonomy', 'Template name' ), @@ -555,7 +555,8 @@ function _build_block_template_result_from_post( $post ) { $template_file = _get_block_template_file( $post->post_type, $post->post_name ); $has_theme_file = wp_get_theme()->get_stylesheet() === $theme && null !== $template_file; - $origin = get_post_meta( $post->ID, 'origin', true ); + $origin = get_post_meta( $post->ID, 'origin', true ); + $is_wp_suggestion = get_post_meta( $post->ID, 'is_wp_suggestion', true ); $template = new WP_Block_Template(); $template->wp_id = $post->ID; @@ -570,7 +571,7 @@ function _build_block_template_result_from_post( $post ) { $template->title = $post->post_title; $template->status = $post->post_status; $template->has_theme_file = $has_theme_file; - $template->is_custom = true; + $template->is_custom = empty( $is_wp_suggestion ); $template->author = $post->post_author; if ( 'wp_template' === $post->post_type && $has_theme_file && isset( $template_file['postTypes'] ) ) { @@ -679,7 +680,8 @@ function get_block_templates( $query = array(), $template_type = 'wp_template' ) continue; } - if ( $post_type && + if ( + $post_type && isset( $template->post_types ) && ! in_array( $post_type, $template->post_types, true ) ) { @@ -912,9 +914,10 @@ function block_footer_area() { * @return Bool Whether this file is in an ignored directory. */ function wp_is_theme_directory_ignored( $path ) { - $directories_to_ignore = array( '.svn', '.git', '.hg', '.bzr', 'node_modules', 'vendor' ); + $directories_to_ignore = array( '.DS_Store', '.svn', '.git', '.hg', '.bzr', 'node_modules', 'vendor' ); + foreach ( $directories_to_ignore as $directory ) { - if ( strpos( $path, $directory ) === 0 ) { + if ( str_starts_with( $path, $directory ) ) { return true; } } @@ -1023,3 +1026,74 @@ function wp_generate_block_templates_export_file() { return $filename; } + +/** + * Gets the template hierarchy for the given template slug to be created. + * + * + * Note: Always add `index` as the last fallback template. + * + * @since 6.1.0 + * + * @param string $slug The template slug to be created. + * @param boolean $is_custom Optional. Indicates if a template is custom or + * part of the template hierarchy. Default false. + * @param string $template_prefix Optional. The template prefix for the created template. + * Used to extract the main template type, e.g. + * in `taxonomy-books` the `taxonomy` is extracted. + * Default empty string. + * @return string[] The template hierarchy. + */ +function get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '' ) { + if ( 'index' === $slug ) { + return array( 'index' ); + } + if ( $is_custom ) { + return array( 'page', 'singular', 'index' ); + } + if ( 'front-page' === $slug ) { + return array( 'front-page', 'home', 'index' ); + } + + $template_hierarchy = array( $slug ); + + // Most default templates don't have `$template_prefix` assigned. + if ( $template_prefix ) { + list( $type ) = explode( '-', $template_prefix ); + // These checks are needed because the `$slug` above is always added. + if ( ! in_array( $template_prefix, array( $slug, $type ), true ) ) { + $template_hierarchy[] = $template_prefix; + } + if ( $slug !== $type ) { + $template_hierarchy[] = $type; + } + } + + // Handle `archive` template. + if ( + str_starts_with( $slug, 'author' ) || + str_starts_with( $slug, 'taxonomy' ) || + str_starts_with( $slug, 'category' ) || + str_starts_with( $slug, 'tag' ) || + 'date' === $slug + ) { + $template_hierarchy[] = 'archive'; + } + // Handle `single` template. + if ( 'attachment' === $slug ) { + $template_hierarchy[] = 'single'; + } + + // Handle `singular` template. + if ( + str_starts_with( $slug, 'single' ) || + str_starts_with( $slug, 'page' ) || + 'attachment' === $slug + ) { + $template_hierarchy[] = 'singular'; + } + + $template_hierarchy[] = 'index'; + + return $template_hierarchy; +}; diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php index cb2daa4b292c6..8223f1dd6f38e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php @@ -186,54 +186,58 @@ public function prepare_item_for_response( $item, $request ) { $fields = $this->get_fields_for_response( $request ); $data = array(); - if ( in_array( 'capabilities', $fields, true ) ) { + if ( rest_is_field_included( 'capabilities', $fields ) ) { $data['capabilities'] = $post_type->cap; } - if ( in_array( 'description', $fields, true ) ) { + if ( rest_is_field_included( 'description', $fields ) ) { $data['description'] = $post_type->description; } - if ( in_array( 'hierarchical', $fields, true ) ) { + if ( rest_is_field_included( 'hierarchical', $fields ) ) { $data['hierarchical'] = $post_type->hierarchical; } - if ( in_array( 'visibility', $fields, true ) ) { + if ( rest_is_field_included( 'visibility', $fields ) ) { $data['visibility'] = array( 'show_in_nav_menus' => (bool) $post_type->show_in_nav_menus, 'show_ui' => (bool) $post_type->show_ui, ); } - if ( in_array( 'viewable', $fields, true ) ) { + if ( rest_is_field_included( 'viewable', $fields ) ) { $data['viewable'] = is_post_type_viewable( $post_type ); } - if ( in_array( 'labels', $fields, true ) ) { + if ( rest_is_field_included( 'labels', $fields ) ) { $data['labels'] = $post_type->labels; } - if ( in_array( 'name', $fields, true ) ) { + if ( rest_is_field_included( 'name', $fields ) ) { $data['name'] = $post_type->label; } - if ( in_array( 'slug', $fields, true ) ) { + if ( rest_is_field_included( 'slug', $fields ) ) { $data['slug'] = $post_type->name; } - if ( in_array( 'supports', $fields, true ) ) { + if ( rest_is_field_included( 'icon', $fields ) ) { + $data['icon'] = $post_type->menu_icon; + } + + if ( rest_is_field_included( 'supports', $fields ) ) { $data['supports'] = $supports; } - if ( in_array( 'taxonomies', $fields, true ) ) { + if ( rest_is_field_included( 'taxonomies', $fields ) ) { $data['taxonomies'] = array_values( $taxonomies ); } - if ( in_array( 'rest_base', $fields, true ) ) { + if ( rest_is_field_included( 'rest_base', $fields ) ) { $data['rest_base'] = $base; } - if ( in_array( 'rest_namespace', $fields, true ) ) { + if ( rest_is_field_included( 'rest_namespace', $fields ) ) { $data['rest_namespace'] = $namespace; } @@ -287,6 +291,7 @@ protected function prepare_links( $post_type ) { * @since 4.7.0 * @since 4.8.0 The `supports` property was added. * @since 5.9.0 The `visibility` and `rest_namespace` properties were added. + * @since 6.1.0 The `icon` property was added. * * @return array Item schema data. */ @@ -385,6 +390,12 @@ public function get_item_schema() { ), ), ), + 'icon' => array( + 'description' => __( 'The icon for the post type.' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), ), ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php index 7e60674890646..e26b4d17e5ebe 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-templates-controller.php @@ -42,6 +42,7 @@ public function __construct( $post_type ) { * Registers the controllers routes. * * @since 5.8.0 + * @since 6.1.0 Endpoint for fallback template content. */ public function register_routes() { // Lists all templates. @@ -65,6 +66,34 @@ public function register_routes() { ) ); + // Get fallback template content. + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/lookup', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_template_fallback' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'slug' => array( + 'description' => __( 'The slug of the template to get the fallback for' ), + 'type' => 'string', + 'required' => true, + ), + 'is_custom' => array( + 'description' => __( ' Indicates if a template is custom or part of the template hierarchy' ), + 'type' => 'boolean', + ), + 'template_prefix' => array( + 'description' => __( 'The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`' ), + 'type' => 'string', + ), + ), + ), + ) + ); + // Lists/updates a single template based on the given id. register_rest_route( $this->namespace, @@ -117,6 +146,21 @@ public function register_routes() { ); } + /** + * Returns the fallback template for the given slug. + * + * @since 6.1.0 + * + * @param WP_REST_Request $request The request instance. + * @return WP_REST_Response|WP_Error + */ + public function get_template_fallback( $request ) { + $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); + $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); + $response = $this->prepare_item_for_response( $fallback_template, $request ); + return rest_ensure_response( $response ); + } + /** * Checks if the user has permissions to make the request. * @@ -525,6 +569,15 @@ protected function prepare_item_for_database( $request ) { $changes->post_excerpt = $template->description; } + if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) { + $changes->meta_input = wp_parse_args( + array( + 'is_wp_suggestion' => $request['is_wp_suggestion'], + ), + $changes->meta_input = array() + ); + } + if ( 'wp_template_part' === $this->post_type ) { if ( isset( $request['area'] ) ) { $changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] ); diff --git a/tests/phpunit/tests/block-templates/base.php b/tests/phpunit/tests/block-templates/base.php new file mode 100644 index 0000000000000..f6a5a9f4dfcec --- /dev/null +++ b/tests/phpunit/tests/block-templates/base.php @@ -0,0 +1,86 @@ +post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + 'this-theme-should-not-resolve', + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, 'this-theme-should-not-resolve', 'wp_theme' ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Set up template part post. + self::$template_part_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template_part', + 'post_name' => 'my_template_part', + 'post_title' => 'My Template Part', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template part', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); + } + + public static function wpTearDownAfterClass() { + wp_delete_post( self::$template_post->ID ); + wp_delete_post( self::$template_part_post->ID ); + } + + public function set_up() { + parent::set_up(); + switch_theme( self::TEST_THEME ); + } +} diff --git a/tests/phpunit/tests/block-templates/getTemplateHierarchy.php b/tests/phpunit/tests/block-templates/getTemplateHierarchy.php new file mode 100644 index 0000000000000..e550b22778417 --- /dev/null +++ b/tests/phpunit/tests/block-templates/getTemplateHierarchy.php @@ -0,0 +1,124 @@ +assertSame( $expected, get_template_hierarchy( ...$args ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_template_hierarchy() { + return array( + 'front-page' => array( + 'args' => array( 'front-page' ), + 'expected' => array( 'front-page', 'home', 'index' ), + ), + 'custom template' => array( + 'args' => array( 'whatever-slug', true ), + 'expected' => array( 'page', 'singular', 'index' ), + ), + 'page' => array( + 'args' => array( 'page' ), + 'expected' => array( 'page', 'singular', 'index' ), + ), + 'tag' => array( + 'args' => array( 'tag' ), + 'expected' => array( 'tag', 'archive', 'index' ), + ), + 'author' => array( + 'args' => array( 'author' ), + 'expected' => array( 'author', 'archive', 'index' ), + ), + 'date' => array( + 'args' => array( 'date' ), + 'expected' => array( 'date', 'archive', 'index' ), + ), + 'taxonomy' => array( + 'args' => array( 'taxonomy' ), + 'expected' => array( 'taxonomy', 'archive', 'index' ), + ), + 'attachment' => array( + 'args' => array( 'attachment' ), + 'expected' => array( 'attachment', 'single', 'singular', 'index' ), + ), + 'singular' => array( + 'args' => array( 'singular' ), + 'expected' => array( 'singular', 'index' ), + ), + 'single' => array( + 'args' => array( 'single' ), + 'expected' => array( 'single', 'singular', 'index' ), + ), + 'archive' => array( + 'args' => array( 'archive' ), + 'expected' => array( 'archive', 'index' ), + ), + 'index' => array( + 'args' => array( 'index' ), + 'expected' => array( 'index' ), + ), + 'specific taxonomies' => array( + 'args' => array( 'taxonomy-books', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word categories' => array( + 'args' => array( 'category-fruits', false, 'category' ), + 'expected' => array( 'category-fruits', 'category', 'archive', 'index' ), + ), + 'multi word categories' => array( + 'args' => array( 'category-fruits-yellow', false, 'category' ), + 'expected' => array( 'category-fruits-yellow', 'category', 'archive', 'index' ), + ), + 'single word taxonomy and term' => array( + 'args' => array( 'taxonomy-books-action', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books-action', 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word taxonomy and multi word term' => array( + 'args' => array( 'taxonomy-books-action-adventure', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books-action-adventure', 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'multi word taxonomy and term' => array( + 'args' => array( 'taxonomy-greek-books-action-adventure', false, 'taxonomy-greek-books' ), + 'expected' => array( 'taxonomy-greek-books-action-adventure', 'taxonomy-greek-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word post type' => array( + 'args' => array( 'single-book', false, 'single-book' ), + 'expected' => array( 'single-book', 'single', 'singular', 'index' ), + ), + 'multi word post type' => array( + 'args' => array( 'single-art-project', false, 'single-art-project' ), + 'expected' => array( 'single-art-project', 'single', 'singular', 'index' ), + ), + 'single post with multi word post type' => array( + 'args' => array( 'single-art-project-imagine', false, 'single-art-project' ), + 'expected' => array( 'single-art-project-imagine', 'single-art-project', 'single', 'singular', 'index' ), + ), + 'single page' => array( + 'args' => array( 'page-hi', false, 'page' ), + 'expected' => array( 'page-hi', 'page', 'singular', 'index' ), + ), + 'authors' => array( + 'args' => array( 'author-rigas', false, 'author' ), + 'expected' => array( 'author-rigas', 'author', 'archive', 'index' ), + ), + ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-post-types-controller.php b/tests/phpunit/tests/rest-api/rest-post-types-controller.php index 0ab2a7b937005..982fb4e04a23d 100644 --- a/tests/phpunit/tests/rest-api/rest-post-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-types-controller.php @@ -156,24 +156,31 @@ public function test_prepare_item_limit_fields() { ); } + /** + * @ticket 56467 + * + * @covers WP_REST_Post_Types_Controller::get_item_schema + */ public function test_get_item_schema() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 12, $properties ); - $this->assertArrayHasKey( 'capabilities', $properties ); - $this->assertArrayHasKey( 'description', $properties ); - $this->assertArrayHasKey( 'hierarchical', $properties ); - $this->assertArrayHasKey( 'viewable', $properties ); - $this->assertArrayHasKey( 'labels', $properties ); - $this->assertArrayHasKey( 'name', $properties ); - $this->assertArrayHasKey( 'slug', $properties ); - $this->assertArrayHasKey( 'supports', $properties ); - $this->assertArrayHasKey( 'taxonomies', $properties ); - $this->assertArrayHasKey( 'rest_base', $properties ); - $this->assertArrayHasKey( 'rest_namespace', $properties ); - $this->assertArrayHasKey( 'visibility', $properties ); + + $this->assertCount( 13, $properties, 'Schema should have 13 properties' ); + $this->assertArrayHasKey( 'capabilities', $properties, '`capabilities` should be included in the schema' ); + $this->assertArrayHasKey( 'description', $properties, '`description` should be included in the schema' ); + $this->assertArrayHasKey( 'hierarchical', $properties, '`hierarchical` should be included in the schema' ); + $this->assertArrayHasKey( 'viewable', $properties, '`viewable` should be included in the schema' ); + $this->assertArrayHasKey( 'labels', $properties, '`labels` should be included in the schema' ); + $this->assertArrayHasKey( 'name', $properties, '`name` should be included in the schema' ); + $this->assertArrayHasKey( 'slug', $properties, '`slug` should be included in the schema' ); + $this->assertArrayHasKey( 'supports', $properties, '`supports` should be included in the schema' ); + $this->assertArrayHasKey( 'taxonomies', $properties, '`taxonomies` should be included in the schema' ); + $this->assertArrayHasKey( 'rest_base', $properties, '`rest_base` should be included in the schema' ); + $this->assertArrayHasKey( 'rest_namespace', $properties, '`rest_namespace` should be included in the schema' ); + $this->assertArrayHasKey( 'visibility', $properties, '`visibility` should be included in the schema' ); + $this->assertArrayHasKey( 'icon', $properties, '`icon` should be included in the schema' ); } public function test_get_additional_field_registration() { diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index a1e4330c0cc4a..afc1ebc125830 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -150,12 +150,14 @@ public function test_expected_routes_in_schema() { '/wp/v2/template-parts/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/template-parts/(?P[\d]+)/revisions', '/wp/v2/template-parts/(?P[\d]+)/revisions/(?P[\d]+)', + '/wp/v2/template-parts/lookup', '/wp/v2/templates', '/wp/v2/templates/(?P[\d]+)/autosaves', '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w-]+)', '/wp/v2/templates/(?P[\d]+)/autosaves/(?P[\d]+)', '/wp/v2/templates/(?P[\d]+)/revisions', '/wp/v2/templates/(?P[\d]+)/revisions/(?P[\d]+)', + '/wp/v2/templates/lookup', '/wp/v2/themes', '/wp/v2/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', '/wp/v2/plugins', diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index c53701f9b6a8c..0ddd023c012f2 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -56,6 +56,7 @@ public static function wpTearDownAfterClass() { /** * @covers WP_REST_Templates_Controller::register_routes * @ticket 54596 + * @ticket 56467 */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); @@ -69,6 +70,11 @@ public function test_register_routes() { $routes, 'Single template based on the given ID route does not exist' ); + $this->assertArrayHasKey( + '/wp/v2/templates/lookup', + $routes, + 'Get template fallback content route does not exist' + ); } /** @@ -678,4 +684,109 @@ protected function find_and_normalize_template_by_id( $templates, $id ) { return null; } + /** + * @dataProvider data_create_item_with_is_wp_suggestion + * @ticket 56467 + * @covers WP_REST_Templates_Controller::create_item + * + * @param array $body_params Data set to test. + * @param array $expected Expected results. + */ + public function test_create_item_with_is_wp_suggestion( array $body_params, array $expected ) { + // Set up the user. + $body_params['author'] = self::$admin_id; + $expected['author'] = self::$admin_id; + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( $body_params ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $this->assertSame( $expected, $data ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_with_is_wp_suggestion() { + $expected = array( + 'id' => 'default//page-rigas', + 'theme' => 'default', + 'content' => array( + 'raw' => 'Content', + ), + 'slug' => 'page-rigas', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Just a description', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => false, + 'author' => null, + ); + + return array( + 'is_wp_suggestion: true' => array( + 'body_params' => array( + 'slug' => 'page-rigas', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'is_wp_suggestion' => true, + 'author' => null, + ), + 'expected' => $expected, + ), + 'is_wp_suggestion: false' => array( + 'body_params' => array( + 'slug' => 'page-hi', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'is_wp_suggestion' => false, + 'author' => null, + ), + 'expected' => array_merge( + $expected, + array( + 'id' => 'default//page-hi', + 'slug' => 'page-hi', + 'is_custom' => true, + ) + ), + ), + ); + } + + /** + * @ticket 56467 + * @covers WP_REST_Templates_Controller::get_template_fallback + */ + public function test_get_template_fallback() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/lookup' ); + // Should fallback to `index.html`. + $request->set_param( 'slug', 'tag-status' ); + $request->set_param( 'is_custom', false ); + $request->set_param( 'template_prefix', 'tag' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 'index', $response->get_data()['slug'], 'Should fallback to `index.html`.' ); + // Should fallback to `page.html`. + $request->set_param( 'slug', 'page-hello' ); + $request->set_param( 'is_custom', false ); + $request->set_param( 'template_prefix', 'page' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 'page', $response->get_data()['slug'], 'Should fallback to `page.html`.' ); + } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 4020b25da844d..051f176fc4a7f 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -5140,6 +5140,43 @@ mockedApiResponse.Schema = { ] } }, + "/wp/v2/templates/lookup": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "slug": { + "description": "The slug of the template to get the fallback for", + "type": "string", + "required": true + }, + "is_custom": { + "description": " Indicates if a template is custom or part of the template hierarchy", + "type": "boolean", + "required": false + }, + "template_prefix": { + "description": "The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/templates/lookup" + } + ] + } + }, "/wp/v2/templates/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ @@ -5792,6 +5829,43 @@ mockedApiResponse.Schema = { ] } }, + "/wp/v2/template-parts/lookup": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "slug": { + "description": "The slug of the template to get the fallback for", + "type": "string", + "required": true + }, + "is_custom": { + "description": " Indicates if a template is custom or part of the template hierarchy", + "type": "boolean", + "required": false + }, + "template_prefix": { + "description": "The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`", + "type": "string", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/template-parts/lookup" + } + ] + } + }, "/wp/v2/template-parts/(?P([^\\/:<>\\*\\?\"\\|]+(?:\\/[^\\/:<>\\*\\?\"\\|]+)?)[\\/\\w-]+)": { "namespace": "wp/v2", "methods": [ @@ -11591,6 +11665,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Posts", "slug": "post", + "icon": "dashicons-admin-post", "taxonomies": [ "category", "post_tag" @@ -11622,6 +11697,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": true, "name": "Pages", "slug": "page", + "icon": "dashicons-admin-page", "taxonomies": [], "rest_base": "pages", "rest_namespace": "wp/v2", @@ -11650,6 +11726,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Media", "slug": "attachment", + "icon": "dashicons-admin-media", "taxonomies": [], "rest_base": "media", "rest_namespace": "wp/v2", @@ -11678,6 +11755,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Navigation Menu Items", "slug": "nav_menu_item", + "icon": null, "taxonomies": [ "nav_menu" ], @@ -11708,6 +11786,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Reusable blocks", "slug": "wp_block", + "icon": null, "taxonomies": [], "rest_base": "blocks", "rest_namespace": "wp/v2", @@ -11736,6 +11815,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Templates", "slug": "wp_template", + "icon": null, "taxonomies": [], "rest_base": "templates", "rest_namespace": "wp/v2", @@ -11764,6 +11844,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Template Parts", "slug": "wp_template_part", + "icon": null, "taxonomies": [], "rest_base": "template-parts", "rest_namespace": "wp/v2", @@ -11792,6 +11873,7 @@ mockedApiResponse.TypesCollection = { "hierarchical": false, "name": "Navigation Menus", "slug": "wp_navigation", + "icon": null, "taxonomies": [], "rest_base": "navigation", "rest_namespace": "wp/v2", @@ -11822,6 +11904,7 @@ mockedApiResponse.TypeModel = { "hierarchical": false, "name": "Posts", "slug": "post", + "icon": "dashicons-admin-post", "taxonomies": [ "category", "post_tag"