diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss
index 12443a30a96656..cbe495d3787cd9 100644
--- a/packages/base-styles/_z-index.scss
+++ b/packages/base-styles/_z-index.scss
@@ -128,6 +128,7 @@ $z-layers: (
".block-editor-block-rename-modal": 1000001,
".edit-site-list__rename-modal": 1000001,
".edit-site-swap-template-modal": 1000001,
+ ".edit-site-template-panel__replace-template-modal": 1000001,
// Note: The ConfirmDialog component's z-index is being set to 1000001 in packages/components/src/confirm-dialog/styles.ts
// because it uses emotion and not sass. We need it to render on top its parent popover.
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js
new file mode 100644
index 00000000000000..b5e5988491396a
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js
@@ -0,0 +1,97 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
+import { store as coreStore } from '@wordpress/core-data';
+import { parse } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../../store';
+import { PATTERN_CORE_SOURCES, PATTERN_TYPES } from '../../../utils/constants';
+import { unlock } from '../../../lock-unlock';
+
+function injectThemeAttributeInBlockTemplateContent(
+ block,
+ currentThemeStylesheet
+) {
+ block.innerBlocks = block.innerBlocks.map( ( innerBlock ) => {
+ return injectThemeAttributeInBlockTemplateContent(
+ innerBlock,
+ currentThemeStylesheet
+ );
+ } );
+
+ if (
+ block.name === 'core/template-part' &&
+ block.attributes.theme === undefined
+ ) {
+ block.attributes.theme = currentThemeStylesheet;
+ }
+ return block;
+}
+
+function preparePatterns( patterns, template, currentThemeStylesheet ) {
+ // Filter out duplicates.
+ const filterOutDuplicatesByName = ( currentItem, index, items ) =>
+ index === items.findIndex( ( item ) => currentItem.name === item.name );
+
+ // Filter out core patterns.
+ const filterOutCorePatterns = ( pattern ) =>
+ ! PATTERN_CORE_SOURCES.includes( pattern.source );
+
+ // Filter only the patterns that are compatible with the current template.
+ const filterCompatiblePatterns = ( pattern ) =>
+ pattern.templateTypes?.includes( template.slug );
+
+ return patterns
+ .filter(
+ filterOutCorePatterns &&
+ filterOutDuplicatesByName &&
+ filterCompatiblePatterns
+ )
+ .map( ( pattern ) => ( {
+ ...pattern,
+ keywords: pattern.keywords || [],
+ type: PATTERN_TYPES.theme,
+ blocks: parse( pattern.content, {
+ __unstableSkipMigrationLogs: true,
+ } ).map( ( block ) =>
+ injectThemeAttributeInBlockTemplateContent(
+ block,
+ currentThemeStylesheet
+ )
+ ),
+ } ) );
+}
+
+export function useAvailablePatterns( template ) {
+ const { blockPatterns, restBlockPatterns, currentThemeStylesheet } =
+ useSelect( ( select ) => {
+ const { getSettings } = unlock( select( editSiteStore ) );
+ const settings = getSettings();
+
+ return {
+ blockPatterns:
+ settings.__experimentalAdditionalBlockPatterns ??
+ settings.__experimentalBlockPatterns,
+ restBlockPatterns: select( coreStore ).getBlockPatterns(),
+ currentThemeStylesheet:
+ select( coreStore ).getCurrentTheme().stylesheet,
+ };
+ }, [] );
+
+ return useMemo( () => {
+ const mergedPatterns = [
+ ...( blockPatterns || [] ),
+ ...( restBlockPatterns || [] ),
+ ];
+ return preparePatterns(
+ mergedPatterns,
+ template,
+ currentThemeStylesheet
+ );
+ }, [ blockPatterns, restBlockPatterns, template, currentThemeStylesheet ] );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js
new file mode 100644
index 00000000000000..658aacd331debc
--- /dev/null
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/replace-template-button.js
@@ -0,0 +1,89 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useState } from '@wordpress/element';
+import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor';
+import { MenuItem, Modal } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { store as coreStore } from '@wordpress/core-data';
+import { useAsyncList } from '@wordpress/compose';
+import { serialize } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { store as editSiteStore } from '../../../store';
+
+export default function ReplaceTemplateButton( {
+ onClick,
+ availableTemplates,
+} ) {
+ const { editEntityRecord } = useDispatch( coreStore );
+ const [ showModal, setShowModal ] = useState( false );
+ const onClose = () => {
+ setShowModal( false );
+ };
+
+ const { postId, postType } = useSelect( ( select ) => {
+ return {
+ postId: select( editSiteStore ).getEditedPostId(),
+ postType: select( editSiteStore ).getEditedPostType(),
+ };
+ }, [] );
+
+ const onTemplateSelect = async ( selectedTemplate ) => {
+ onClose(); // Close the template suggestions modal first.
+ onClick();
+ await editEntityRecord( 'postType', postType, postId, {
+ blocks: selectedTemplate.blocks,
+ content: serialize( selectedTemplate.blocks ),
+ } );
+ };
+
+ if ( ! availableTemplates.length || availableTemplates.length < 1 ) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ { showModal && (
+
+
+
+
+
+ ) }
+ >
+ );
+}
+
+function TemplatesList( { availableTemplates, onSelect } ) {
+ const shownTemplates = useAsyncList( availableTemplates );
+
+ return (
+
+ );
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
index 4c8ef94855dcb1..6eab753e8ad285 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss
@@ -37,3 +37,21 @@ h3.edit-site-template-card__template-areas-title {
font-weight: 500;
margin: 0 0 $grid-unit-10;
}
+
+
+.edit-site-template-panel__replace-template-modal {
+ z-index: z-index(".edit-site-template-panel__replace-template-modal");
+}
+
+.edit-site-template-panel__replace-template-modal__content {
+ column-count: 2;
+ column-gap: $grid-unit-30;
+
+ @include break-medium() {
+ column-count: 3;
+ }
+
+ @include break-wide() {
+ column-count: 4;
+ }
+}
diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
index b68cf1ff617579..81acb244a11863 100644
--- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
+++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/template-actions.js
@@ -11,13 +11,21 @@ import { moreVertical } from '@wordpress/icons';
*/
import { store as editSiteStore } from '../../../store';
import isTemplateRevertable from '../../../utils/is-template-revertable';
+import ReplaceTemplateButton from './replace-template-button';
+import { useAvailablePatterns } from './hooks';
export default function Actions( { template } ) {
+ const availablePatterns = useAvailablePatterns( template );
const { revertTemplate } = useDispatch( editSiteStore );
const isRevertable = isTemplateRevertable( template );
- if ( ! isRevertable ) {
+
+ if (
+ ! isRevertable &&
+ ( ! availablePatterns.length || availablePatterns.length < 1 )
+ ) {
return null;
}
+
return (
{ ( { onClose } ) => (
-
+ { isRevertable && (
+
+ ) }
+
) }