diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php index dd372eff7943b..2e1321dee0f7e 100644 --- a/lib/compat/wordpress-6.5/rest-api.php +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -19,3 +19,124 @@ function gutenberg_register_global_styles_revisions_endpoints() { } add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); + +/** + * Registers additional fields for wp_template rest api. + * + * @access private + * @internal + * + * @param array $template_object Template object. + * @return string Original source of the template one of theme, plugin, site, or user. + */ +function _gutenberg_get_wp_templates_original_source_field( $template_object ) { + if ( 'wp_template' === $template_object['type'] || 'wp_template_part' === $template_object['type'] ) { + // Added by theme. + // Template originally provided by a theme, but customized by a user. + // Templates originally didn't have the 'origin' field so identify + // older customized templates by checking for no origin and a 'theme' + // or 'custom' source. + if ( $template_object['has_theme_file'] && + ( 'theme' === $template_object['origin'] || ( + empty( $template_object['origin'] ) && in_array( + $template_object['source'], + array( + 'theme', + 'custom', + ), + true + ) ) + ) + ) { + return 'theme'; + } + + // Added by plugin. + if ( $template_object['has_theme_file'] && 'plugin' === $template_object['origin'] ) { + return 'plugin'; + } + + // Added by site. + // Template was created from scratch, but has no author. Author support + // was only added to templates in WordPress 5.9. Fallback to showing the + // site logo and title. + if ( empty( $template_object['has_theme_file'] ) && 'custom' === $template_object['source'] && empty( $template_object['author'] ) ) { + return 'site'; + } + } + + // Added by user. + return 'user'; +} + +/** + * Registers additional fields for wp_template rest api. + * + * @access private + * @internal + * + * @param array $template_object Template object. + * @return string Human readable text for the author. + */ +function _gutenberg_get_wp_templates_author_text_field( $template_object ) { + $original_source = _gutenberg_get_wp_templates_original_source_field( $template_object ); + switch ( $original_source ) { + case 'theme': + $theme_name = wp_get_theme( $template_object['theme'] )->get( 'Name' ); + return empty( $theme_name ) ? $template_object['theme'] : $theme_name; + case 'plugin': + $plugins = get_plugins(); + $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object['theme'] . '.php' ) ) ]; + return empty( $plugin['Name'] ) ? $template_object['theme'] : $plugin['Name']; + case 'site': + return get_bloginfo( 'name' ); + case 'user': + return get_user_by( 'id', $template_object['author'] )->get( 'display_name' ); + } +} + +/** + * Registers additional fields for wp_template rest api. + * + * @access private + * @internal + */ +function _gutenberg_register_wp_templates_additional_fields() { + register_rest_field( + 'wp_template', + 'author_text', + array( + 'get_callback' => '_gutenberg_get_wp_templates_author_text_field', + 'update_callback' => null, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'Human readable text for the author.', 'gutenberg' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + + register_rest_field( + 'wp_template', + 'original_source', + array( + 'get_callback' => '_gutenberg_get_wp_templates_original_source_field', + 'update_callback' => null, + 'schema' => array( + 'description' => __( 'Where the template originally comes from e.g. \'theme\'', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + 'enum' => array( + 'theme', + 'plugin', + 'site', + 'user', + ), + ), + ) + ); +} + +add_action( 'rest_api_init', '_gutenberg_register_wp_templates_additional_fields' ); diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 26d1061432311..1b342b414db76 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -142,57 +142,7 @@ export default function DataviewsTemplates() { useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, } ); - const { shownTemplates, paginationInfo } = useMemo( () => { - if ( ! allTemplates ) { - return { - shownTemplates: EMPTY_ARRAY, - paginationInfo: { totalItems: 0, totalPages: 0 }, - }; - } - let filteredTemplates = [ ...allTemplates ]; - // Handle global search. - if ( view.search ) { - const normalizedSearch = normalizeSearchInput( view.search ); - filteredTemplates = filteredTemplates.filter( ( item ) => { - const title = item.title?.rendered || item.slug; - return ( - normalizeSearchInput( title ).includes( - normalizedSearch - ) || - normalizeSearchInput( item.description ).includes( - normalizedSearch - ) - ); - } ); - } - // Handle sorting. - // TODO: Explore how this can be more dynamic.. - if ( view.sort ) { - if ( view.sort.field === 'title' ) { - filteredTemplates.sort( ( a, b ) => { - const titleA = a.title?.rendered || a.slug; - const titleB = b.title?.rendered || b.slug; - return view.sort.direction === 'asc' - ? titleA.localeCompare( titleB ) - : titleB.localeCompare( titleA ); - } ); - } - } - // Handle pagination. - const start = ( view.page - 1 ) * view.perPage; - const totalItems = filteredTemplates?.length || 0; - filteredTemplates = filteredTemplates?.slice( - start, - start + view.perPage - ); - return { - shownTemplates: filteredTemplates, - paginationInfo: { - totalItems, - totalPages: Math.ceil( totalItems / view.perPage ), - }, - }; - }, [ allTemplates, view ] ); + const fields = useMemo( () => [ { @@ -237,13 +187,74 @@ export default function DataviewsTemplates() { { header: __( 'Author' ), id: 'author', - render: ( { item } ) => , + getValue: ( { item } ) => item.author_text, + render: ( { item } ) => { + return ; + }, enableHiding: false, - enableSorting: false, }, ], [] ); + + const { shownTemplates, paginationInfo } = useMemo( () => { + if ( ! allTemplates ) { + return { + shownTemplates: EMPTY_ARRAY, + paginationInfo: { totalItems: 0, totalPages: 0 }, + }; + } + let filteredTemplates = [ ...allTemplates ]; + // Handle global search. + if ( view.search ) { + const normalizedSearch = normalizeSearchInput( view.search ); + filteredTemplates = filteredTemplates.filter( ( item ) => { + const title = item.title?.rendered || item.slug; + return ( + normalizeSearchInput( title ).includes( + normalizedSearch + ) || + normalizeSearchInput( item.description ).includes( + normalizedSearch + ) + ); + } ); + } + + // Handle sorting. + if ( view.sort ) { + const stringSortingFields = [ 'title', 'author' ]; + const fieldId = view.sort.field; + if ( stringSortingFields.includes( fieldId ) ) { + const fieldToSort = fields.find( ( field ) => { + return field.id === fieldId; + } ); + filteredTemplates.sort( ( a, b ) => { + const valueA = fieldToSort.getValue( { item: a } ) ?? ''; + const valueB = fieldToSort.getValue( { item: b } ) ?? ''; + return view.sort.direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + } ); + } + } + + // Handle pagination. + const start = ( view.page - 1 ) * view.perPage; + const totalItems = filteredTemplates?.length || 0; + filteredTemplates = filteredTemplates?.slice( + start, + start + view.perPage + ); + return { + shownTemplates: filteredTemplates, + paginationInfo: { + totalItems, + totalPages: Math.ceil( totalItems / view.perPage ), + }, + }; + }, [ allTemplates, view, fields ] ); + const resetTemplateAction = useResetTemplateAction(); const actions = useMemo( () => [ diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php index 0992399464c5c..bbb588fd583ea 100644 --- a/phpunit/class-gutenberg-rest-templates-controller-test.php +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -99,7 +99,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 15, $properties ); + $this->assertCount( 17, $properties ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'slug', $properties ); @@ -131,23 +131,25 @@ public function test_get_item() { $this->assertSame( array( - 'id' => 'emptytheme//my_template', - 'theme' => 'emptytheme', - 'slug' => 'my_template', - 'source' => 'custom', - 'origin' => null, - 'type' => 'wp_template', - 'description' => 'Description of my template.', - 'title' => array( + 'id' => 'emptytheme//my_template', + 'theme' => 'emptytheme', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( 'raw' => 'My Template', 'rendered' => 'My Template', ), - 'status' => 'publish', - 'wp_id' => self::$post->ID, - 'has_theme_file' => false, - 'is_custom' => true, - 'author' => 0, - 'modified' => mysql_to_rfc3339( self::$post->post_modified ), + 'status' => 'publish', + 'wp_id' => self::$post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => mysql_to_rfc3339( self::$post->post_modified ), + 'author_text' => 'Test Blog', + 'original_source' => 'site', ), $data ); @@ -164,23 +166,25 @@ public function test_get_items() { $this->assertSame( array( - 'id' => 'emptytheme//my_template', - 'theme' => 'emptytheme', - 'slug' => 'my_template', - 'source' => 'custom', - 'origin' => null, - 'type' => 'wp_template', - 'description' => 'Description of my template.', - 'title' => array( + 'id' => 'emptytheme//my_template', + 'theme' => 'emptytheme', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( 'raw' => 'My Template', 'rendered' => 'My Template', ), - 'status' => 'publish', - 'wp_id' => self::$post->ID, - 'has_theme_file' => false, - 'is_custom' => true, - 'author' => 0, - 'modified' => mysql_to_rfc3339( self::$post->post_modified ), + 'status' => 'publish', + 'wp_id' => self::$post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => mysql_to_rfc3339( self::$post->post_modified ), + 'author_text' => 'Test Blog', + 'original_source' => 'site', ), $this->find_and_normalize_template_by_id( $data, 'emptytheme//my_template' ) ); @@ -225,27 +229,31 @@ public function test_create_item() { unset( $data['_links'] ); unset( $data['wp_id'] ); + $author_name = get_user_by( 'id', self::$admin_id )->get( 'display_name' ); + $this->assertSame( array( - 'id' => 'emptytheme//my_custom_template', - 'theme' => 'emptytheme', - 'content' => array( + 'id' => 'emptytheme//my_custom_template', + 'theme' => 'emptytheme', + 'content' => array( 'raw' => 'Content', ), - 'slug' => 'my_custom_template', - 'source' => 'custom', - 'origin' => null, - 'type' => 'wp_template', - 'description' => 'Just a description', - 'title' => array( + 'slug' => 'my_custom_template', + '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' => true, - 'author' => self::$admin_id, - 'modified' => $data['modified'], + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => self::$admin_id, + 'modified' => $data['modified'], + 'author_text' => $author_name, + 'original_source' => 'user', ), $data );