diff --git a/packages/edit-site/src/components/page-templates/hooks.js b/packages/edit-site/src/components/page-templates/hooks.js
index 3cf2d78a59ee99..3b4e5e445caaa8 100644
--- a/packages/edit-site/src/components/page-templates/hooks.js
+++ b/packages/edit-site/src/components/page-templates/hooks.js
@@ -98,3 +98,31 @@ export function useAddedBy( postType, postId ) {
[ postType, postId ]
);
}
+
+/**
+ * Returns the template part title.
+ *
+ * @param {?string} slug The template part slug.
+ * @return {?string} The template part title.
+ */
+export function useTemplatePartTitle( slug ) {
+ return useSelect(
+ ( select ) => {
+ const { getEntityRecord, getCurrentTheme } = select( coreStore );
+ const theme = getCurrentTheme()?.stylesheet;
+ if ( ! theme ) {
+ return;
+ }
+ const templatePart = getEntityRecord(
+ 'postType',
+ 'wp_template_part',
+ `${ theme }//${ slug }`
+ );
+ if ( ! templatePart ) {
+ return;
+ }
+ return templatePart.title?.rendered;
+ },
+ [ slug ]
+ );
+}
diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js
index e0e04e0f5da924..971d4466340624 100644
--- a/packages/edit-site/src/components/page-templates/index.js
+++ b/packages/edit-site/src/components/page-templates/index.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { useState, useMemo, useCallback, useEffect } from '@wordpress/element';
import { privateApis as corePrivateApis } from '@wordpress/core-data';
import { DataViews, filterSortAndPaginate } from '@wordpress/dataviews';
@@ -27,7 +27,10 @@ import { useEditPostAction } from '../dataviews-actions';
import { authorField, descriptionField, previewField } from './fields';
import { useEvent } from '@wordpress/compose';
-const { usePostActions } = unlock( editorPrivateApis );
+import { useTemplatePartTitle } from './hooks';
+
+const { usePostActions, useTemplatesFilteredByTemplatePart } =
+ unlock( editorPrivateApis );
const { useHistory, useLocation } = unlock( routerPrivateApis );
const { useEntityRecordsWithPermissions } = unlock( corePrivateApis );
@@ -71,7 +74,7 @@ const DEFAULT_VIEW = {
export default function PageTemplates() {
const { path, query } = useLocation();
- const { activeView = 'all', layout, postId } = query;
+ const { activeView = 'all', layout, postId, usingTemplatePart } = query;
const [ selection, setSelection ] = useState( [ postId ] );
const defaultView = useMemo( () => {
@@ -119,10 +122,17 @@ export default function PageTemplates() {
} ) );
}, [ setView, activeView ] );
- const { records, isResolving: isLoadingData } =
+ const { records: unfilteredRecords, isResolving: isLoadingData } =
useEntityRecordsWithPermissions( 'postType', TEMPLATE_POST_TYPE, {
per_page: -1,
} );
+ const records = useTemplatesFilteredByTemplatePart(
+ usingTemplatePart,
+ unfilteredRecords
+ );
+
+ const templatePartTitle = useTemplatePartTitle( usingTemplatePart );
+
const history = useHistory();
const onChangeSelection = useCallback(
( items ) => {
@@ -189,12 +199,19 @@ export default function PageTemplates() {
);
}
} );
-
+ let title = __( 'Templates' );
+ if ( usingTemplatePart && templatePartTitle ) {
+ /* translators: %s: template part title */
+ title = sprintf(
+ 'Templates using %s template part.',
+ templatePartTitle
+ );
+ }
return (
}
+ title={ title }
+ actions={ usingTemplatePart ? undefined : }
>
+ { createInterpolateElement(
+ sprintf(
+ /* translators: 1: number of templates. */
+ _n(
+ 'Used by %1$s template.',
+ 'Used by %1$s templates.',
+ filteredTemplates.length
+ ),
+ filteredTemplates.length
+ ),
+ {
+ Link: (
+
+ ),
+ }
+ ) }
+
+ );
+}
+
+export default function PostUsedByPanel() {
+ const { postType, slug } = useSelect( ( select ) => {
+ select( editorStore ).getCurrentPostType();
+ return {
+ postType: select( editorStore ).getCurrentPostType(),
+ slug: select( editorStore ).getCurrentPostAttribute( 'slug' ),
+ };
+ }, [] );
+ if ( postType !== TEMPLATE_PART_POST_TYPE ) {
+ return null;
+ }
+ return (
+
+ );
+}
diff --git a/packages/editor/src/components/sidebar/post-summary.js b/packages/editor/src/components/sidebar/post-summary.js
index 3539f7ba964ec7..b1ca20e661e026 100644
--- a/packages/editor/src/components/sidebar/post-summary.js
+++ b/packages/editor/src/components/sidebar/post-summary.js
@@ -29,6 +29,7 @@ import SiteDiscussion from '../site-discussion';
import { store as editorStore } from '../../store';
import { PrivatePostLastRevision } from '../post-last-revision';
import PostTrash from '../post-trash';
+import PostUsedByPanel from '../post-used-by-panel';
/**
* Module Constants
@@ -70,6 +71,7 @@ export default function PostSummary( { onActionPerformed } ) {
+
{ ! isRemovedPostStatusPanel && (
diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js
index 11083eb6ab8a45..283cc3fd774707 100644
--- a/packages/editor/src/private-apis.js
+++ b/packages/editor/src/private-apis.js
@@ -26,6 +26,7 @@ import {
import { CreateTemplatePartModal } from '@wordpress/fields';
import { registerCoreBlockBindingsSources } from './bindings/api';
import { getTemplateInfo } from './utils/get-template-info';
+import useTemplatesFilteredByTemplatePart from './utils/use-templates-filtered-by-template-part';
const { store: interfaceStore, ...remainingInterfaceApis } = interfaceApis;
@@ -51,4 +52,5 @@ lock( privateApis, {
// This is a temporary private API while we're updating the site editor to use EditorProvider.
interfaceStore,
...remainingInterfaceApis,
+ useTemplatesFilteredByTemplatePart,
} );
diff --git a/packages/editor/src/utils/use-templates-filtered-by-template-part.js b/packages/editor/src/utils/use-templates-filtered-by-template-part.js
new file mode 100644
index 00000000000000..0dece943a6761f
--- /dev/null
+++ b/packages/editor/src/utils/use-templates-filtered-by-template-part.js
@@ -0,0 +1,60 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { parse } from '@wordpress/blocks';
+
+function findBlock( blocks, isMatch ) {
+ if ( ! Array.isArray( blocks ) ) {
+ return;
+ }
+ for ( const block of blocks ) {
+ if ( isMatch( block ) ) {
+ return block;
+ }
+ const childResult = findBlock( block.innerBlocks, isMatch );
+ if ( childResult ) {
+ return childResult;
+ }
+ }
+}
+
+export default function useTemplatesFilteredByTemplatePart(
+ templatePartSlug,
+ templates
+) {
+ const [ result, setResult ] = useState( [] );
+ // We are using a timeout and an effect just to make sure
+ // the parsing of templates is done asynchronously and does not blocks
+ // the rendering of the page.
+ useEffect( () => {
+ if ( ! templatePartSlug || ! templates ) {
+ return;
+ }
+ const timeoutId = setTimeout( () => {
+ setResult(
+ templates.filter( ( template ) => {
+ if ( template?.content?.raw ) {
+ const blocks = parse( template.content.raw );
+ return !! findBlock( blocks, ( block ) => {
+ return (
+ block.name === 'core/template-part' &&
+ block.attributes.slug === templatePartSlug
+ );
+ } );
+ }
+ return false;
+ } )
+ );
+ }, 0 );
+ return () => {
+ if ( timeoutId ) {
+ clearTimeout( timeoutId );
+ }
+ };
+ } );
+ if ( ! templatePartSlug || ! templates ) {
+ return templates;
+ }
+ return result;
+}