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'; }