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
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..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
@@ -122,6 +122,10 @@ 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 = $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(
@@ -675,4 +679,185 @@ 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_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',
+ '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',
+ ),
+ );
+
+ $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.
+ $seen_authors = array();
+ $registered_authors = array();
+ $user_authors = array();
+ foreach ( $templates as $template ) {
+ $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(
+ 'title' => $author_text,
+ 'slug' => $author_text,
+ 'view' => array(
+ 'filters' => array(
+ array(
+ 'field' => 'author',
+ 'operator' => 'is',
+ 'value' => $author_text,
+ 'isLocked' => true,
+ ),
+ ),
+ ),
+ );
+ if ( 'user' === $original_source ) {
+ $user_authors[] = $entry;
+ } else {
+ $registered_authors[] = $entry;
+ }
+ }
+ }
+
+ $view_list = array_merge( $view_list, $registered_authors, $user_authors );
+
+ return $view_list;
+ }
}
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..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
@@ -1,79 +1,85 @@
/**
* 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 { 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';
+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 SOURCE_TO_ICON = {
+ 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
+ const authorSourceMap = useSelect( ( select ) => {
+ const templates = select( coreStore ).getEntityRecords(
+ 'postType',
+ TEMPLATE_POST_TYPE,
+ { per_page: -1 }
);
- }, [ records ] );
+ 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 (
-
- { __( '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..fdde0b7387697a 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,23 @@ 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 activeViewOverrides =
+ config?.view_list?.find( ( v ) => v.slug === activeView )?.view ?? {};
const view = await loadView( {
kind: 'postType',
name: 'wp_template',
slug: 'default',
- defaultView: DEFAULT_VIEW,
- activeViewOverrides: getActiveViewOverridesForTab( activeView ),
+ defaultView,
+ activeViewOverrides,
} );
return view.type === 'list';
}