diff --git a/docs/designers-developers/developers/data/data-core-edit-post.md b/docs/designers-developers/developers/data/data-core-edit-post.md
index 03be0d1e9ff917..ddaac2ac0ca707 100644
--- a/docs/designers-developers/developers/data/data-core-edit-post.md
+++ b/docs/designers-developers/developers/data/data-core-edit-post.md
@@ -110,6 +110,18 @@ _Returns_
- `boolean`: Whether there are metaboxes or not.
+# **isEditingTemplate**
+
+Returns true if the template editing mode is enabled.
+
+_Parameters_
+
+- _state_ `Object`: Global application state.
+
+_Returns_
+
+- `boolean`: Whether we're editing the template.
+
# **isEditorPanelEnabled**
Returns true if the given panel is enabled, or false otherwise. Panels are
@@ -381,6 +393,18 @@ _Parameters_
- _metaBoxesPerLocation_ `Object`: Meta boxes per location.
+# **setIsEditingTemplate**
+
+Returns an action object used to switch to template editing.
+
+_Parameters_
+
+- _value_ `boolean`: Is editing template.
+
+_Returns_
+
+- `Object`: Action object.
+
# **setIsInserterOpened**
Returns an action object used to open/close the inserter.
diff --git a/lib/client-assets.php b/lib/client-assets.php
index d17b40b801bebf..fb3b528a566426 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -649,3 +649,16 @@ function gutenberg_extend_block_editor_settings_with_default_editor_styles( $set
return $settings;
}
add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_settings_with_default_editor_styles' );
+
+/**
+ * Adds a flag to the editor settings to know whether we're in FSE theme or not.
+ *
+ * @param array $settings Default editor settings.
+ *
+ * @return array Filtered editor settings.
+ */
+function gutenberg_extend_block_editor_settings_with_fse_theme_flag( $settings ) {
+ $settings['isFSETheme'] = gutenberg_is_fse_theme();
+ return $settings;
+}
+add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_settings_with_fse_theme_flag' );
diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js
index 855383fe19c4cd..d4c86853533d89 100644
--- a/packages/block-library/src/post-content/edit.js
+++ b/packages/block-library/src/post-content/edit.js
@@ -2,7 +2,6 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { useSelect } from '@wordpress/data';
import { useBlockProps } from '@wordpress/block-editor';
/**
@@ -13,19 +12,8 @@ import PostContentInnerBlocks from './inner-blocks';
export default function PostContentEdit( {
context: { postId: contextPostId, postType: contextPostType },
} ) {
- const { id: currentPostId, type: currentPostType } = useSelect(
- ( select ) => select( 'core/editor' ).getCurrentPost() ?? {}
- );
const blockProps = useBlockProps();
-
- // Only render InnerBlocks if the context is different from the active post
- // to avoid infinite recursion of post content.
- if (
- contextPostId &&
- contextPostType &&
- contextPostId !== currentPostId &&
- contextPostType !== currentPostType
- ) {
+ if ( contextPostId && contextPostType ) {
return (
res.json() );
+
+ return data;
+ },
+};
+
+export default controls;
diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js
index fc2c051a52165a..c59d5bcf97ce35 100644
--- a/packages/core-data/src/index.js
+++ b/packages/core-data/src/index.js
@@ -13,6 +13,7 @@ import * as actions from './actions';
import * as resolvers from './resolvers';
import * as locksSelectors from './locks/selectors';
import * as locksActions from './locks/actions';
+import customControls from './controls';
import { defaultEntities, getMethodName } from './entities';
import { STORE_NAME } from './name';
@@ -58,7 +59,7 @@ const entityActions = defaultEntities.reduce( ( result, entity ) => {
const storeConfig = {
reducer,
- controls,
+ controls: { ...customControls, ...controls },
actions: { ...actions, ...entityActions, ...locksActions },
selectors: { ...selectors, ...entitySelectors, ...locksSelectors },
resolvers: { ...resolvers, ...entityResolvers },
diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js
index 9a1db4022b5580..3458c260d3f603 100644
--- a/packages/core-data/src/resolvers.js
+++ b/packages/core-data/src/resolvers.js
@@ -10,6 +10,10 @@ import { addQueryArgs } from '@wordpress/url';
import deprecated from '@wordpress/deprecated';
import { controls } from '@wordpress/data';
import { apiFetch } from '@wordpress/data-controls';
+/**
+ * Internal dependencies
+ */
+import { regularFetch } from './controls';
/**
* Internal dependencies
@@ -382,3 +386,38 @@ export function* getAutosaves( postType, postId ) {
export function* getAutosave( postType, postId ) {
yield controls.resolveSelect( 'core', 'getAutosaves', postType, postId );
}
+
+/**
+ * Retrieve the frontend template used for a given link.
+ *
+ * @param {string} link Link.
+ */
+export function* __experimentalGetTemplateForLink( link ) {
+ // Ideally this should be using an apiFetch call
+ // We could potentially do so by adding a "filter" to the `wp_template` end point.
+ // Also it seems the returned object is not a regular REST API post type.
+ const template = yield regularFetch(
+ addQueryArgs( link, {
+ '_wp-find-template': true,
+ } )
+ );
+
+ if ( template === null ) {
+ return;
+ }
+
+ yield getEntityRecord( 'postType', 'wp_template', template.ID );
+ const record = yield controls.select(
+ 'core',
+ 'getEntityRecord',
+ 'postType',
+ 'wp_template',
+ template.ID
+ );
+
+ if ( record ) {
+ yield receiveEntityRecords( 'postType', 'wp_template', [ record ], {
+ 'find-template': link,
+ } );
+ }
+}
diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js
index d00356fa3483e6..5f4a9501f9bd54 100644
--- a/packages/core-data/src/selectors.js
+++ b/packages/core-data/src/selectors.js
@@ -740,3 +740,19 @@ export const getReferenceByDistinctEdits = createSelector(
state.undo.flattenedUndo,
]
);
+
+/**
+ * Retrieve the frontend template used for a given link.
+ *
+ * @param {Object} state Editor state.
+ * @param {string} link Link.
+ *
+ * @return {Object?} The template record.
+ */
+export function __experimentalGetTemplateForLink( state, link ) {
+ const records = getEntityRecords( state, 'postType', 'wp_template', {
+ 'find-template': link,
+ } );
+
+ return records?.length ? records[ 0 ] : null;
+}
diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md
index e3b052a81f4dbd..1ec564dcec4d94 100644
--- a/packages/e2e-test-utils/README.md
+++ b/packages/e2e-test-utils/README.md
@@ -446,6 +446,18 @@ Clicks on the button in the header which opens Document Settings sidebar when it
Opens the global block inserter.
+# **openPreviewPage**
+
+Opens the preview page of an edited post.
+
+_Parameters_
+
+- _editorPage_ `Page`: puppeteer editor page.
+
+_Returns_
+
+- `Page`: preview page.
+
# **openPublishPanel**
Opens the publish panel.
diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js
index 39660c8173c223..c3bdfa98177cc7 100644
--- a/packages/e2e-test-utils/src/index.js
+++ b/packages/e2e-test-utils/src/index.js
@@ -74,5 +74,6 @@ export { uninstallPlugin } from './uninstall-plugin';
export { visitAdminPage } from './visit-admin-page';
export { waitForWindowDimensions } from './wait-for-window-dimensions';
export { showBlockToolbar } from './show-block-toolbar';
+export { openPreviewPage } from './preview';
export * from './mocks';
diff --git a/packages/e2e-test-utils/src/preview.js b/packages/e2e-test-utils/src/preview.js
new file mode 100644
index 00000000000000..90486bb174c65d
--- /dev/null
+++ b/packages/e2e-test-utils/src/preview.js
@@ -0,0 +1,29 @@
+/**
+ * External dependencies
+ */
+import { last } from 'lodash';
+
+/** @typedef {import('puppeteer').Page} Page */
+
+/**
+ * Opens the preview page of an edited post.
+ *
+ * @param {Page} editorPage puppeteer editor page.
+ * @return {Page} preview page.
+ */
+export async function openPreviewPage( editorPage = page ) {
+ let openTabs = await browser.pages();
+ const expectedTabsCount = openTabs.length + 1;
+ await editorPage.click( '.block-editor-post-preview__button-toggle' );
+ await editorPage.waitFor( '.edit-post-header-preview__button-external' );
+ await editorPage.click( '.edit-post-header-preview__button-external' );
+
+ // Wait for the new tab to open.
+ while ( openTabs.length < expectedTabsCount ) {
+ await editorPage.waitFor( 1 );
+ openTabs = await browser.pages();
+ }
+
+ const previewPage = last( openTabs );
+ return previewPage;
+}
diff --git a/packages/e2e-tests/specs/editor/plugins/block-context.test.js b/packages/e2e-tests/specs/editor/plugins/block-context.test.js
index 3a96eb3e141f6a..f09df529e70ce0 100644
--- a/packages/e2e-tests/specs/editor/plugins/block-context.test.js
+++ b/packages/e2e-tests/specs/editor/plugins/block-context.test.js
@@ -1,8 +1,3 @@
-/**
- * External dependencies
- */
-import { last } from 'lodash';
-
/**
* WordPress dependencies
*/
@@ -12,29 +7,9 @@ import {
deactivatePlugin,
insertBlock,
saveDraft,
+ openPreviewPage,
} from '@wordpress/e2e-test-utils';
-async function openPreviewPage( editorPage ) {
- let openTabs = await browser.pages();
- const expectedTabsCount = openTabs.length + 1;
- await editorPage.click( '.block-editor-post-preview__button-toggle' );
- await editorPage.waitFor( '.edit-post-header-preview__button-external' );
- await editorPage.click( '.edit-post-header-preview__button-external' );
-
- // Wait for the new tab to open.
- while ( openTabs.length < expectedTabsCount ) {
- await editorPage.waitFor( 1 );
- openTabs = await browser.pages();
- }
-
- const previewPage = last( openTabs );
- // Wait for the preview to load. We can't do interstitial detection here,
- // because it might load too quickly for us to pick up, so we wait for
- // the preview to load by waiting for the content to appear.
- await previewPage.waitForSelector( '.entry-content' );
- return previewPage;
-}
-
describe( 'Block context', () => {
beforeAll( async () => {
await activatePlugin( 'gutenberg-test-block-context' );
@@ -74,6 +49,7 @@ describe( 'Block context', () => {
await insertBlock( 'Test Context Provider' );
const editorPage = page;
const previewPage = await openPreviewPage( editorPage );
+ await previewPage.waitForSelector( '.entry-content' );
// Check default context values are populated.
let content = await previewPage.$eval(
diff --git a/packages/e2e-tests/specs/editor/various/preview.test.js b/packages/e2e-tests/specs/editor/various/preview.test.js
index 87c00255c508cf..d0996211a512c9 100644
--- a/packages/e2e-tests/specs/editor/various/preview.test.js
+++ b/packages/e2e-tests/specs/editor/various/preview.test.js
@@ -1,8 +1,3 @@
-/**
- * External dependencies
- */
-import { last } from 'lodash';
-
/**
* WordPress dependencies
*/
@@ -15,31 +10,11 @@ import {
deactivatePlugin,
publishPost,
saveDraft,
+ openPreviewPage,
} from '@wordpress/e2e-test-utils';
/** @typedef {import('puppeteer').Page} Page */
-async function openPreviewPage( editorPage ) {
- let openTabs = await browser.pages();
- const expectedTabsCount = openTabs.length + 1;
- await editorPage.click( '.block-editor-post-preview__button-toggle' );
- await editorPage.waitFor( '.edit-post-header-preview__button-external' );
- await editorPage.click( '.edit-post-header-preview__button-external' );
-
- // Wait for the new tab to open.
- while ( openTabs.length < expectedTabsCount ) {
- await editorPage.waitFor( 1 );
- openTabs = await browser.pages();
- }
-
- const previewPage = last( openTabs );
- // Wait for the preview to load. We can't do interstitial detection here,
- // because it might load too quickly for us to pick up, so we wait for
- // the preview to load by waiting for the title to appear.
- await previewPage.waitForSelector( '.entry-title' );
- return previewPage;
-}
-
/**
* Given the Page instance for the editor, opens preview drodpdown, and
* awaits the presence of the external preview selector.
@@ -123,6 +98,7 @@ describe( 'Preview', () => {
await editorPage.type( '.editor-post-title__input', 'Hello World' );
const previewPage = await openPreviewPage( editorPage );
+ await previewPage.waitForSelector( '.entry-title' );
// When autosave completes for a new post, the URL of the editor should
// update to include the ID. Use this to assert on preview URL.
@@ -222,6 +198,7 @@ describe( 'Preview', () => {
// Open the preview page.
const previewPage = await openPreviewPage( editorPage );
+ await previewPage.waitForSelector( '.entry-title' );
// Title in preview should match input.
let previewTitle = await previewPage.$eval(
@@ -282,6 +259,7 @@ describe( 'Preview with Custom Fields enabled', () => {
// Open the preview page.
const previewPage = await openPreviewPage( editorPage );
+ await previewPage.waitForSelector( '.entry-title' );
// Check the title and preview match.
let previewTitle = await previewPage.$eval(
diff --git a/packages/e2e-tests/specs/experiments/post-editor-template-mode.test.js b/packages/e2e-tests/specs/experiments/post-editor-template-mode.test.js
new file mode 100644
index 00000000000000..31a795a1b2d2f4
--- /dev/null
+++ b/packages/e2e-tests/specs/experiments/post-editor-template-mode.test.js
@@ -0,0 +1,85 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ activateTheme,
+ createNewPost,
+ insertBlock,
+ saveDraft,
+ trashAllPosts,
+ openPreviewPage,
+ openDocumentSettingsSidebar,
+} from '@wordpress/e2e-test-utils';
+
+describe( 'Post Editor Template mode', () => {
+ beforeAll( async () => {
+ await activateTheme( 'twentytwentyone-blocks' );
+ await trashAllPosts( 'wp_template' );
+ await trashAllPosts( 'wp_template_part' );
+ } );
+
+ afterAll( async () => {
+ await activateTheme( 'twentytwentyone' );
+ } );
+
+ beforeEach( async () => {
+ await createNewPost();
+ } );
+
+ it( 'Allow to switch to template mode, edit the template and check the result', async () => {
+ // Create a random post.
+ await page.type( '.editor-post-title__input', 'Just an FSE Post' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( 'Hello World' );
+
+ // Unselect the blocks.
+ await page.evaluate( () => {
+ wp.data.dispatch( 'core/block-editor' ).clearSelectedBlock();
+ } );
+
+ // Save the post
+ // Saving shouldn't be necessary but unfortunately,
+ // there's a template resolution bug forcing us to do so.
+ await saveDraft();
+ await page.reload();
+
+ // Switch to template mode.
+ await openDocumentSettingsSidebar();
+ const switchLink = await page.waitForSelector(
+ '.edit-post-post-template button'
+ );
+ await switchLink.click();
+
+ // Check that we switched properly to edit mode.
+ await page.waitForXPath(
+ '//*[contains(@class, "components-snackbar")]/*[text()="Editing template. Changes made here affect all posts and pages that use the template."]'
+ );
+ const title = await page.$eval(
+ '.edit-post-template-title',
+ ( el ) => el.innerText
+ );
+ expect( title ).toContain( 'Editing template:' );
+
+ // Edit the template
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type(
+ 'Just a random paragraph added to the template'
+ );
+
+ // Save changes
+ const doneButton = await page.waitForXPath(
+ `//button[contains(text(), 'Apply')]`
+ );
+ await doneButton.click();
+ const saveButton = await page.waitForXPath(
+ `//div[contains(@class, "entities-saved-states__panel-header")]/button[contains(text(), 'Save')]`
+ );
+ await saveButton.click();
+
+ // Preview changes
+ const previewPage = await openPreviewPage();
+ await previewPage.waitForXPath(
+ '//p[contains(text(), "Just a random paragraph added to the template")]'
+ );
+ } );
+} );
diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js
index ad8737767a7f9a..1349d67ffbc612 100644
--- a/packages/edit-post/src/components/header/header-toolbar/index.js
+++ b/packages/edit-post/src/components/header/header-toolbar/index.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+
/**
* WordPress dependencies
*/
@@ -25,6 +30,11 @@ import {
import { plus } from '@wordpress/icons';
import { useRef } from '@wordpress/element';
+/**
+ * Internal dependencies
+ */
+import TemplateTitle from '../template-title';
+
function HeaderToolbar() {
const inserterButton = useRef();
const { setIsInserterOpened } = useDispatch( 'core/edit-post' );
@@ -36,6 +46,7 @@ function HeaderToolbar() {
previewDeviceType,
showIconLabels,
isNavigationTool,
+ isTemplateMode,
} = useSelect( ( select ) => {
const {
hasInserterItems,
@@ -64,6 +75,7 @@ function HeaderToolbar() {
'showIconLabels'
),
isNavigationTool: select( 'core/block-editor' ).isNavigationMode(),
+ isTemplateMode: select( 'core/edit-post' ).isEditingTemplate(),
};
}, [] );
const isLargeViewport = useViewportMatch( 'medium' );
@@ -206,8 +218,17 @@ characters. */
) }
+
+
{ displayBlockToolbar && (
-
+
) }
diff --git a/packages/edit-post/src/components/header/header-toolbar/style.scss b/packages/edit-post/src/components/header/header-toolbar/style.scss
index 84de5011a39de3..fc3bd59f4232fe 100644
--- a/packages/edit-post/src/components/header/header-toolbar/style.scss
+++ b/packages/edit-post/src/components/header/header-toolbar/style.scss
@@ -1,5 +1,6 @@
.edit-post-header-toolbar {
display: inline-flex;
+ flex-grow: 1;
align-items: center;
border: none;
@@ -117,25 +118,27 @@
// Move toolbar into top Editor Bar.
@include break-wide {
- position: static;
- left: auto;
- right: auto;
- background: none;
- border-bottom: none;
-
- .is-sidebar-opened & {
+ &:not(.is-pushed-down) {
+ position: static;
+ left: auto;
right: auto;
- }
+ background: none;
+ border-bottom: none;
- .block-editor-block-toolbar {
- border-left: $border-width solid $gray-300;
- }
+ .is-sidebar-opened & {
+ right: auto;
+ }
- .block-editor-block-toolbar .components-toolbar-group,
- .block-editor-block-toolbar .components-toolbar {
- $top-toolbar-padding: ($header-height - $grid-unit-60) / 2;
- height: $header-height;
- padding: $top-toolbar-padding 0;
+ .block-editor-block-toolbar {
+ border-left: $border-width solid $gray-300;
+ }
+
+ .block-editor-block-toolbar .components-toolbar-group,
+ .block-editor-block-toolbar .components-toolbar {
+ $top-toolbar-padding: ($header-height - $grid-unit-60) / 2;
+ height: $header-height;
+ padding: $top-toolbar-padding 0;
+ }
}
}
}
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index cd5457ff3d29e3..639ba8424aeab8 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -19,7 +19,8 @@ import HeaderToolbar from './header-toolbar';
import MoreMenu from './more-menu';
import PostPublishButtonOrToggle from './post-publish-button-or-toggle';
import { default as DevicePreview } from '../device-preview';
-import MainDashboardButton from '../header/main-dashboard-button';
+import MainDashboardButton from './main-dashboard-button';
+import TemplateSaveButton from './template-save-button';
function Header( { setEntitiesSavedStatesCallback } ) {
const {
@@ -28,6 +29,7 @@ function Header( { setEntitiesSavedStatesCallback } ) {
isSaving,
showIconLabels,
hasReducedUI,
+ isEditingTemplate,
} = useSelect(
( select ) => ( {
hasActiveMetaboxes: select( 'core/edit-post' ).hasMetaBoxes(),
@@ -41,6 +43,7 @@ function Header( { setEntitiesSavedStatesCallback } ) {
hasReducedUI: select( 'core/edit-post' ).isFeatureActive(
'reducedUI'
),
+ isEditingTemplate: select( 'core/edit-post' ).isEditingTemplate(),
} ),
[]
);
@@ -60,30 +63,35 @@ function Header( { setEntitiesSavedStatesCallback } ) {
- { ! isPublishSidebarOpened && (
- // This button isn't completely hidden by the publish sidebar.
- // We can't hide the whole toolbar when the publish sidebar is open because
- // we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node.
- // We track that DOM node to return focus to the PostPublishButtonOrToggle
- // when the publish sidebar has been closed.
-
+ { ! isEditingTemplate && (
+ <>
+ { ! isPublishSidebarOpened && (
+ // This button isn't completely hidden by the publish sidebar.
+ // We can't hide the whole toolbar when the publish sidebar is open because
+ // we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node.
+ // We track that DOM node to return focus to the PostPublishButtonOrToggle
+ // when the publish sidebar has been closed.
+
+ ) }
+
+
+
+ >
) }
-
-
-
+ { isEditingTemplate &&
}
{ ( isLargeViewport || ! showIconLabels ) && (
<>
diff --git a/packages/edit-post/src/components/header/template-save-button/index.js b/packages/edit-post/src/components/header/template-save-button/index.js
new file mode 100644
index 00000000000000..57c660d65ef167
--- /dev/null
+++ b/packages/edit-post/src/components/header/template-save-button/index.js
@@ -0,0 +1,100 @@
+/**
+ * WordPress dependencies
+ */
+import { EntitiesSavedStates, store as editorStore } from '@wordpress/editor';
+import { Button } from '@wordpress/components';
+import { store as coreStore } from '@wordpress/core-data';
+import { useState } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { ActionsPanelFill } from '../../layout/actions-panel';
+import { store as editPostStore } from '../../../store';
+
+function TemplateSaveButton() {
+ const [
+ isEntitiesReviewPanelOpen,
+ setIsEntitiesReviewPanelOpen,
+ ] = useState( false );
+ const { setIsEditingTemplate } = useDispatch( editPostStore );
+ const { editEntityRecord } = useDispatch( coreStore );
+ const { getTemplateInfo, getEditedEntityRecord } = useSelect(
+ ( select ) => {
+ return {
+ getTemplateInfo: select( editorStore )
+ .__experimentalGetTemplateInfo,
+ getEditedEntityRecord: select( coreStore )
+ .getEditedEntityRecord,
+ };
+ },
+ []
+ );
+ return (
+ <>
+
+
+
+ {
+ // The logic here should be abstracted in the entities save handler/component
+ // and not duplicated accross multi-entity save behavior.
+ if ( entities?.length ) {
+ entities.forEach( ( entity ) => {
+ const edits = {};
+ if (
+ entity.kind === 'postType' &&
+ entity.name === 'wp_template'
+ ) {
+ const record = getEditedEntityRecord(
+ entity.kind,
+ entity.name,
+ entity.key
+ );
+ edits.title =
+ getTemplateInfo( record ).title ??
+ entity.title ??
+ record.slug;
+ }
+
+ if (
+ entity.kind === 'postType' &&
+ [
+ 'wp_template',
+ 'wp_template_part',
+ ].includes( entity.name )
+ ) {
+ edits.status = 'publish';
+ }
+
+ editEntityRecord(
+ entity.kind,
+ entity.name,
+ entity.key,
+ edits
+ );
+ } );
+ }
+ setIsEntitiesReviewPanelOpen( false );
+ if ( entities?.length ) {
+ setIsEditingTemplate( false );
+ }
+ } }
+ />
+
+ >
+ );
+}
+
+export default TemplateSaveButton;
diff --git a/packages/edit-post/src/components/header/template-title/index.js b/packages/edit-post/src/components/header/template-title/index.js
new file mode 100644
index 00000000000000..deb2b905ee9e34
--- /dev/null
+++ b/packages/edit-post/src/components/header/template-title/index.js
@@ -0,0 +1,43 @@
+/**
+ * WordPress dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { useSelect } from '@wordpress/data';
+import { store as editorStore } from '@wordpress/editor';
+import { store as coreStore } from '@wordpress/core-data';
+
+/**
+ * Internal dependencies
+ */
+import { store as editPostStore } from '../../../store';
+
+function TemplateTitle() {
+ const { template, isEditing } = useSelect( ( select ) => {
+ const { getEditedPostAttribute } = select( editorStore );
+ const { __experimentalGetTemplateForLink } = select( coreStore );
+ const { isEditingTemplate } = select( editPostStore );
+ const link = getEditedPostAttribute( 'link' );
+ const _isEditing = isEditingTemplate();
+ return {
+ template: _isEditing
+ ? __experimentalGetTemplateForLink( link )
+ : null,
+ isEditing: _isEditing,
+ };
+ }, [] );
+
+ if ( ! isEditing || ! template ) {
+ return null;
+ }
+
+ return (
+
+ {
+ /* translators: 1: Template name. */
+ sprintf( __( 'Editing template: %s' ), template.slug )
+ }
+
+ );
+}
+
+export default TemplateTitle;
diff --git a/packages/edit-post/src/components/header/template-title/style.scss b/packages/edit-post/src/components/header/template-title/style.scss
new file mode 100644
index 00000000000000..b4d6132c089757
--- /dev/null
+++ b/packages/edit-post/src/components/header/template-title/style.scss
@@ -0,0 +1,5 @@
+.edit-post-template-title {
+ display: inline-flex;
+ flex-grow: 1;
+ justify-content: center;
+}
diff --git a/packages/edit-post/src/components/layout/actions-panel.js b/packages/edit-post/src/components/layout/actions-panel.js
index 5ac15a9c253c91..e1ad347a4bf7d7 100644
--- a/packages/edit-post/src/components/layout/actions-panel.js
+++ b/packages/edit-post/src/components/layout/actions-panel.js
@@ -3,7 +3,7 @@
*/
import { EntitiesSavedStates, PostPublishPanel } from '@wordpress/editor';
import { useSelect, useDispatch } from '@wordpress/data';
-import { Button } from '@wordpress/components';
+import { Button, createSlotFill } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useCallback } from '@wordpress/element';
/**
@@ -12,6 +12,10 @@ import { useCallback } from '@wordpress/element';
import PluginPostPublishPanel from '../sidebar/plugin-post-publish-panel';
import PluginPrePublishPanel from '../sidebar/plugin-pre-publish-panel';
+const { Fill, Slot } = createSlotFill( 'ActionsPanel' );
+
+export const ActionsPanelFill = Fill;
+
export default function ActionsPanel( {
setEntitiesSavedStatesCallback,
closeEntitiesSavedStates,
@@ -92,6 +96,7 @@ export default function ActionsPanel( {
isOpen={ isEntitiesSavedStatesOpen }
close={ closeEntitiesSavedStates }
/>
+
{ ! isEntitiesSavedStatesOpen && unmountableContent }
>
);
diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js
index 11311e91849c2b..ad2437e4c1f1a0 100644
--- a/packages/edit-post/src/components/sidebar/post-status/index.js
+++ b/packages/edit-post/src/components/sidebar/post-status/index.js
@@ -18,6 +18,7 @@ import PostSlug from '../post-slug';
import PostFormat from '../post-format';
import PostPendingStatus from '../post-pending-status';
import PluginPostStatusInfo from '../plugin-post-status-info';
+import PostTemplate from '../post-template';
/**
* Module Constants
@@ -35,6 +36,7 @@ function PostStatus( { isOpened, onTogglePanel } ) {
{ ( fills ) => (
<>
+
diff --git a/packages/edit-post/src/components/sidebar/post-template/index.js b/packages/edit-post/src/components/sidebar/post-template/index.js
new file mode 100644
index 00000000000000..9c3380860aacbf
--- /dev/null
+++ b/packages/edit-post/src/components/sidebar/post-template/index.js
@@ -0,0 +1,91 @@
+/**
+ * WordPress dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { PanelRow, Button } from '@wordpress/components';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { createInterpolateElement } from '@wordpress/element';
+import { store as editorStore } from '@wordpress/editor';
+import { store as coreStore } from '@wordpress/core-data';
+import { store as noticesStore } from '@wordpress/notices';
+
+/**
+ * Internal dependencies
+ */
+import { store as editPostStore } from '../../../store';
+
+function PostTemplate() {
+ const { template, isEditing, isFSETheme } = useSelect( ( select ) => {
+ const {
+ getEditedPostAttribute,
+ __unstableIsAutodraftPost,
+ getCurrentPostType,
+ } = select( editorStore );
+ const { __experimentalGetTemplateForLink } = select( coreStore );
+ const { isEditingTemplate } = select( editPostStore );
+ const link = getEditedPostAttribute( 'link' );
+ const isFSEEnabled = select( editorStore ).getEditorSettings()
+ .isFSETheme;
+ return {
+ template:
+ isFSEEnabled &&
+ link &&
+ ! __unstableIsAutodraftPost() &&
+ getCurrentPostType() !== 'wp_template'
+ ? __experimentalGetTemplateForLink( link )
+ : null,
+ isEditing: isEditingTemplate(),
+ isFSETheme: isFSEEnabled,
+ };
+ }, [] );
+ const { setIsEditingTemplate } = useDispatch( editPostStore );
+ const { createSuccessNotice } = useDispatch( noticesStore );
+
+ if ( ! isFSETheme || ! template ) {
+ return null;
+ }
+
+ return (
+
+ { __( 'Template' ) }
+ { ! isEditing && (
+
+ { createInterpolateElement(
+ sprintf(
+ /* translators: 1: Template name. */
+ __( '%s (Edit)' ),
+ template.slug
+ ),
+ {
+ a: (
+
+ ),
+ }
+ ) }
+
+ ) }
+ { isEditing && (
+
+ { template.slug }
+
+ ) }
+
+ );
+}
+
+export default PostTemplate;
diff --git a/packages/edit-post/src/components/sidebar/post-template/style.scss b/packages/edit-post/src/components/sidebar/post-template/style.scss
new file mode 100644
index 00000000000000..d625e897224368
--- /dev/null
+++ b/packages/edit-post/src/components/sidebar/post-template/style.scss
@@ -0,0 +1,13 @@
+.edit-post-post-template {
+ width: 100%;
+ justify-content: left;
+
+ span {
+ display: block;
+ width: 45%;
+ }
+}
+
+.edit-post-post-template__value {
+ padding-left: 6px;
+}
diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js
index e51b8f4f691898..cf87daa479eda9 100644
--- a/packages/edit-post/src/components/visual-editor/index.js
+++ b/packages/edit-post/src/components/visual-editor/index.js
@@ -2,8 +2,8 @@
* WordPress dependencies
*/
import {
- PostTitle,
VisualEditorGlobalKeyboardShortcuts,
+ PostTitle,
} from '@wordpress/editor';
import {
WritingFlow,
@@ -28,8 +28,15 @@ import { useSelect } from '@wordpress/data';
export default function VisualEditor() {
const ref = useRef();
- const deviceType = useSelect( ( select ) => {
- return select( 'core/edit-post' ).__experimentalGetPreviewDeviceType();
+ const { deviceType, isTemplateMode } = useSelect( ( select ) => {
+ const {
+ isEditingTemplate,
+ __experimentalGetPreviewDeviceType,
+ } = select( 'core/edit-post' );
+ return {
+ deviceType: __experimentalGetPreviewDeviceType(),
+ isTemplateMode: isEditingTemplate(),
+ };
}, [] );
const hasMetaBoxes = useSelect(
( select ) => select( 'core/edit-post' ).hasMetaBoxes(),
@@ -61,9 +68,11 @@ export default function VisualEditor() {
style={ resizedCanvasStyles || desktopCanvasStyles }
>
-
+ { ! isTemplateMode && (
+
+ ) }
diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js
index 6694ffdfb34dcd..3f44768560bf5e 100644
--- a/packages/edit-post/src/editor.js
+++ b/packages/edit-post/src/editor.js
@@ -1,26 +1,24 @@
/**
* External dependencies
*/
-import memize from 'memize';
import { size, map, without, omit } from 'lodash';
/**
* WordPress dependencies
*/
import { store as blocksStore } from '@wordpress/blocks';
-import { withSelect, withDispatch } from '@wordpress/data';
+import { useSelect, useDispatch } from '@wordpress/data';
import {
EditorProvider,
ErrorBoundary,
PostLockedModal,
} from '@wordpress/editor';
-import { StrictMode, Component } from '@wordpress/element';
+import { StrictMode, useMemo } from '@wordpress/element';
import {
KeyboardShortcuts,
SlotFillProvider,
DropZoneProvider,
} from '@wordpress/components';
-import { compose } from '@wordpress/compose';
/**
* Internal dependencies
@@ -30,30 +28,78 @@ import Layout from './components/layout';
import EditorInitialization from './components/editor-initialization';
import EditPostSettings from './components/edit-post-settings';
-class Editor extends Component {
- constructor() {
- super( ...arguments );
-
- this.getEditorSettings = memize( this.getEditorSettings, {
- maxSize: 1,
- } );
- }
-
- getEditorSettings(
- settings,
+function Editor( {
+ postId,
+ postType,
+ settings,
+ initialEdits,
+ onError,
+ ...props
+} ) {
+ const {
hasFixedToolbar,
focusMode,
hasReducedUI,
hasThemeStyles,
+ post,
+ preferredStyleVariations,
hiddenBlockTypes,
blockTypes,
- preferredStyleVariations,
__experimentalLocalAutosaveInterval,
- __experimentalSetIsInserterOpened,
- updatePreferredStyleVariations,
- keepCaretInsideBlock
- ) {
- settings = {
+ keepCaretInsideBlock,
+ isTemplateMode,
+ template,
+ } = useSelect( ( select ) => {
+ const {
+ isFeatureActive,
+ getPreference,
+ __experimentalGetPreviewDeviceType,
+ isEditingTemplate,
+ } = select( 'core/edit-post' );
+ const { getEntityRecord, __experimentalGetTemplateForLink } = select(
+ 'core'
+ );
+ const { getEditorSettings, __unstableIsAutodraftPost } = select(
+ 'core/editor'
+ );
+ const { getBlockTypes } = select( blocksStore );
+ const postObject = getEntityRecord( 'postType', postType, postId );
+ const isFSETheme = getEditorSettings().isFSETheme;
+
+ return {
+ hasFixedToolbar:
+ isFeatureActive( 'fixedToolbar' ) ||
+ __experimentalGetPreviewDeviceType() !== 'Desktop',
+ focusMode: isFeatureActive( 'focusMode' ),
+ hasReducedUI: isFeatureActive( 'reducedUI' ),
+ hasThemeStyles: isFeatureActive( 'themeStyles' ),
+ preferredStyleVariations: getPreference(
+ 'preferredStyleVariations'
+ ),
+ hiddenBlockTypes: getPreference( 'hiddenBlockTypes' ),
+ blockTypes: getBlockTypes(),
+ __experimentalLocalAutosaveInterval: getPreference(
+ 'localAutosaveInterval'
+ ),
+ keepCaretInsideBlock: isFeatureActive( 'keepCaretInsideBlock' ),
+ isTemplateMode: isEditingTemplate(),
+ template:
+ isFSETheme &&
+ postObject &&
+ ! __unstableIsAutodraftPost() &&
+ postType !== 'wp_template'
+ ? __experimentalGetTemplateForLink( postObject.link )
+ : null,
+ post: postObject,
+ };
+ } );
+
+ const { updatePreferredStyleVariations, setIsInserterOpened } = useDispatch(
+ 'core/edit-post'
+ );
+
+ const editorSettings = useMemo( () => {
+ const result = {
...( hasThemeStyles
? settings
: omit( settings, [ 'defaultEditorStyles' ] ) ),
@@ -67,7 +113,7 @@ class Editor extends Component {
__experimentalLocalAutosaveInterval,
// This is marked as experimental to give time for the quick inserter to mature.
- __experimentalSetIsInserterOpened,
+ __experimentalSetIsInserterOpened: setIsInserterOpened,
keepCaretInsideBlock,
styles: hasThemeStyles
? settings.styles
@@ -84,121 +130,61 @@ class Editor extends Component {
? map( blockTypes, 'name' )
: settings.allowedBlockTypes || [];
- settings.allowedBlockTypes = without(
+ result.allowedBlockTypes = without(
defaultAllowedBlockTypes,
...hiddenBlockTypes
);
}
- return settings;
- }
-
- render() {
- const {
- settings,
- hasFixedToolbar,
- focusMode,
- hasReducedUI,
- hasThemeStyles,
- post,
- postId,
- initialEdits,
- onError,
- hiddenBlockTypes,
- blockTypes,
- preferredStyleVariations,
- __experimentalLocalAutosaveInterval,
- setIsInserterOpened,
- updatePreferredStyleVariations,
- keepCaretInsideBlock,
- ...props
- } = this.props;
-
- if ( ! post ) {
- return null;
- }
-
- const editorSettings = this.getEditorSettings(
- settings,
- hasFixedToolbar,
- focusMode,
- hasReducedUI,
- hasThemeStyles,
- hiddenBlockTypes,
- blockTypes,
- preferredStyleVariations,
- __experimentalLocalAutosaveInterval,
- setIsInserterOpened,
- updatePreferredStyleVariations,
- keepCaretInsideBlock
- );
+ return result;
+ }, [
+ settings,
+ hasFixedToolbar,
+ focusMode,
+ hasReducedUI,
+ hasThemeStyles,
+ hiddenBlockTypes,
+ blockTypes,
+ preferredStyleVariations,
+ __experimentalLocalAutosaveInterval,
+ setIsInserterOpened,
+ updatePreferredStyleVariations,
+ keepCaretInsideBlock,
+ ] );
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ if ( ! post ) {
+ return null;
}
-}
-export default compose( [
- withSelect( ( select, { postId, postType } ) => {
- const {
- isFeatureActive,
- getPreference,
- __experimentalGetPreviewDeviceType,
- } = select( 'core/edit-post' );
- const { getEntityRecord } = select( 'core' );
- const { getBlockTypes } = select( blocksStore );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
- return {
- hasFixedToolbar:
- isFeatureActive( 'fixedToolbar' ) ||
- __experimentalGetPreviewDeviceType() !== 'Desktop',
- focusMode: isFeatureActive( 'focusMode' ),
- hasReducedUI: isFeatureActive( 'reducedUI' ),
- hasThemeStyles: isFeatureActive( 'themeStyles' ),
- post: getEntityRecord( 'postType', postType, postId ),
- preferredStyleVariations: getPreference(
- 'preferredStyleVariations'
- ),
- hiddenBlockTypes: getPreference( 'hiddenBlockTypes' ),
- blockTypes: getBlockTypes(),
- __experimentalLocalAutosaveInterval: getPreference(
- 'localAutosaveInterval'
- ),
- keepCaretInsideBlock: isFeatureActive( 'keepCaretInsideBlock' ),
- };
- } ),
- withDispatch( ( dispatch ) => {
- const {
- updatePreferredStyleVariations,
- setIsInserterOpened,
- } = dispatch( 'core/edit-post' );
- return {
- updatePreferredStyleVariations,
- setIsInserterOpened,
- };
- } ),
-] )( Editor );
+export default Editor;
diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js
index 5dbbaed47b7096..95901295d0ca3b 100644
--- a/packages/edit-post/src/store/actions.js
+++ b/packages/edit-post/src/store/actions.js
@@ -422,3 +422,16 @@ export function setIsInserterOpened( value ) {
value,
};
}
+
+/**
+ * Returns an action object used to switch to template editing.
+ *
+ * @param {boolean} value Is editing template.
+ * @return {Object} Action object.
+ */
+export function setIsEditingTemplate( value ) {
+ return {
+ type: 'SET_IS_EDITING_TEMPLATE',
+ value,
+ };
+}
diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js
index 934de97aa73c08..1f1e762f728135 100644
--- a/packages/edit-post/src/store/reducer.js
+++ b/packages/edit-post/src/store/reducer.js
@@ -252,6 +252,20 @@ function isInserterOpened( state = false, action ) {
return state;
}
+/**
+ * Reducer tracking whether the inserter is open.
+ *
+ * @param {boolean} state
+ * @param {Object} action
+ */
+function isEditingTemplate( state = false, action ) {
+ switch ( action.type ) {
+ case 'SET_IS_EDITING_TEMPLATE':
+ return action.value;
+ }
+ return state;
+}
+
const metaBoxes = combineReducers( {
isSaving: isSavingMetaBoxes,
locations: metaBoxLocations,
@@ -265,4 +279,5 @@ export default combineReducers( {
removedPanels,
deviceType,
isInserterOpened,
+ isEditingTemplate,
} );
diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js
index 68dcc83671ed00..69d1ded82cf568 100644
--- a/packages/edit-post/src/store/selectors.js
+++ b/packages/edit-post/src/store/selectors.js
@@ -324,3 +324,14 @@ export function __experimentalGetPreviewDeviceType( state ) {
export function isInserterOpened( state ) {
return state.isInserterOpened;
}
+
+/**
+ * Returns true if the template editing mode is enabled.
+ *
+ * @param {Object} state Global application state.
+ *
+ * @return {boolean} Whether we're editing the template.
+ */
+export function isEditingTemplate( state ) {
+ return state.isEditingTemplate;
+}
diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss
index cc64fba010a678..f902e64e5848ba 100644
--- a/packages/edit-post/src/style.scss
+++ b/packages/edit-post/src/style.scss
@@ -3,6 +3,7 @@
@import "./components/header/fullscreen-mode-close/style.scss";
@import "./components/header/header-toolbar/style.scss";
@import "./components/header/more-menu/style.scss";
+@import "./components/header/template-title/style.scss";
@import "./components/keyboard-shortcut-help-modal/style.scss";
@import "./components/layout/style.scss";
@import "./components/manage-blocks-modal/style.scss";
@@ -15,6 +16,7 @@
@import "./components/sidebar/post-schedule/style.scss";
@import "./components/sidebar/post-slug/style.scss";
@import "./components/sidebar/post-status/style.scss";
+@import "./components/sidebar/post-template/style.scss";
@import "./components/sidebar/post-visibility/style.scss";
@import "./components/sidebar/settings-header/style.scss";
@import "./components/text-editor/style.scss";
@@ -68,12 +70,12 @@ body.block-editor-page {
right: 0;
bottom: 0;
left: 0;
- min-height: calc(100vh - #{ $admin-bar-height-big });
+ min-height: calc(100vh - #{$admin-bar-height-big});
}
// The WP header height changes at this breakpoint.
@include break-medium {
- min-height: calc(100vh - #{ $admin-bar-height });
+ min-height: calc(100vh - #{$admin-bar-height});
body.is-fullscreen-mode & {
min-height: 100vh;
diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js
index 546a0b38107e7e..d4d31398b94bc2 100644
--- a/packages/editor/src/components/entities-saved-states/index.js
+++ b/packages/editor/src/components/entities-saved-states/index.js
@@ -6,7 +6,7 @@ import { some, groupBy } from 'lodash';
/**
* WordPress dependencies
*/
-import { Button } from '@wordpress/components';
+import { Button, withFocusReturn } from '@wordpress/components';
import { __, sprintf, _n } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
import { useState, useCallback } from '@wordpress/element';
@@ -47,7 +47,7 @@ const PLACEHOLDER_PHRASES = {
),
};
-export default function EntitiesSavedStates( { isOpen, close } ) {
+function EntitiesSavedStates( { isOpen, close } ) {
const { dirtyEntityRecords } = useSelect( ( select ) => {
return {
dirtyEntityRecords: select(
@@ -165,3 +165,5 @@ export default function EntitiesSavedStates( { isOpen, close } ) {
) : null;
}
+
+export default withFocusReturn( EntitiesSavedStates );
diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss
index ece02ec75a38d4..e2e9203dec07ba 100644
--- a/packages/editor/src/components/entities-saved-states/style.scss
+++ b/packages/editor/src/components/entities-saved-states/style.scss
@@ -18,7 +18,6 @@
}
@include break-medium() {
- z-index: z-index(".entities-saved-states__panel {greater than small}");
top: $admin-bar-height;
left: auto;
width: $sidebar-width;
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js
index bd0ffb99021a2a..6c0d3f090b1ef1 100644
--- a/packages/editor/src/components/provider/index.js
+++ b/packages/editor/src/components/provider/index.js
@@ -27,6 +27,7 @@ import { ReusableBlocksMenuItems } from '@wordpress/reusable-blocks';
import withRegistryProvider from './with-registry-provider';
import { mediaUpload } from '../../utils';
import ConvertToGroupButtons from '../convert-to-group-buttons';
+import serializeBlocks from '../../store/utils/serialize-blocks';
/**
* Fetches link suggestions from the API. This function is an exact copy of a function found at:
@@ -214,6 +215,10 @@ class EditorProvider extends Component {
}
getDefaultBlockContext( postId, postType ) {
+ // To avoid infinite loops, the template CPT shouldn't provide itself as a post content.
+ if ( postType === 'wp_template' ) {
+ return {};
+ }
return { postId, postType };
}
@@ -225,6 +230,13 @@ class EditorProvider extends Component {
if ( this.props.settings !== prevProps.settings ) {
this.props.updateEditorSettings( this.props.settings );
}
+ if (
+ this.props.__unstableTemplate &&
+ this.props.__unstableTemplate.id !==
+ prevProps.__unstableTemplate?.id
+ ) {
+ this.props.setupTemplate( this.props.__unstableTemplate );
+ }
}
componentWillUnmount() {
@@ -297,21 +309,21 @@ class EditorProvider extends Component {
export default compose( [
withRegistryProvider,
- withSelect( ( select ) => {
+ withSelect( ( select, { __unstableTemplate, post } ) => {
const {
canUserUseUnfilteredHTML,
__unstableIsEditorReady: isEditorReady,
- getEditorBlocks,
getEditorSelectionStart,
getEditorSelectionEnd,
isPostTitleSelected,
} = select( 'core/editor' );
- const { canUser } = select( 'core' );
+ const { canUser, getEditedEntityRecord } = select( 'core' );
+ const { id, type } = __unstableTemplate ?? post;
return {
canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(),
isReady: isEditorReady(),
- blocks: getEditorBlocks(),
+ blocks: getEditedEntityRecord( 'postType', type, id ).blocks,
selectionStart: getEditorSelectionStart(),
selectionEnd: getEditorSelectionEnd(),
reusableBlocks: select( 'core' ).getEntityRecords(
@@ -327,30 +339,66 @@ export default compose( [
isPostTitleSelected: isPostTitleSelected && isPostTitleSelected(),
};
} ),
- withDispatch( ( dispatch ) => {
+ withDispatch( ( dispatch, props ) => {
const {
setupEditor,
updatePostLock,
- resetEditorBlocks,
updateEditorSettings,
__experimentalTearDownEditor,
undo,
+ __unstableSetupTemplate,
} = dispatch( 'core/editor' );
const { createWarningNotice } = dispatch( 'core/notices' );
+ const { __unstableCreateUndoLevel, editEntityRecord } = dispatch(
+ 'core'
+ );
+
+ // This is not breaking the withDispatch rule.
+ // eslint-disable-next-line no-restricted-syntax
+ function updateBlocks( blocks, options ) {
+ const {
+ post,
+ __unstableTemplate: template,
+ blocks: currentBlocks,
+ } = props;
+ const { id, type } = template ?? post;
+ const {
+ __unstableShouldCreateUndoLevel,
+ selectionStart,
+ selectionEnd,
+ } = options;
+ const edits = { blocks, selectionStart, selectionEnd };
+
+ if ( __unstableShouldCreateUndoLevel !== false ) {
+ const noChange = currentBlocks === edits.blocks;
+ if ( noChange ) {
+ return __unstableCreateUndoLevel( 'postType', type, id );
+ }
+
+ // We create a new function here on every persistent edit
+ // to make sure the edit makes the post dirty and creates
+ // a new undo level.
+ edits.content = ( { blocks: blocksForSerialization = [] } ) =>
+ serializeBlocks( blocksForSerialization );
+ }
+
+ editEntityRecord( 'postType', type, id, edits );
+ }
return {
setupEditor,
updatePostLock,
createWarningNotice,
- resetEditorBlocks,
+ resetEditorBlocks: updateBlocks,
updateEditorSettings,
resetEditorBlocksWithoutUndoLevel( blocks, options ) {
- resetEditorBlocks( blocks, {
+ updateBlocks( blocks, {
...options,
__unstableShouldCreateUndoLevel: false,
} );
},
tearDownEditor: __experimentalTearDownEditor,
+ setupTemplate: __unstableSetupTemplate,
undo,
};
} ),
diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js
index 51eba62c14505e..4b857af7276423 100644
--- a/packages/editor/src/store/actions.js
+++ b/packages/editor/src/store/actions.js
@@ -76,6 +76,27 @@ export function* setupEditor( post, edits, template ) {
}
}
+/**
+ * Initiliazes an FSE template into the core-data store.
+ * We could avoid this action entirely by having a fallback if the edit is undefined.
+ *
+ * @param {Object} template Template object.
+ */
+export function* __unstableSetupTemplate( template ) {
+ const blocks = parse( template.content.raw );
+ yield controls.dispatch(
+ 'core',
+ 'editEntityRecord',
+ 'postType',
+ template.type,
+ template.id,
+ {
+ blocks,
+ },
+ { __unstableShouldCreateUndoLevel: false }
+ );
+}
+
/**
* Returns an action object signalling that the editor is being destroyed and
* that any necessary state or side-effect cleanup should occur.
diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js
index 337d560998c5cf..5849de6a6521fa 100644
--- a/packages/editor/src/store/selectors.js
+++ b/packages/editor/src/store/selectors.js
@@ -1269,6 +1269,20 @@ export function getEditorBlocks( state ) {
return getEditedPostAttribute( state, 'blocks' ) || EMPTY_ARRAY;
}
+/**
+ * Checks whether a post is an auto-draft ignoring the optimistic transaction.
+ * This selector shouldn't be necessary. It's currently used as a workaround
+ * to avoid template resolution for auto-drafts which has a backend bug.
+ *
+ * @param {Object} state State.
+ * @return {boolean} Whether the post is "auto-draft" on the backend.
+ */
+export function __unstableIsAutodraftPost( state ) {
+ const post = getCurrentPost( state );
+ const isSaving = isSavingPost( state );
+ return isSaving || post.status === 'auto-draft';
+}
+
/**
* A block selection object.
*