From f99819b3e33dcc7f8c90ded369e0e1b1c3c3a3ce Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Thu, 4 Aug 2022 08:55:27 +0300 Subject: [PATCH] [Site Editor]: Add fallback template content on creation (#42520) * [Site Editor]: Add fallback template content on creation * Update lib/compat/wordpress-6.1/block-template-utils.php Co-authored-by: Bernie Reiter * fix function name typo * rename the route * move template hierarchy calculation client side * move get_template_hierarchy server side * remove js tests Co-authored-by: Bernie Reiter --- .../wordpress-6.1/block-template-utils.php | 60 +++++++ ...ss-gutenberg-rest-templates-controller.php | 56 ++++++ .../add-custom-template-modal.js | 15 +- .../add-new-template/new-template.js | 17 +- .../src/components/add-new-template/utils.js | 17 +- phpunit/block-template-utils-test.php | 164 ++++++++++++++++++ ...tenberg-rest-templates-controller-test.php | 97 +++++++++++ test/emptytheme/block-templates/category.html | 1 + test/emptytheme/block-templates/index.html | 3 +- test/emptytheme/block-templates/singular.html | 6 +- 10 files changed, 423 insertions(+), 13 deletions(-) create mode 100644 phpunit/block-template-utils-test.php create mode 100644 phpunit/class-gutenberg-rest-templates-controller-test.php create mode 100644 test/emptytheme/block-templates/category.html diff --git a/lib/compat/wordpress-6.1/block-template-utils.php b/lib/compat/wordpress-6.1/block-template-utils.php index 5a2c7c5664ae2..03b93c8393746 100644 --- a/lib/compat/wordpress-6.1/block-template-utils.php +++ b/lib/compat/wordpress-6.1/block-template-utils.php @@ -296,3 +296,63 @@ function gutenberg_build_block_template_result_from_post( $post ) { } return $template; } + +/** + * Helper function to get the Template Hierarchy for a given slug. + * We need to Handle special cases here like `front-page`, `singular` and `archive` templates. + * + * Noting that we always add `index` as the last fallback template. + * + * @param string $slug The template slug to be created. + * @param boolean $is_custom Indicates if a template is custom or part of the template hierarchy. + * @param string $template_prefix The template prefix for the created template. This is used to extract the main template type ex. in `taxonomy-books` we extract the `taxonomy`. + * + * @return array 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 ); + // We need these checks because we always add the `$slug` above. + 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/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php b/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php index 7c73d799aa843..1f62bd2df74ba 100644 --- a/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php +++ b/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php @@ -10,6 +10,62 @@ * Base Templates REST API Controller. */ class Gutenberg_REST_Templates_Controller extends WP_REST_Templates_Controller { + + /** + * Registers the controllers routes. + * + * @return void + */ + 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_items_permissions_check' ), + 'args' => array( + 'slug' => array( + 'description' => __( 'The slug of the template to get the fallback for', 'gutenberg' ), + 'type' => 'string', + ), + 'is_custom' => array( + 'description' => __( ' Indicates if a template is custom or part of the template hierarchy', 'gutenberg' ), + 'type' => 'boolean', + ), + 'template_prefix' => array( + 'description' => __( 'The template prefix for the created template. This is used to extract the main template type ex. in `taxonomy-books` we extract the `taxonomy`', 'gutenberg' ), + 'type' => 'string', + ), + ), + ), + ) + ); + parent::register_routes(); + } + + /** + * Returns the fallback template for a given slug. + * + * @param WP_REST_Request $request The request instance. + * + * @return WP_REST_Response|WP_Error + */ + public function get_template_fallback( $request ) { + if ( empty( $request['slug'] ) ) { + return new WP_Error( + 'rest_invalid_param', + __( 'Invalid slug.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); + $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); + return rest_ensure_response( $fallback_template ); + } + /** * Returns a list of templates. * diff --git a/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js b/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js index 378bfbed4d8e3..3b425966b4b12 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-template-modal.js @@ -192,9 +192,18 @@ function AddCustomTemplateModal( { onClose, onSelect, entityForSuggestions } ) { isBlock as={ Button } onClick={ () => { - const { slug, title, description } = - entityForSuggestions.template; - onSelect( { slug, title, description } ); + const { + slug, + title, + description, + templatePrefix, + } = entityForSuggestions.template; + onSelect( { + slug, + title, + description, + templatePrefix, + } ); } } > diff --git a/packages/edit-site/src/components/add-new-template/new-template.js b/packages/edit-site/src/components/add-new-template/new-template.js index 58cd9d86925ef..035f21f07c982 100644 --- a/packages/edit-site/src/components/add-new-template/new-template.js +++ b/packages/edit-site/src/components/add-new-template/new-template.js @@ -1,6 +1,8 @@ /** * WordPress dependencies */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; import { DropdownMenu, MenuGroup, @@ -91,7 +93,19 @@ export default function NewTemplate( { postType } ) { async function createTemplate( template, isWPSuggestion = true ) { try { - const { title, description, slug } = template; + const { title, description, slug, templatePrefix } = template; + let templateContent = template.content; + // Try to find fallback content from existing templates. + if ( ! templateContent ) { + const fallbackTemplate = await apiFetch( { + path: addQueryArgs( '/wp/v2/templates/lookup', { + slug, + is_custom: ! isWPSuggestion, + template_prefix: templatePrefix, + } ), + } ); + templateContent = fallbackTemplate.content; + } const newTemplate = await saveEntityRecord( 'postType', 'wp_template', @@ -101,6 +115,7 @@ export default function NewTemplate( { postType } ) { slug: slug.toString(), status: 'publish', title, + content: templateContent, // This adds a post meta field in template that is part of `is_custom` value calculation. is_wp_suggestion: isWPSuggestion, }, diff --git a/packages/edit-site/src/components/add-new-template/utils.js b/packages/edit-site/src/components/add-new-template/utils.js index 7c176cb4b4f90..607848291bdb1 100644 --- a/packages/edit-site/src/components/add-new-template/utils.js +++ b/packages/edit-site/src/components/add-new-template/utils.js @@ -152,7 +152,10 @@ export const usePostTypeMenuItems = ( onClickMenuItem ) => { ); } const menuItem = defaultTemplateType - ? { ...defaultTemplateType } + ? { + ...defaultTemplateType, + templatePrefix: templatePrefixes[ slug ], + } : { slug: generalTemplateSlug, title: menuItemTitle, @@ -167,6 +170,7 @@ export const usePostTypeMenuItems = ( onClickMenuItem ) => { icon: icon?.startsWith( 'dashicons-' ) ? icon.slice( 10 ) : post, + templatePrefix: templatePrefixes[ slug ], }; const hasEntities = postTypesInfo?.[ slug ]?.hasEntities; // We have a different template creation flow only if they have entities. @@ -210,6 +214,7 @@ export const usePostTypeMenuItems = ( onClickMenuItem ) => { title, description, slug: `${ templatePrefixes[ slug ] }-${ suggestion.slug }`, + templatePrefix: templatePrefixes[ slug ], }; }, }, @@ -317,7 +322,10 @@ export const useTaxonomiesMenuItems = ( onClickMenuItem ) => { ); } const menuItem = defaultTemplateType - ? { ...defaultTemplateType } + ? { + ...defaultTemplateType, + templatePrefix: templatePrefixes[ slug ], + } : { slug: generalTemplateSlug, title: menuItemTitle, @@ -327,6 +335,7 @@ export const useTaxonomiesMenuItems = ( onClickMenuItem ) => { labels.singular_name ), icon: blockMeta, + templatePrefix: templatePrefixes[ slug ], }; const hasEntities = taxonomiesInfo?.[ slug ]?.hasEntities; // We have a different template creation flow only if they have entities. @@ -369,6 +378,7 @@ export const useTaxonomiesMenuItems = ( onClickMenuItem ) => { title, description, slug: `${ templatePrefixes[ slug ] }-${ suggestion.slug }`, + templatePrefix: templatePrefixes[ slug ], }; }, }, @@ -455,7 +465,7 @@ export function useAuthorMenuItem( onClickMenuItem ) { ( { slug } ) => slug === 'author' ); if ( authorInfo.user?.hasEntities ) { - authorMenuItem = { ...authorMenuItem }; + authorMenuItem = { ...authorMenuItem, templatePrefix: 'author' }; authorMenuItem.onClick = ( template ) => { onClickMenuItem( { type: 'root', @@ -494,6 +504,7 @@ export function useAuthorMenuItem( onClickMenuItem ) { title, description, slug: `author-${ suggestion.slug }`, + templatePrefix: 'author', }; }, }, diff --git a/phpunit/block-template-utils-test.php b/phpunit/block-template-utils-test.php new file mode 100644 index 0000000000000..ceffe180d9870 --- /dev/null +++ b/phpunit/block-template-utils-test.php @@ -0,0 +1,164 @@ +assertEquals( array( 'front-page', 'home', 'index' ), $hierarchy ); + // Custom templates. + $hierarchy = get_template_hierarchy( 'whatever-slug', true ); + $this->assertEquals( array( 'page', 'singular', 'index' ), $hierarchy ); + // Single slug templates(ex. page, tag, author, etc.. + $hierarchy = get_template_hierarchy( 'page' ); + $this->assertEquals( array( 'page', 'singular', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'tag' ); + $this->assertEquals( array( 'tag', 'archive', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'author' ); + $this->assertEquals( array( 'author', 'archive', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'date' ); + $this->assertEquals( array( 'date', 'archive', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'taxonomy' ); + $this->assertEquals( array( 'taxonomy', 'archive', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'attachment' ); + $this->assertEquals( + array( + 'attachment', + 'single', + 'singular', + 'index', + ), + $hierarchy + ); + $hierarchy = get_template_hierarchy( 'singular' ); + $this->assertEquals( array( 'singular', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'single' ); + $this->assertEquals( array( 'single', 'singular', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'archive' ); + $this->assertEquals( array( 'archive', 'index' ), $hierarchy ); + $hierarchy = get_template_hierarchy( 'index' ); + $this->assertEquals( array( 'index' ), $hierarchy ); + + // Taxonomies. + $hierarchy = get_template_hierarchy( 'taxonomy-books', false, 'taxonomy-books' ); + $this->assertEquals( array( 'taxonomy-books', 'taxonomy', 'archive', 'index' ), $hierarchy ); + // Single word category. + $hierarchy = get_template_hierarchy( 'category-fruits', false, 'category' ); + $this->assertEquals( + array( + 'category-fruits', + 'category', + 'archive', + 'index', + ), + $hierarchy + ); + // Multi word category. + $hierarchy = get_template_hierarchy( 'category-fruits-yellow', false, 'category' ); + $this->assertEquals( + array( + 'category-fruits-yellow', + 'category', + 'archive', + 'index', + ), + $hierarchy + ); + // Single word taxonomy. + $hierarchy = get_template_hierarchy( 'taxonomy-books-action', false, 'taxonomy-books' ); + $this->assertEquals( + array( + 'taxonomy-books-action', + 'taxonomy-books', + 'taxonomy', + 'archive', + 'index', + ), + $hierarchy + ); + $hierarchy = get_template_hierarchy( 'taxonomy-books-action-adventure', false, 'taxonomy-books' ); + $this->assertEquals( + array( + 'taxonomy-books-action-adventure', + 'taxonomy-books', + 'taxonomy', + 'archive', + 'index', + ), + $hierarchy + ); + // Multi word taxonomy/terms. + $hierarchy = get_template_hierarchy( 'taxonomy-greek-books-action-adventure', false, 'taxonomy-greek-books' ); + $this->assertEquals( + array( + 'taxonomy-greek-books-action-adventure', + 'taxonomy-greek-books', + 'taxonomy', + 'archive', + 'index', + ), + $hierarchy + ); + // Post types. + $hierarchy = get_template_hierarchy( 'single-book', false, 'single-book' ); + $this->assertEquals( + array( + 'single-book', + 'single', + 'singular', + 'index', + ), + $hierarchy + ); + $hierarchy = get_template_hierarchy( 'single-art-project', false, 'single-art-project' ); + $this->assertEquals( + array( + 'single-art-project', + 'single', + 'singular', + 'index', + ), + $hierarchy + ); + $hierarchy = get_template_hierarchy( 'single-art-project-imagine', false, 'single-art-project' ); + $this->assertEquals( + array( + 'single-art-project-imagine', + 'single-art-project', + 'single', + 'singular', + 'index', + ), + $hierarchy + ); + $hierarchy = get_template_hierarchy( 'page-hi', false, 'page' ); + $this->assertEquals( + array( + 'page-hi', + 'page', + 'singular', + 'index', + ), + $hierarchy + ); + // Authors. + $hierarchy = get_template_hierarchy( 'author-rigas', false, 'author' ); + $this->assertEquals( + array( + 'author-rigas', + 'author', + 'archive', + 'index', + ), + $hierarchy + ); + } +} diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php new file mode 100644 index 0000000000000..cd91f3dcf6bf2 --- /dev/null +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -0,0 +1,97 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/templates/lookup', + $routes, + 'Get template fallback content route does not exist' + ); + } + + public function test_get_template_fallback() { + $base_path = gutenberg_dir_path() . 'test/emptytheme/block-templates/'; + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/lookup' ); + // Should match `category.html`. + $request->set_param( 'slug', 'category-fruits' ); + $request->set_param( 'is_custom', false ); + $request->set_param( 'template_prefix', 'category' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data()->content; + $expected = file_get_contents( $base_path . 'category.html' ); + $this->assertEquals( $expected, $data ); + // 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 ); + $data = $response->get_data()->content; + $expected = file_get_contents( $base_path . 'index.html' ); + $this->assertEquals( $expected, $data ); + // Should fallback to `singular.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 ); + $data = $response->get_data()->content; + $expected = file_get_contents( $base_path . 'singular.html' ); + $this->assertEquals( $expected, $data ); + } + + public function test_context_param() { + $this->markTestIncomplete(); + } + + public function test_get_items() { + $this->markTestIncomplete(); + } + + public function test_get_item() { + $this->markTestIncomplete(); + } + + public function test_create_item() { + $this->markTestIncomplete(); + } + + public function test_update_item() { + $this->markTestIncomplete(); + } + + public function test_delete_item() { + $this->markTestIncomplete(); + } + + public function test_prepare_item() { + $this->markTestIncomplete(); + } + + public function test_get_item_schema() { + $this->markTestIncomplete(); + } +} diff --git a/test/emptytheme/block-templates/category.html b/test/emptytheme/block-templates/category.html new file mode 100644 index 0000000000000..8c389a214392d --- /dev/null +++ b/test/emptytheme/block-templates/category.html @@ -0,0 +1 @@ + diff --git a/test/emptytheme/block-templates/index.html b/test/emptytheme/block-templates/index.html index 74f7551f8bb49..f1cd4dcbb0065 100644 --- a/test/emptytheme/block-templates/index.html +++ b/test/emptytheme/block-templates/index.html @@ -1,5 +1,4 @@ - - +
diff --git a/test/emptytheme/block-templates/singular.html b/test/emptytheme/block-templates/singular.html index 45d77e8279e49..b77fd11ad9a76 100644 --- a/test/emptytheme/block-templates/singular.html +++ b/test/emptytheme/block-templates/singular.html @@ -1,5 +1,3 @@ - - + - - \ No newline at end of file +