diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php index a3043bf78d52d..55207a9b6496a 100644 --- a/lib/class-wp-theme-json.php +++ b/lib/class-wp-theme-json.php @@ -104,6 +104,7 @@ class WP_Theme_JSON { */ const SCHEMA = array( 'customTemplates' => null, + 'templateParts' => null, 'styles' => array( 'border' => array( 'radius' => null, @@ -1064,6 +1065,18 @@ public function get_custom_templates() { } } + /** + * Returns the template part data of current theme. + * + * @return array + */ + public function get_template_parts() { + if ( ! isset( $this->theme_json['templateParts'] ) ) { + return array(); + } + return $this->theme_json['templateParts']; + } + /** * Returns the stylesheet that results of processing * the theme.json structure this object represents. diff --git a/lib/full-site-editing/block-templates.php b/lib/full-site-editing/block-templates.php index 5f706c2233680..62bc8f818f2b5 100644 --- a/lib/full-site-editing/block-templates.php +++ b/lib/full-site-editing/block-templates.php @@ -49,12 +49,17 @@ function _gutenberg_get_template_file( $template_type, $slug ) { foreach ( $themes as $theme_slug => $theme_dir ) { $file_path = $theme_dir . '/' . $template_base_paths[ $template_type ] . '/' . $slug . '.html'; if ( file_exists( $file_path ) ) { - return array( + $new_template_item = array( 'slug' => $slug, 'path' => $file_path, 'theme' => $theme_slug, 'type' => $template_type, ); + + if ( 'wp_template_part' === $template_type ) { + return _gutenberg_add_template_part_area_info( $new_template_item ); + } + return $new_template_item; } } @@ -93,18 +98,45 @@ function _gutenberg_get_template_files( $template_type ) { // Subtract ending '.html'. -5 ); - $template_files[] = array( + $new_template_item = array( 'slug' => $template_slug, 'path' => $template_file, 'theme' => $theme_slug, 'type' => $template_type, ); + + if ( 'wp_template_part' === $template_type ) { + $template_files[] = _gutenberg_add_template_part_area_info( $new_template_item ); + } else { + $template_files[] = $new_template_item; + } } } return $template_files; } +/** + * Attempts to add the template part's area information to the input template. + * + * @param array $template_info Template to add information to (requires 'type' and 'slug' fields). + * + * @return array Template. + */ +function _gutenberg_add_template_part_area_info( $template_info ) { + if ( WP_Theme_JSON_Resolver::theme_has_support() ) { + $theme_data = WP_Theme_JSON_Resolver::get_theme_data()->get_template_parts(); + } + + if ( isset( $theme_data[ $template_info['slug'] ]['area'] ) ) { + $template_info['area'] = gutenberg_filter_template_part_area_type( $theme_data[ $template_info['slug'] ]['area'] ); + } else { + $template_info['area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED; + } + + return $template_info; +} + /** * Parses wp_template content and injects the current theme's * stylesheet as a theme attribute into each wp_template_part @@ -171,6 +203,10 @@ function _gutenberg_build_template_result_from_file( $template_file, $template_t $template->title = $default_template_types[ $template_file['slug'] ]['title']; } + if ( 'wp_template_part' === $template_type && isset( $template_file['area'] ) ) { + $template->area = $template_file['area']; + } + return $template; } @@ -206,6 +242,13 @@ function _gutenberg_build_template_result_from_post( $post ) { $template->title = $post->post_title; $template->status = $post->post_status; + if ( 'wp_template_part' === $post->post_type ) { + $type_terms = get_the_terms( $post, 'wp_template_part_area' ); + if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) { + $template->area = $type_terms[0]->name; + } + } + return $template; } @@ -237,6 +280,15 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t ), ); + if ( 'wp_template_part' === $template_type && isset( $query['area'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'wp_template_part_area', + 'field' => 'name', + 'terms' => $query['area'], + ); + $wp_query_args['tax_query']['relation'] = 'AND'; + } + if ( isset( $query['slug__in'] ) ) { $wp_query_args['post_name__in'] = $query['slug__in']; } @@ -261,14 +313,16 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t if ( ! isset( $query['wp_id'] ) ) { $template_files = _gutenberg_get_template_files( $template_type ); foreach ( $template_files as $template_file ) { - $is_custom = array_search( + $is_not_custom = false === array_search( wp_get_theme()->get_stylesheet() . '//' . $template_file['slug'], array_column( $query_result, 'id' ), true ); - $should_include = false === $is_custom && ( - ! isset( $query['slug__in'] ) || in_array( $template_file['slug'], $query['slug__in'], true ) - ); + $fits_slug_query = + ! isset( $query['slug__in'] ) || in_array( $template_file['slug'], $query['slug__in'], true ); + $fits_area_query = + ! isset( $query['area'] ) || $template_file['area'] === $query['area']; + $should_include = $is_not_custom && $fits_slug_query && $fits_area_query; if ( $should_include ) { $query_result[] = _gutenberg_build_template_result_from_file( $template_file, $template_type ); } diff --git a/lib/full-site-editing/class-wp-rest-templates-controller.php b/lib/full-site-editing/class-wp-rest-templates-controller.php index 79eb4dbba56d7..f2baa6a5b3698 100644 --- a/lib/full-site-editing/class-wp-rest-templates-controller.php +++ b/lib/full-site-editing/class-wp-rest-templates-controller.php @@ -138,6 +138,9 @@ public function get_items( $request ) { if ( isset( $request['wp_id'] ) ) { $query['wp_id'] = $request['wp_id']; } + if ( isset( $request['area'] ) ) { + $query['area'] = $request['area']; + } $templates = array(); foreach ( gutenberg_get_block_templates( $query, $this->post_type ) as $template ) { $data = $this->prepare_item_for_response( $template, $request ); @@ -358,6 +361,16 @@ protected function prepare_item_for_database( $request ) { $changes->post_excerpt = $template->description; } + if ( 'wp_template_part' === $this->post_type ) { + if ( isset( $request['area'] ) ) { + $changes->tax_input['wp_template_part_area'] = gutenberg_filter_template_part_area_type( $request['area'] ); + } elseif ( null !== $template && ! $template->is_custom && $template->area ) { + $changes->tax_input['wp_template_part_area'] = gutenberg_filter_template_part_area_type( $template->area ); + } elseif ( ! $template->area ) { + $changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED; + } + } + return $changes; } @@ -386,6 +399,10 @@ public function prepare_item_for_response( $template, $request ) { // phpcs:igno 'wp_id' => $template->wp_id, ); + if ( 'wp_template_part' === $template->type ) { + $result['area'] = $template->area; + } + $result = $this->add_additional_fields_to_object( $result, $request ); $response = rest_ensure_response( $result ); @@ -536,6 +553,14 @@ public function get_item_schema() { ), ); + if ( 'wp_template_part' === $this->post_type ) { + $schema['properties']['area'] = array( + 'description' => __( 'Where the template part is intended for use (header, footer, etc.)', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'embed', 'view', 'edit' ), + ); + } + $this->schema = $schema; return $this->add_additional_fields_schema( $this->schema ); diff --git a/lib/full-site-editing/template-parts.php b/lib/full-site-editing/template-parts.php index 172ca6a5ed2f9..46552ba9d3e6c 100644 --- a/lib/full-site-editing/template-parts.php +++ b/lib/full-site-editing/template-parts.php @@ -60,6 +60,49 @@ function gutenberg_register_template_part_post_type() { } add_action( 'init', 'gutenberg_register_template_part_post_type' ); +/** + * Registers the 'wp_template_part_area' taxonomy. + */ +function gutenberg_register_wp_template_part_area_taxonomy() { + if ( ! gutenberg_is_fse_theme() ) { + return; + } + + register_taxonomy( + 'wp_template_part_area', + array( 'wp_template_part' ), + array( + 'public' => false, + 'hierarchical' => false, + 'labels' => array( + 'name' => __( 'Template Part Areas', 'gutenberg' ), + 'singular_name' => __( 'Template Part Area', 'gutenberg' ), + ), + 'query_var' => false, + 'rewrite' => false, + 'show_ui' => false, + '_builtin' => true, + 'show_in_nav_menus' => false, + 'show_in_rest' => false, + ) + ); +} +add_action( 'init', 'gutenberg_register_wp_template_part_area_taxonomy' ); + +// Definte constants for supported wp_template_part_area taxonomy. +if ( ! defined( 'WP_TEMPLATE_PART_AREA_HEADER' ) ) { + define( 'WP_TEMPLATE_PART_AREA_HEADER', 'header' ); +} +if ( ! defined( 'WP_TEMPLATE_PART_AREA_FOOTER' ) ) { + define( 'WP_TEMPLATE_PART_AREA_FOOTER', 'footer' ); +} +if ( ! defined( 'WP_TEMPLATE_PART_AREA_SIDEBAR' ) ) { + define( 'WP_TEMPLATE_PART_AREA_SIDEBAR', 'sidebar' ); +} +if ( ! defined( 'WP_TEMPLATE_PART_AREA_UNCATEGORIZED' ) ) { + define( 'WP_TEMPLATE_PART_AREA_UNCATEGORIZED', 'uncategorized' ); +} + /** * Fixes the label of the 'wp_template_part' admin menu entry. */ @@ -118,3 +161,43 @@ function set_unique_slug_on_create_template_part( $post_id ) { } } add_action( 'save_post_wp_template_part', 'set_unique_slug_on_create_template_part' ); + +/** + * Returns a filtered list of allowed area types for template parts. + * + * @return array The supported template part area types. + */ +function gutenberg_get_allowed_template_part_area_types() { + $default_area_types = array( + WP_TEMPLATE_PART_AREA_HEADER, + WP_TEMPLATE_PART_AREA_FOOTER, + WP_TEMPLATE_PART_AREA_SIDEBAR, + WP_TEMPLATE_PART_AREA_UNCATEGORIZED, + ); + + /** + * Filters the list of allowed template part area types. + * + * @param array $default_area_types An array of supported area types. + */ + return apply_filters( 'default_wp_template_part_area_types', $default_area_types ); +} + +/** + * Checks whether the input 'type' is a supported area type. + * Returns the input if supported, otherwise returns the 'other' type. + * + * @param string $type Template part area name. + * + * @return string Input if supported, else 'other'. + */ +function gutenberg_filter_template_part_area_type( $type ) { + if ( in_array( $type, gutenberg_get_allowed_template_part_area_types(), true ) ) { + return $type; + } + $warning_message = '"' . $type . '"'; + $warning_message .= __( ' is not a supported wp_template_part_area type and has been added as ', 'gutenberg' ); + $warning_message .= '"' . WP_TEMPLATE_PART_AREA_UNCATEGORIZED . '".'; + trigger_error( $warning_message, E_USER_NOTICE ); + return WP_TEMPLATE_PART_AREA_UNCATEGORIZED; +} diff --git a/phpunit/class-block-templates-test.php b/phpunit/class-block-templates-test.php index 40c62bf3e6f78..df952f0f01ec4 100644 --- a/phpunit/class-block-templates-test.php +++ b/phpunit/class-block-templates-test.php @@ -11,13 +11,16 @@ */ class Block_Templates_Test extends WP_UnitTestCase { private static $post; + private static $template_part_post; public static function wpSetUpBeforeClass() { switch_theme( 'tt1-blocks' ); gutenberg_register_template_post_type(); gutenberg_register_template_part_post_type(); gutenberg_register_wp_theme_taxonomy(); + gutenberg_register_wp_template_part_area_taxonomy(); + // Set up template post. $args = array( 'post_type' => 'wp_template', 'post_name' => 'my_template', @@ -32,6 +35,26 @@ public static function wpSetUpBeforeClass() { ); self::$post = self::factory()->post->create_and_get( $args ); wp_set_post_terms( self::$post->ID, get_stylesheet(), 'wp_theme' ); + + // Set up template part post. + $template_part_args = 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( + get_stylesheet(), + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_SIDEBAR, + ), + ), + ); + self::$template_part_post = self::factory()->post->create_and_get( $template_part_args ); + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_SIDEBAR, 'wp_template_part_area' ); + wp_set_post_terms( self::$template_part_post->ID, get_stylesheet(), 'wp_theme' ); } public static function wpTearDownAfterClass() { @@ -55,6 +78,25 @@ function test_gutenberg_build_template_result_from_file() { $this->assertEquals( 'Single', $template->title ); $this->assertEquals( 'Used when a single entry that is not a Page is queried', $template->description ); $this->assertEquals( 'wp_template', $template->type ); + + // Test template parts. + $template_part = _gutenberg_build_template_result_from_file( + array( + 'slug' => 'header', + 'path' => __DIR__ . '/fixtures/template.html', + 'area' => WP_TEMPLATE_PART_AREA_HEADER, + ), + 'wp_template_part' + ); + $this->assertEquals( get_stylesheet() . '//header', $template_part->id ); + $this->assertEquals( get_stylesheet(), $template_part->theme ); + $this->assertEquals( 'header', $template_part->slug ); + $this->assertEquals( 'publish', $template_part->status ); + $this->assertEquals( false, $template_part->is_custom ); + $this->assertEquals( 'header', $template_part->title ); + $this->assertEquals( '', $template_part->description ); + $this->assertEquals( 'wp_template_part', $template_part->type ); + $this->assertEquals( WP_TEMPLATE_PART_AREA_HEADER, $template_part->area ); } function test_gutenberg_build_template_result_from_post() { @@ -72,6 +114,22 @@ function test_gutenberg_build_template_result_from_post() { $this->assertEquals( 'My Template', $template->title ); $this->assertEquals( 'Description of my template', $template->description ); $this->assertEquals( 'wp_template', $template->type ); + + // Test template parts. + $template_part = _gutenberg_build_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertNotWPError( $template_part ); + $this->assertEquals( get_stylesheet() . '//my_template_part', $template_part->id ); + $this->assertEquals( get_stylesheet(), $template_part->theme ); + $this->assertEquals( 'my_template_part', $template_part->slug ); + $this->assertEquals( 'publish', $template_part->status ); + $this->assertEquals( true, $template_part->is_custom ); + $this->assertEquals( 'My Template Part', $template_part->title ); + $this->assertEquals( 'Description of my template part', $template_part->description ); + $this->assertEquals( 'wp_template_part', $template_part->type ); + $this->assertEquals( WP_TEMPLATE_PART_AREA_SIDEBAR, $template_part->area ); } function test_inject_theme_attribute_in_content() { @@ -116,6 +174,18 @@ function test_gutenberg_get_block_template_from_file() { $this->assertEquals( 'publish', $template->status ); $this->assertEquals( false, $template->is_custom ); $this->assertEquals( 'wp_template', $template->type ); + + // Test template parts. + $id = get_stylesheet() . '//' . 'header'; + $template = gutenberg_get_block_template( $id, 'wp_template_part' ); + $this->assertEquals( $id, $template->id ); + $this->assertEquals( get_stylesheet(), $template->theme ); + $this->assertEquals( 'header', $template->slug ); + $this->assertEquals( 'publish', $template->status ); + $this->assertEquals( false, $template->is_custom ); + $this->assertEquals( 'wp_template_part', $template->type ); + // TODO - update 'UNCATEGORIZED' to 'HEADER' once tt1-blocks theme.json updated for template part area info. + $this->assertEquals( WP_TEMPLATE_PART_AREA_UNCATEGORIZED, $template->area ); } /** @@ -130,6 +200,17 @@ function test_gutenberg_get_block_template_from_post() { $this->assertEquals( 'publish', $template->status ); $this->assertEquals( true, $template->is_custom ); $this->assertEquals( 'wp_template', $template->type ); + + // Test template parts. + $id = get_stylesheet() . '//' . 'my_template_part'; + $template = gutenberg_get_block_template( $id, 'wp_template_part' ); + $this->assertEquals( $id, $template->id ); + $this->assertEquals( get_stylesheet(), $template->theme ); + $this->assertEquals( 'my_template_part', $template->slug ); + $this->assertEquals( 'publish', $template->status ); + $this->assertEquals( true, $template->is_custom ); + $this->assertEquals( 'wp_template_part', $template->type ); + $this->assertEquals( WP_TEMPLATE_PART_AREA_SIDEBAR, $template->area ); } /** @@ -162,5 +243,11 @@ function( $template ) { $templates = gutenberg_get_block_templates( array( 'wp_id' => self::$post->ID ), 'wp_template' ); $template_ids = get_template_ids( $templates ); $this->assertEquals( array( get_stylesheet() . '//' . 'my_template' ), $template_ids ); + + // Filter template part by area. + $templates = gutenberg_get_block_templates( array( 'area' => WP_TEMPLATE_PART_AREA_SIDEBAR ), 'wp_template_part' ); + $template_ids = get_template_ids( $templates ); + // TODO - update following array result once tt1-blocks theme.json is updated for area info. + $this->assertEquals( array( get_stylesheet() . '//' . 'my_template_part' ), $template_ids ); } } diff --git a/phpunit/class-wp-rest-template-controller-test.php b/phpunit/class-wp-rest-template-controller-test.php index f7d8ffd182c74..154a8d440990f 100644 --- a/phpunit/class-wp-rest-template-controller-test.php +++ b/phpunit/class-wp-rest-template-controller-test.php @@ -16,6 +16,7 @@ public static function wpSetupBeforeClass( $factory ) { gutenberg_register_template_post_type(); gutenberg_register_template_part_post_type(); gutenberg_register_wp_theme_taxonomy(); + gutenberg_register_wp_template_part_area_taxonomy(); self::$admin_id = $factory->user->create( array( 'role' => 'administrator', @@ -76,6 +77,31 @@ function find_and_normalize_template_by_id( $templates, $id ) { ), find_and_normalize_template_by_id( $data, 'tt1-blocks//index' ) ); + + // Test template parts. + $request = new WP_REST_Request( 'GET', '/wp/v2/template-parts' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( + array( + 'id' => 'tt1-blocks//header', + 'theme' => 'tt1-blocks', + 'slug' => 'header', + 'title' => array( + 'raw' => 'header', + 'rendered' => 'header', + ), + 'description' => '', + 'status' => 'publish', + 'is_custom' => false, + 'type' => 'wp_template_part', + 'wp_id' => null, + // TODO - update 'UNCATEGORIZED' to 'HEADER' once tt1-blocks theme.json updated for template part area info. + 'area' => WP_TEMPLATE_PART_AREA_UNCATEGORIZED, + ), + find_and_normalize_template_by_id( $data, 'tt1-blocks//header' ) + ); } public function test_get_item() { @@ -103,6 +129,32 @@ public function test_get_item() { ), $data ); + + // Test template parts. + $request = new WP_REST_Request( 'GET', '/wp/v2/template-parts/tt1-blocks//header' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['content'] ); + unset( $data['_links'] ); + $this->assertEquals( + array( + 'id' => 'tt1-blocks//header', + 'theme' => 'tt1-blocks', + 'slug' => 'header', + 'title' => array( + 'raw' => 'header', + 'rendered' => 'header', + ), + 'description' => '', + 'status' => 'publish', + 'is_custom' => false, + 'type' => 'wp_template_part', + 'wp_id' => null, + // TODO - update 'UNCATEGORIZED' to 'HEADER' once tt1-blocks theme.json updated for template part area info. + 'area' => WP_TEMPLATE_PART_AREA_UNCATEGORIZED, + ), + $data + ); } public function test_create_item() { @@ -140,6 +192,43 @@ public function test_create_item() { ), $data ); + + // Test template parts. + $request = new WP_REST_Request( 'POST', '/wp/v2/template-parts' ); + $request->set_body_params( + array( + 'slug' => 'my_custom_template_part', + 'title' => 'My Template Part', + 'description' => 'Just a description of a template part', + 'content' => 'Content', + 'area' => 'header', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $this->assertEquals( + array( + 'id' => 'tt1-blocks//my_custom_template_part', + 'theme' => 'tt1-blocks', + 'slug' => 'my_custom_template_part', + 'title' => array( + 'raw' => 'My Template Part', + 'rendered' => 'My Template Part', + ), + 'description' => 'Just a description of a template part', + 'status' => 'publish', + 'is_custom' => true, + 'type' => 'wp_template_part', + 'content' => array( + 'raw' => 'Content', + ), + 'area' => 'header', + ), + $data + ); } public function test_update_item() { @@ -154,6 +243,18 @@ public function test_update_item() { $data = $response->get_data(); $this->assertEquals( 'My new Index Title', $data['title']['raw'] ); $this->assertEquals( true, $data['is_custom'] ); + + // Test template parts. + $request = new WP_REST_Request( 'PUT', '/wp/v2/template-parts/tt1-blocks//header' ); + $request->set_body_params( + array( + 'area' => 'something unsupported', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( WP_TEMPLATE_PART_AREA_UNCATEGORIZED, $data['area'] ); + $this->assertEquals( true, $data['is_custom'] ); } public function test_delete_item() { diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 13dd9c41930f9..857b46d57baf5 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -247,7 +247,7 @@ function test_get_stylesheet() { ), 'misc' => 'value', ), - 'core/group' => array( + 'core/group' => array( 'custom' => array( 'base-font' => 16, 'line-height' => array( @@ -760,4 +760,27 @@ function test_get_custom_templates() { ) ); } + + function test_get_template_parts() { + $theme_json = new WP_Theme_JSON( + array( + 'templateParts' => array( + 'header' => array( + 'area' => 'Some area', + ), + ), + ) + ); + + $template_parts = $theme_json->get_template_parts(); + + $this->assertEqualSetsWithIndex( + $template_parts, + array( + 'header' => array( + 'area' => 'Some area', + ), + ) + ); + } }