From aeb2be60c494e1b607c8aa91429c667ec4291e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:05:05 +0100 Subject: [PATCH 01/11] Site Editor > Templates: move view config to server --- ...enberg-rest-view-config-controller-7-1.php | 169 ++++++++++++++++++ .../components/page-templates/index-legacy.js | 25 +-- .../content-legacy.js | 86 ++++----- .../site-editor-routes/templates.js | 19 +- 4 files changed, 231 insertions(+), 68 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index 35c88e46751142..6e54d12031ecfe 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -122,6 +122,83 @@ public function get_items( $request ) { } elseif ( 'postType' === $kind && 'wp_template_part' === $name ) { $default_layouts = $this->get_default_layouts_for_wp_template_part(); $default_view = $this->get_default_view_for_wp_template_part( $default_layouts ); + } elseif ( 'postType' === $kind && 'wp_template' === $name ) { + $default_view = array( + 'type' => 'grid', + 'perPage' => 20, + 'sort' => array( + 'field' => 'title', + 'direction' => 'asc', + ), + 'titleField' => 'title', + 'descriptionField' => 'description', + 'mediaField' => 'preview', + 'fields' => array( 'author', 'active', 'slug', 'theme' ), + 'filters' => array(), + 'showMedia' => true, + ); + $default_layouts = array( + 'table' => array( 'showMedia' => false ), + 'grid' => array( 'showMedia' => true ), + 'list' => array( 'showMedia' => false ), + ); + $view_list = array( + array( + 'title' => __( 'All templates', 'gutenberg' ), + 'slug' => 'all', + ), + ); + + // Add unique author-based items from registered templates. + $registered_templates = gutenberg_get_registered_block_templates( array() ); + $seen_authors = array(); + foreach ( $registered_templates as $template ) { + $original_source = self::get_template_original_source( $template ); + $author_text = self::get_template_author_text( $template, $original_source ); + if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { + $seen_authors[ $author_text ] = true; + $view_list[] = array( + 'title' => $author_text, + 'slug' => $author_text, + 'icon' => $original_source, + 'view' => array( + 'filters' => array( + array( + 'field' => 'author', + 'operator' => 'is', + 'value' => $author_text, + 'isLocked' => true, + ), + ), + ), + ); + } + } + + // User-created DB templates. + $db_templates = get_block_templates( array(), 'wp_template' ); + foreach ( $db_templates as $template ) { + $original_source = self::get_template_original_source( $template ); + $author_text = self::get_template_author_text( $template, $original_source ); + if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { + $seen_authors[ $author_text ] = true; + $view_list[] = array( + 'title' => $author_text, + 'slug' => $author_text, + 'icon' => $original_source, + 'view' => array( + 'filters' => array( + array( + 'field' => 'author', + 'operator' => 'is', + 'value' => $author_text, + 'isLocked' => true, + ), + ), + ), + ); + } + } } $response = array( @@ -249,6 +326,9 @@ public function get_item_schema() { 'slug' => array( 'type' => 'string', ), + 'icon' => array( + 'type' => 'string', + ), 'view' => array( 'type' => 'object', 'properties' => array_merge( @@ -675,4 +755,93 @@ private function get_default_view_for_wp_template_part( $default_layouts ) { 'layout' => $default_layouts['grid']['layout'], ); } + + /** + * Returns the original source of a template. + * + * @param WP_Block_Template $template_object Template instance. + * @return string The original source ('theme', 'plugin', 'site', or 'user'). + */ + private static function get_template_original_source( $template_object ) { + if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { + 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'; + } + + if ( 'plugin' === $template_object->origin ) { + return 'plugin'; + } + + if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { + return 'site'; + } + } + + return 'user'; + } + + /** + * Returns a human readable text for the author of a template. + * + * @param WP_Block_Template $template_object Template instance. + * @param string $original_source The original source of the template. + * @return string Human readable text for the author. + */ + private static function get_template_author_text( $template_object, $original_source ) { + 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': + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + if ( isset( $template_object->plugin ) ) { + $plugins = wp_get_active_and_valid_plugins(); + + foreach ( $plugins as $plugin_file ) { + $plugin_basename = plugin_basename( $plugin_file ); + list( $plugin_slug, ) = explode( '/', $plugin_basename ); + + if ( $plugin_slug === $template_object->plugin ) { + $plugin_data = get_plugin_data( $plugin_file ); + + if ( ! empty( $plugin_data['Name'] ) ) { + return $plugin_data['Name']; + } + + break; + } + } + } + + $plugins = get_plugins(); + $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); + if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { + return $plugins[ $plugin_basename ]['Name']; + } + return $template_object->plugin ?? $template_object->theme; + case 'site': + return get_bloginfo( 'name' ); + case 'user': + $author = get_user_by( 'id', $template_object->author ); + if ( ! $author ) { + return __( 'Unknown author', 'gutenberg' ); + } + return $author->get( 'display_name' ); + } + + return ''; + } } diff --git a/packages/edit-site/src/components/page-templates/index-legacy.js b/packages/edit-site/src/components/page-templates/index-legacy.js index 1eb1ffd858d717..7b407f9bee955c 100644 --- a/packages/edit-site/src/components/page-templates/index-legacy.js +++ b/packages/edit-site/src/components/page-templates/index-legacy.js @@ -10,7 +10,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { addQueryArgs } from '@wordpress/url'; import { useEvent } from '@wordpress/compose'; -import { useView } from '@wordpress/views'; +import { useView, useViewConfig } from '@wordpress/views'; /** * Internal dependencies @@ -20,11 +20,6 @@ import { TEMPLATE_POST_TYPE } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; import { useEditPostAction } from '../dataviews-actions'; import { authorField, descriptionField, previewField } from './fields'; -import { - defaultLayouts, - DEFAULT_VIEW, - getActiveViewOverridesForTab, -} from './view-utils'; const { usePostActions, templateTitleField } = unlock( editorPrivateApis ); const { useHistory, useLocation } = unlock( routerPrivateApis ); @@ -32,13 +27,20 @@ const { useEntityRecordsWithPermissions } = unlock( corePrivateApis ); export default function PageTemplates() { const { path, query } = useLocation(); - const { activeView = 'active', postId } = query; + const { activeView = 'all', postId } = query; const [ selection, setSelection ] = useState( [ postId ] ); - const defaultView = DEFAULT_VIEW; + const { + default_view: defaultView, + default_layouts: defaultLayouts, + view_list: viewList, + } = useViewConfig( { + kind: 'postType', + name: TEMPLATE_POST_TYPE, + } ); const activeViewOverrides = useMemo( - () => getActiveViewOverridesForTab( activeView ), - [ activeView ] + () => viewList?.find( ( v ) => v.slug === activeView )?.view ?? {}, + [ viewList, activeView ] ); const { view, updateView, isModified, resetToDefault } = useView( { kind: 'postType', @@ -46,6 +48,7 @@ export default function PageTemplates() { slug: 'default', defaultView, activeViewOverrides, + defaultLayouts, queryParams: { page: query.pageNumber, search: query.search, @@ -150,7 +153,7 @@ export default function PageTemplates() { history.navigate( `/wp_template/${ id }?canvas=edit` ); } } selection={ selection } - defaultLayouts={ defaultLayouts } + defaultLayouts={ defaultLayouts ?? {} } onReset={ isModified ? () => { diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js index 7920d49a43c8cd..e827b55986c484 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js @@ -1,79 +1,63 @@ /** * WordPress dependencies */ -import { useEntityRecords } from '@wordpress/core-data'; -import { useMemo } from '@wordpress/element'; import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { addQueryArgs } from '@wordpress/url'; +import { useViewConfig } from '@wordpress/views'; +import { + commentAuthorAvatar, + layout, + plugins as pluginIcon, + globe, +} from '@wordpress/icons'; /** * Internal dependencies */ import SidebarNavigationItem from '../sidebar-navigation-item'; -import { useAddedBy } from '../page-templates/hooks'; -import { layout } from '@wordpress/icons'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); -const EMPTY_ARRAY = []; - -function TemplateDataviewItem( { template, isActive } ) { - const { text, icon } = useAddedBy( template.type, template.id ); - - return ( - - { text } - - ); -} +const SLUG_TO_ICON = { + all: layout, + user: commentAuthorAvatar, + theme: layout, + plugin: pluginIcon, + site: globe, +}; export default function DataviewsTemplatesSidebarContent() { const { query: { activeView = 'all' }, } = useLocation(); - const { records } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { - per_page: -1, + const { view_list: viewList } = useViewConfig( { + kind: 'postType', + name: TEMPLATE_POST_TYPE, } ); - const firstItemPerAuthorText = useMemo( () => { - const firstItemPerAuthor = records?.reduce( ( acc, template ) => { - const author = template.author_text; - if ( author && ! acc[ author ] ) { - acc[ author ] = template; - } - return acc; - }, {} ); - return ( - ( firstItemPerAuthor && Object.values( firstItemPerAuthor ) ) ?? - EMPTY_ARRAY - ); - }, [ records ] ); return ( - - { __( 'All templates' ) } - - { firstItemPerAuthorText.map( ( template ) => { - return ( - - ); - } ) } + { viewList?.map( ( item ) => ( + + { item.title } + + ) ) } ); } diff --git a/packages/edit-site/src/components/site-editor-routes/templates.js b/packages/edit-site/src/components/site-editor-routes/templates.js index 51636d044adda0..1225bbc59743b2 100644 --- a/packages/edit-site/src/components/site-editor-routes/templates.js +++ b/packages/edit-site/src/components/site-editor-routes/templates.js @@ -1,6 +1,8 @@ /** * WordPress dependencies */ +import { resolveSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; import { loadView } from '@wordpress/views'; /** @@ -11,19 +13,24 @@ import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen import SidebarNavigationScreenUnsupported from '../sidebar-navigation-screen-unsupported'; import PageTemplates from '../page-templates'; import PageTemplatesLegacy from '../page-templates/index-legacy'; -import { - DEFAULT_VIEW, - getActiveViewOverridesForTab, -} from '../page-templates/view-utils'; +import { unlock } from '../../lock-unlock'; async function isTemplateListView( query ) { const { activeView = 'active' } = query; + const config = await unlock( resolveSelect( coreStore ) ).getViewConfig( + 'postType', + 'wp_template' + ); + const defaultView = config?.default_view; + const viewEntry = config?.view_list?.find( ( v ) => v.slug === activeView ); + // For author-based views, no overrides needed for type determination. + const activeViewOverrides = viewEntry?.view ?? {}; const view = await loadView( { kind: 'postType', name: 'wp_template', slug: 'default', - defaultView: DEFAULT_VIEW, - activeViewOverrides: getActiveViewOverridesForTab( activeView ), + defaultView, + activeViewOverrides, } ); return view.type === 'list'; } From 979be83b374da0edf83a2831ff136c914430d52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:14:55 +0100 Subject: [PATCH 02/11] simplify --- .../edit-site/src/components/site-editor-routes/templates.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/site-editor-routes/templates.js b/packages/edit-site/src/components/site-editor-routes/templates.js index 1225bbc59743b2..eb6f49d524ef53 100644 --- a/packages/edit-site/src/components/site-editor-routes/templates.js +++ b/packages/edit-site/src/components/site-editor-routes/templates.js @@ -22,9 +22,8 @@ async function isTemplateListView( query ) { 'wp_template' ); const defaultView = config?.default_view; - const viewEntry = config?.view_list?.find( ( v ) => v.slug === activeView ); - // For author-based views, no overrides needed for type determination. - const activeViewOverrides = viewEntry?.view ?? {}; + const activeViewOverrides = + config?.view_list?.find( ( v ) => v.slug === activeView ).view ?? {}; const view = await loadView( { kind: 'postType', name: 'wp_template', From 28455038c43e82e9cbf9563a6d747567c8b909e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:17:36 +0100 Subject: [PATCH 03/11] Icon --- ...enberg-rest-view-config-controller-7-1.php | 5 ---- .../content-legacy.js | 29 ++++++++++++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index 6e54d12031ecfe..4753526d7ca712 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -160,7 +160,6 @@ public function get_items( $request ) { $view_list[] = array( 'title' => $author_text, 'slug' => $author_text, - 'icon' => $original_source, 'view' => array( 'filters' => array( array( @@ -185,7 +184,6 @@ public function get_items( $request ) { $view_list[] = array( 'title' => $author_text, 'slug' => $author_text, - 'icon' => $original_source, 'view' => array( 'filters' => array( array( @@ -326,9 +324,6 @@ public function get_item_schema() { 'slug' => array( 'type' => 'string', ), - 'icon' => array( - 'type' => 'string', - ), 'view' => array( 'type' => 'object', 'properties' => array_merge( diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js index e827b55986c484..60d7ff4e06f0b6 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js @@ -2,6 +2,8 @@ * WordPress dependencies */ import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { addQueryArgs } from '@wordpress/url'; import { useViewConfig } from '@wordpress/views'; @@ -21,7 +23,7 @@ import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); -const SLUG_TO_ICON = { +const SOURCE_TO_ICON = { all: layout, user: commentAuthorAvatar, theme: layout, @@ -37,6 +39,27 @@ export default function DataviewsTemplatesSidebarContent() { kind: 'postType', name: TEMPLATE_POST_TYPE, } ); + const authorSourceMap = useSelect( ( select ) => { + const templates = select( coreStore ).getEntityRecords( + 'postType', + TEMPLATE_POST_TYPE, + { per_page: -1 } + ); + if ( ! templates ) { + return {}; + } + const map = {}; + for ( const template of templates ) { + if ( + template.author_text && + template.original_source && + ! map[ template.author_text ] + ) { + map[ template.author_text ] = template.original_source; + } + } + return map; + }, [] ); return ( @@ -50,9 +73,7 @@ export default function DataviewsTemplatesSidebarContent() { activeView: item.slug, } ) } - icon={ - SLUG_TO_ICON[ item.icon ] || SLUG_TO_ICON[ item.slug ] - } + icon={ SOURCE_TO_ICON[ authorSourceMap[ item.slug ] ] } aria-current={ activeView === item.slug } > { item.title } From 5a2d4ee2f5853aab606f05a9cf937572da20d438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:25:53 +0100 Subject: [PATCH 04/11] Fix typo --- .../edit-site/src/components/site-editor-routes/templates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/site-editor-routes/templates.js b/packages/edit-site/src/components/site-editor-routes/templates.js index eb6f49d524ef53..fdde0b7387697a 100644 --- a/packages/edit-site/src/components/site-editor-routes/templates.js +++ b/packages/edit-site/src/components/site-editor-routes/templates.js @@ -23,7 +23,7 @@ async function isTemplateListView( query ) { ); const defaultView = config?.default_view; const activeViewOverrides = - config?.view_list?.find( ( v ) => v.slug === activeView ).view ?? {}; + config?.view_list?.find( ( v ) => v.slug === activeView )?.view ?? {}; const view = await loadView( { kind: 'postType', name: 'wp_template', From 35d4391f04045c8821637c9c6cbd10f4a2eecd72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:26:38 +0100 Subject: [PATCH 05/11] Account for unknown source/all --- .../content-legacy.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js index 60d7ff4e06f0b6..706e05b09b2900 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content-legacy.js @@ -24,7 +24,6 @@ import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); const SOURCE_TO_ICON = { - all: layout, user: commentAuthorAvatar, theme: layout, plugin: pluginIcon, @@ -73,7 +72,9 @@ export default function DataviewsTemplatesSidebarContent() { activeView: item.slug, } ) } - icon={ SOURCE_TO_ICON[ authorSourceMap[ item.slug ] ] } + icon={ + SOURCE_TO_ICON[ authorSourceMap[ item.slug ] ] ?? layout + } aria-current={ activeView === item.slug } > { item.title } From 67cbb1000adcb561b941eff6d7f2d1adfe82f808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:42:01 +0100 Subject: [PATCH 06/11] Add backport note --- backport-changelog/7.1/11272.md | 1 + 1 file changed, 1 insertion(+) diff --git a/backport-changelog/7.1/11272.md b/backport-changelog/7.1/11272.md index 7237a0a5a4e2da..d615b1c64a32a2 100644 --- a/backport-changelog/7.1/11272.md +++ b/backport-changelog/7.1/11272.md @@ -2,3 +2,4 @@ https://github.com/WordPress/wordpress-develop/pull/11272 * https://github.com/WordPress/gutenberg/pull/76573 * https://github.com/WordPress/gutenberg/pull/76734 +* https://github.com/WordPress/gutenberg/pull/76622 From d4cf7dbc4c63d6cc84bd26b3a000495e26f58ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:25:25 +0100 Subject: [PATCH 07/11] Extract to method --- ...enberg-rest-view-config-controller-7-1.php | 162 ++++++++++-------- 1 file changed, 88 insertions(+), 74 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index 4753526d7ca712..b3616027a7965b 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -123,80 +123,9 @@ public function get_items( $request ) { $default_layouts = $this->get_default_layouts_for_wp_template_part(); $default_view = $this->get_default_view_for_wp_template_part( $default_layouts ); } elseif ( 'postType' === $kind && 'wp_template' === $name ) { - $default_view = array( - 'type' => 'grid', - 'perPage' => 20, - 'sort' => array( - 'field' => 'title', - 'direction' => 'asc', - ), - 'titleField' => 'title', - 'descriptionField' => 'description', - 'mediaField' => 'preview', - 'fields' => array( 'author', 'active', 'slug', 'theme' ), - 'filters' => array(), - 'showMedia' => true, - ); - $default_layouts = array( - 'table' => array( 'showMedia' => false ), - 'grid' => array( 'showMedia' => true ), - 'list' => array( 'showMedia' => false ), - ); - $view_list = array( - array( - 'title' => __( 'All templates', 'gutenberg' ), - 'slug' => 'all', - ), - ); - - // Add unique author-based items from registered templates. - $registered_templates = gutenberg_get_registered_block_templates( array() ); - $seen_authors = array(); - foreach ( $registered_templates as $template ) { - $original_source = self::get_template_original_source( $template ); - $author_text = self::get_template_author_text( $template, $original_source ); - if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { - $seen_authors[ $author_text ] = true; - $view_list[] = array( - 'title' => $author_text, - 'slug' => $author_text, - 'view' => array( - 'filters' => array( - array( - 'field' => 'author', - 'operator' => 'is', - 'value' => $author_text, - 'isLocked' => true, - ), - ), - ), - ); - } - } - - // User-created DB templates. - $db_templates = get_block_templates( array(), 'wp_template' ); - foreach ( $db_templates as $template ) { - $original_source = self::get_template_original_source( $template ); - $author_text = self::get_template_author_text( $template, $original_source ); - if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { - $seen_authors[ $author_text ] = true; - $view_list[] = array( - 'title' => $author_text, - 'slug' => $author_text, - 'view' => array( - 'filters' => array( - array( - 'field' => 'author', - 'operator' => 'is', - 'value' => $author_text, - 'isLocked' => true, - ), - ), - ), - ); - } - } + $default_view = $this->get_default_view_for_wp_template(); + $default_layouts = $this->get_default_layouts_for_wp_template(); + $view_list = $this->get_view_list_for_wp_template(); } $response = array( @@ -839,4 +768,89 @@ private static function get_template_author_text( $template_object, $original_so return ''; } + + private function get_default_view_for_wp_template() { + return array( + 'type' => 'grid', + 'perPage' => 20, + 'sort' => array( + 'field' => 'title', + 'direction' => 'asc', + ), + 'titleField' => 'title', + 'descriptionField' => 'description', + 'mediaField' => 'preview', + 'fields' => array( 'author', 'active', 'slug', 'theme' ), + 'filters' => array(), + 'showMedia' => true, + ); + } + + private function get_default_layouts_for_wp_template() { + return array( + 'table' => array( 'showMedia' => false ), + 'grid' => array( 'showMedia' => true ), + 'list' => array( 'showMedia' => false ), + ); + } + + private function get_view_list_for_wp_template() { + $view_list = array( + array( + 'title' => __( 'All templates', 'gutenberg' ), + 'slug' => 'all', + ), + ); + + // Add unique author-based items from registered templates. + $registered_templates = gutenberg_get_registered_block_templates( array() ); + $seen_authors = array(); + foreach ( $registered_templates as $template ) { + $original_source = self::get_template_original_source( $template ); + $author_text = self::get_template_author_text( $template, $original_source ); + if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { + $seen_authors[ $author_text ] = true; + $view_list[] = array( + 'title' => $author_text, + 'slug' => $author_text, + 'view' => array( + 'filters' => array( + array( + 'field' => 'author', + 'operator' => 'is', + 'value' => $author_text, + 'isLocked' => true, + ), + ), + ), + ); + } + } + + // User-created DB templates. + $db_templates = get_block_templates( array(), 'wp_template' ); + foreach ( $db_templates as $template ) { + $original_source = self::get_template_original_source( $template ); + $author_text = self::get_template_author_text( $template, $original_source ); + if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { + $seen_authors[ $author_text ] = true; + $view_list[] = array( + 'title' => $author_text, + 'slug' => $author_text, + 'view' => array( + 'filters' => array( + array( + 'field' => 'author', + 'operator' => 'is', + 'value' => $author_text, + 'isLocked' => true, + ), + ), + ), + ); + } + } + + return $view_list; + } } From c01e1da6a65291e570fa3e7240cd7955a8a9e7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:00:46 +0100 Subject: [PATCH 08/11] Rename for clarity: it matches original name --- ...ass-gutenberg-rest-view-config-controller-7-1.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index b3616027a7965b..1a716acaf6b63f 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -686,7 +686,7 @@ private function get_default_view_for_wp_template_part( $default_layouts ) { * @param WP_Block_Template $template_object Template instance. * @return string The original source ('theme', 'plugin', 'site', or 'user'). */ - private static function get_template_original_source( $template_object ) { + private static function get_wp_templates_original_source_field( $template_object ) { if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { if ( $template_object->has_theme_file && ( 'theme' === $template_object->origin || ( @@ -722,7 +722,7 @@ private static function get_template_original_source( $template_object ) { * @param string $original_source The original source of the template. * @return string Human readable text for the author. */ - private static function get_template_author_text( $template_object, $original_source ) { + private static function get_wp_templates_author_text_field( $template_object, $original_source ) { switch ( $original_source ) { case 'theme': $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); @@ -806,8 +806,8 @@ private function get_view_list_for_wp_template() { $registered_templates = gutenberg_get_registered_block_templates( array() ); $seen_authors = array(); foreach ( $registered_templates as $template ) { - $original_source = self::get_template_original_source( $template ); - $author_text = self::get_template_author_text( $template, $original_source ); + $original_source = self::get_wp_templates_original_source_field( $template ); + $author_text = self::get_wp_templates_author_text_field( $template, $original_source ); if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { $seen_authors[ $author_text ] = true; $view_list[] = array( @@ -830,8 +830,8 @@ private function get_view_list_for_wp_template() { // User-created DB templates. $db_templates = get_block_templates( array(), 'wp_template' ); foreach ( $db_templates as $template ) { - $original_source = self::get_template_original_source( $template ); - $author_text = self::get_template_author_text( $template, $original_source ); + $original_source = self::get_wp_templates_original_source_field( $template ); + $author_text = self::get_wp_templates_author_text_field( $template, $original_source ); if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { $seen_authors[ $author_text ] = true; $view_list[] = array( From 6ad2858643bcd3b72f5b45da4a022d423671ceb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:20:44 +0100 Subject: [PATCH 09/11] Call REST templates endpoint --- ...enberg-rest-view-config-controller-7-1.php | 128 ++---------------- 1 file changed, 11 insertions(+), 117 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index 1a716acaf6b63f..641bf12464c582 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -680,95 +680,6 @@ private function get_default_view_for_wp_template_part( $default_layouts ) { ); } - /** - * Returns the original source of a template. - * - * @param WP_Block_Template $template_object Template instance. - * @return string The original source ('theme', 'plugin', 'site', or 'user'). - */ - private static function get_wp_templates_original_source_field( $template_object ) { - if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { - 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'; - } - - if ( 'plugin' === $template_object->origin ) { - return 'plugin'; - } - - if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { - return 'site'; - } - } - - return 'user'; - } - - /** - * Returns a human readable text for the author of a template. - * - * @param WP_Block_Template $template_object Template instance. - * @param string $original_source The original source of the template. - * @return string Human readable text for the author. - */ - private static function get_wp_templates_author_text_field( $template_object, $original_source ) { - 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': - if ( ! function_exists( 'get_plugins' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - if ( isset( $template_object->plugin ) ) { - $plugins = wp_get_active_and_valid_plugins(); - - foreach ( $plugins as $plugin_file ) { - $plugin_basename = plugin_basename( $plugin_file ); - list( $plugin_slug, ) = explode( '/', $plugin_basename ); - - if ( $plugin_slug === $template_object->plugin ) { - $plugin_data = get_plugin_data( $plugin_file ); - - if ( ! empty( $plugin_data['Name'] ) ) { - return $plugin_data['Name']; - } - - break; - } - } - } - - $plugins = get_plugins(); - $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); - if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { - return $plugins[ $plugin_basename ]['Name']; - } - return $template_object->plugin ?? $template_object->theme; - case 'site': - return get_bloginfo( 'name' ); - case 'user': - $author = get_user_by( 'id', $template_object->author ); - if ( ! $author ) { - return __( 'Unknown author', 'gutenberg' ); - } - return $author->get( 'display_name' ); - } - - return ''; - } - private function get_default_view_for_wp_template() { return array( 'type' => 'grid', @@ -802,36 +713,19 @@ private function get_view_list_for_wp_template() { ), ); - // Add unique author-based items from registered templates. - $registered_templates = gutenberg_get_registered_block_templates( array() ); - $seen_authors = array(); - foreach ( $registered_templates as $template ) { - $original_source = self::get_wp_templates_original_source_field( $template ); - $author_text = self::get_wp_templates_author_text_field( $template, $original_source ); - if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { - $seen_authors[ $author_text ] = true; - $view_list[] = array( - 'title' => $author_text, - 'slug' => $author_text, - 'view' => array( - 'filters' => array( - array( - 'field' => 'author', - 'operator' => 'is', - 'value' => $author_text, - 'isLocked' => true, - ), - ), - ), - ); - } + // Fetch all templates via the REST API to get computed author_text values. + $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); + $request->set_param( '_fields', 'author_text' ); + $response = rest_do_request( $request ); + + if ( $response->is_error() ) { + return $view_list; } - // User-created DB templates. - $db_templates = get_block_templates( array(), 'wp_template' ); - foreach ( $db_templates as $template ) { - $original_source = self::get_wp_templates_original_source_field( $template ); - $author_text = self::get_wp_templates_author_text_field( $template, $original_source ); + $templates = $response->get_data(); + $seen_authors = array(); + foreach ( $templates as $template ) { + $author_text = $template['author_text'] ?? ''; if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { $seen_authors[ $author_text ] = true; $view_list[] = array( From 242eee5ee003e4e58ed15381c5a89029169bc44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:25:51 +0100 Subject: [PATCH 10/11] List registered sources first --- ...enberg-rest-view-config-controller-7-1.php | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index 641bf12464c582..3f7fce3b003491 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -715,20 +715,26 @@ private function get_view_list_for_wp_template() { // Fetch all templates via the REST API to get computed author_text values. $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); - $request->set_param( '_fields', 'author_text' ); + $request->set_param( '_fields', 'author_text,original_source' ); $response = rest_do_request( $request ); if ( $response->is_error() ) { return $view_list; } - $templates = $response->get_data(); - $seen_authors = array(); + $templates = $response->get_data(); + + // Collect unique authors, tracking whether they come from a registered + // source (theme, plugin, site) so we can sort those before user ones. + $seen_authors = array(); + $registered_authors = array(); + $user_authors = array(); foreach ( $templates as $template ) { - $author_text = $template['author_text'] ?? ''; + $author_text = $template['author_text'] ?? ''; + $original_source = $template['original_source'] ?? 'user'; if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { $seen_authors[ $author_text ] = true; - $view_list[] = array( + $entry = array( 'title' => $author_text, 'slug' => $author_text, 'view' => array( @@ -742,9 +748,17 @@ private function get_view_list_for_wp_template() { ), ), ); + if ( 'user' === $original_source ) { + $user_authors[] = $entry; + } else { + $registered_authors[] = $entry; + } } } + // Registered sources (theme, plugin, site) first, then user-created. + $view_list = array_merge( $view_list, $registered_authors, $user_authors ); + return $view_list; } } From 015e3ddc5e3721ac0e1f8689b3e070caff360480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Maneiro?= <583546+oandregal@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:35:35 +0100 Subject: [PATCH 11/11] Use get_block_templates directly --- ...enberg-rest-view-config-controller-7-1.php | 125 ++++++++++++++++-- 1 file changed, 112 insertions(+), 13 deletions(-) diff --git a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php index 3f7fce3b003491..6146aa42ce2854 100644 --- a/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php +++ b/lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php @@ -680,6 +680,115 @@ private function get_default_view_for_wp_template_part( $default_layouts ) { ); } + /** + * Returns the original source of a template. + * + * @param WP_Block_Template $template_object Template instance. + * @return string The original source ('theme', 'plugin', 'site', or 'user'). + */ + private static function 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 ( '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'; + } + + /** + * Returns a human readable text for the author of a template. + * + * @param WP_Block_Template $template_object Template instance. + * @return string Human readable text for the author. + */ + private static function get_wp_templates_author_text_field( $template_object ) { + $original_source = self::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': + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + if ( isset( $template_object->plugin ) ) { + $plugins = wp_get_active_and_valid_plugins(); + + foreach ( $plugins as $plugin_file ) { + $plugin_basename = plugin_basename( $plugin_file ); + list( $plugin_slug, ) = explode( '/', $plugin_basename ); + + if ( $plugin_slug === $template_object->plugin ) { + $plugin_data = get_plugin_data( $plugin_file ); + + if ( ! empty( $plugin_data['Name'] ) ) { + return $plugin_data['Name']; + } + + break; + } + } + } + + /* + * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards + * compatibility with templates that were registered before the plugin attribute was added. + */ + $plugins = get_plugins(); + $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); + if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { + return $plugins[ $plugin_basename ]['Name']; + } + return $template_object->plugin ?? $template_object->theme; + case 'site': + return get_bloginfo( 'name' ); + case 'user': + $author = get_user_by( 'id', $template_object->author ); + if ( ! $author ) { + return __( 'Unknown author', 'gutenberg' ); + } + return $author->get( 'display_name' ); + } + + // Fail-safe to return a string should the original source ever fall through. + return ''; + } + private function get_default_view_for_wp_template() { return array( 'type' => 'grid', @@ -713,16 +822,7 @@ private function get_view_list_for_wp_template() { ), ); - // Fetch all templates via the REST API to get computed author_text values. - $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); - $request->set_param( '_fields', 'author_text,original_source' ); - $response = rest_do_request( $request ); - - if ( $response->is_error() ) { - return $view_list; - } - - $templates = $response->get_data(); + $templates = get_block_templates( array(), 'wp_template' ); // Collect unique authors, tracking whether they come from a registered // source (theme, plugin, site) so we can sort those before user ones. @@ -730,8 +830,8 @@ private function get_view_list_for_wp_template() { $registered_authors = array(); $user_authors = array(); foreach ( $templates as $template ) { - $author_text = $template['author_text'] ?? ''; - $original_source = $template['original_source'] ?? 'user'; + $original_source = self::get_wp_templates_original_source_field( $template ); + $author_text = self::get_wp_templates_author_text_field( $template ); if ( ! empty( $author_text ) && ! isset( $seen_authors[ $author_text ] ) ) { $seen_authors[ $author_text ] = true; $entry = array( @@ -756,7 +856,6 @@ private function get_view_list_for_wp_template() { } } - // Registered sources (theme, plugin, site) first, then user-created. $view_list = array_merge( $view_list, $registered_authors, $user_authors ); return $view_list;