diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js
index a1811e85dba6a..ca9b3898d1d37 100644
--- a/packages/block-editor/src/components/inserter/menu.js
+++ b/packages/block-editor/src/components/inserter/menu.js
@@ -21,6 +21,7 @@ import InserterSearchForm from './search-form';
import InserterPreviewPanel from './preview-panel';
import InserterBlockList from './block-list';
import BlockPatterns from './block-patterns';
+import TemplateParts from './template-parts';
const stopKeyPropagation = ( event ) => event.stopPropagation();
@@ -41,6 +42,7 @@ function InserterMenu( {
getBlockIndex,
getBlockSelectionEnd,
getBlockOrder,
+ hasTemplateParts,
} = useSelect(
( select ) => {
const {
@@ -57,6 +59,8 @@ function InserterMenu( {
}
}
return {
+ hasTemplateParts: getSettings()
+ .__experimentalEnableFullSiteEditing,
hasPatterns: !! getSettings().__experimentalBlockPatterns
?.length,
destinationRootClientId: destRootClientId,
@@ -172,6 +176,36 @@ function InserterMenu( {
);
+ const templatePartsTab = (
+
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss
index f7557d631f2bd..4a4b786544ded 100644
--- a/packages/block-editor/src/components/inserter/style.scss
+++ b/packages/block-editor/src/components/inserter/style.scss
@@ -247,7 +247,8 @@ $block-inserter-tabs-height: 44px;
flex-shrink: 0;
}
-.block-editor-inserter__patterns-item {
+.block-editor-inserter__patterns-item,
+.block-editor-inserter__template-part-item {
border-radius: $radius-block-ui;
cursor: pointer;
margin-top: $grid-unit-20;
@@ -271,7 +272,8 @@ $block-inserter-tabs-height: 44px;
}
}
-.block-editor-inserter__patterns-item-title {
+.block-editor-inserter__patterns-item-title,
+.block-editor-inserter__template-part-item-title {
padding: $grid-unit-05;
font-size: 12px;
text-align: center;
diff --git a/packages/block-editor/src/components/inserter/template-parts.js b/packages/block-editor/src/components/inserter/template-parts.js
new file mode 100644
index 0000000000000..d3ffbfdc24b85
--- /dev/null
+++ b/packages/block-editor/src/components/inserter/template-parts.js
@@ -0,0 +1,197 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+import { parse, createBlock } from '@wordpress/blocks';
+import { useMemo, useCallback } from '@wordpress/element';
+import { ENTER, SPACE } from '@wordpress/keycodes';
+import { __, sprintf } from '@wordpress/i18n';
+/**
+ * Internal dependencies
+ */
+import BlockPreview from '../block-preview';
+import InserterPanel from './panel';
+import useAsyncList from './use-async-list';
+
+/**
+ * External dependencies
+ */
+import { groupBy } from 'lodash';
+
+function TemplatePartPlaceholder() {
+ return (
+
+ );
+}
+
+function TemplatePartItem( { templatePart, onInsert } ) {
+ const { id, slug, theme } = templatePart;
+ // The 'raw' property is not defined for a brief period in the save cycle.
+ // The fallback prevents an error in the parse function while saving.
+ const content = templatePart.content.raw || '';
+ const blocks = useMemo( () => parse( content ), [ content ] );
+ const { createSuccessNotice } = useDispatch( 'core/notices' );
+
+ const onClick = useCallback( () => {
+ const templatePartBlock = createBlock( 'core/template-part', {
+ postId: id,
+ slug,
+ theme,
+ } );
+ onInsert( templatePartBlock );
+ createSuccessNotice(
+ sprintf(
+ /* translators: %s: template part title. */
+ __( 'Template Part "%s" inserted.' ),
+ slug
+ ),
+ {
+ type: 'snackbar',
+ }
+ );
+ }, [ id, slug, theme ] );
+
+ return (
+
{
+ if ( ENTER === event.keyCode || SPACE === event.keyCode ) {
+ onClick();
+ }
+ } }
+ tabIndex={ 0 }
+ aria-label={ templatePart.slug }
+ >
+
+
+ { templatePart.slug }
+
+
+ );
+}
+
+function TemplatePartsByTheme( { templateParts, onInsert } ) {
+ const templatePartsByTheme = useMemo( () => {
+ return Object.values( groupBy( templateParts, 'meta.theme' ) );
+ }, [ templateParts ] );
+ const currentShownTPs = useAsyncList( templateParts );
+
+ return (
+ <>
+ { templatePartsByTheme.length &&
+ templatePartsByTheme.map( ( templatePartList ) => (
+
+ { templatePartList.map( ( templatePart ) => {
+ return currentShownTPs.includes( templatePart ) ? (
+
+ ) : (
+
+ );
+ } ) }
+
+ ) ) }
+ >
+ );
+}
+
+function TemplatePartSearchResults( { templateParts, onInsert, filterValue } ) {
+ const filteredTPs = useMemo( () => {
+ // Filter based on value.
+ const lowerFilterValue = filterValue.toLowerCase();
+ const searchResults = templateParts.filter(
+ ( { slug, meta: { theme } } ) =>
+ slug.toLowerCase().includes( lowerFilterValue ) ||
+ theme.toLowerCase().includes( lowerFilterValue )
+ );
+ // Order based on value location.
+ searchResults.sort( ( a, b ) => {
+ // First prioritize index found in slug.
+ const indexInSlugA = a.slug
+ .toLowerCase()
+ .indexOf( lowerFilterValue );
+ const indexInSlugB = b.slug
+ .toLowerCase()
+ .indexOf( lowerFilterValue );
+ if ( indexInSlugA !== -1 && indexInSlugB !== -1 ) {
+ return indexInSlugA - indexInSlugB;
+ } else if ( indexInSlugA !== -1 ) {
+ return -1;
+ } else if ( indexInSlugB !== -1 ) {
+ return 1;
+ }
+ // Second prioritize index found in theme.
+ return (
+ a.meta.theme.toLowerCase().indexOf( lowerFilterValue ) -
+ b.meta.theme.toLowerCase().indexOf( lowerFilterValue )
+ );
+ } );
+ return searchResults;
+ }, [ filterValue, templateParts ] );
+
+ const currentShownTPs = useAsyncList( filteredTPs );
+
+ return (
+ <>
+ { filteredTPs.map( ( templatePart ) => (
+
+ { currentShownTPs.includes( templatePart ) ? (
+
+ ) : (
+
+ ) }
+
+ ) ) }
+ >
+ );
+}
+
+export default function TemplateParts( { onInsert, filterValue } ) {
+ const templateParts = useSelect( ( select ) => {
+ return select( 'core' ).getEntityRecords(
+ 'postType',
+ 'wp_template_part',
+ {
+ status: [ 'publish', 'auto-draft' ],
+ }
+ );
+ }, [] );
+
+ if ( ! templateParts || ! templateParts.length ) {
+ return null;
+ }
+
+ if ( filterValue ) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}