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
);