diff --git a/packages/block-editor/src/components/preview-options/README.md b/packages/block-editor/src/components/preview-options/README.md
index 0a2e89a70c7d44..6e9a029fa83a57 100644
--- a/packages/block-editor/src/components/preview-options/README.md
+++ b/packages/block-editor/src/components/preview-options/README.md
@@ -28,23 +28,24 @@ const MyPreviewOptions = () => (
className="edit-post-post-preview-dropdown"
deviceType={ deviceType }
setDeviceType={ setPreviewDeviceType }
- >
-
-
-
- { __( 'Preview in new tab' ) }
-
- >
- }
- />
-
-
+ > { ( { onClose } ) => (
+
+
+
+ { __( 'Preview in new tab' ) }
+
+ >
+ }
+ onPreview={ onClose }
+ />
+
+
+ ) }
);
```
diff --git a/packages/block-editor/src/components/preview-options/index.js b/packages/block-editor/src/components/preview-options/index.js
index c9f9a6ff782e4f..c22109e7359d1d 100644
--- a/packages/block-editor/src/components/preview-options/index.js
+++ b/packages/block-editor/src/components/preview-options/index.js
@@ -54,7 +54,7 @@ export default function PreviewOptions( {
icon={ deviceIcons[ deviceType.toLowerCase() ] }
label={ label || __( 'Preview' ) }
>
- { () => (
+ { ( renderProps ) => (
<>
- { children }
+ { children( renderProps ) }
>
) }
diff --git a/packages/e2e-tests/specs/editor/various/publish-button.test.js b/packages/e2e-tests/specs/editor/various/publish-button.test.js
index b6461ef11bc5b4..90ef0950e535bb 100644
--- a/packages/e2e-tests/specs/editor/various/publish-button.test.js
+++ b/packages/e2e-tests/specs/editor/various/publish-button.test.js
@@ -43,19 +43,4 @@ describe( 'PostPublishButton', () => {
await page.$( '.editor-post-publish-button[aria-disabled="true"]' )
).not.toBeNull();
} );
-
- it( 'should be disabled when metabox is being saved', async () => {
- await canvas().type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable.
- expect(
- await page.$( '.editor-post-publish-button[aria-disabled="true"]' )
- ).toBeNull();
-
- await page.evaluate( () => {
- window.wp.data.dispatch( 'core/edit-post' ).requestMetaBoxUpdates();
- return true;
- } );
- expect(
- await page.$( '.editor-post-publish-button[aria-disabled="true"]' )
- ).not.toBeNull();
- } );
} );
diff --git a/packages/edit-post/src/components/device-preview/index.js b/packages/edit-post/src/components/device-preview/index.js
index 5c061c92fde8e0..ab38523969ace1 100644
--- a/packages/edit-post/src/components/device-preview/index.js
+++ b/packages/edit-post/src/components/device-preview/index.js
@@ -15,26 +15,22 @@ import { store as coreStore } from '@wordpress/core-data';
import { store as editPostStore } from '../../store';
export default function DevicePreview() {
- const {
- hasActiveMetaboxes,
- isPostSaveable,
- isSaving,
- isViewable,
- deviceType,
- } = useSelect( ( select ) => {
- const { getEditedPostAttribute } = select( editorStore );
- const { getPostType } = select( coreStore );
- const postType = getPostType( getEditedPostAttribute( 'type' ) );
+ const { hasActiveMetaboxes, isPostSaveable, isViewable, deviceType } =
+ useSelect( ( select ) => {
+ const { getEditedPostAttribute } = select( editorStore );
+ const { getPostType } = select( coreStore );
+ const postType = getPostType( getEditedPostAttribute( 'type' ) );
- return {
- hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(),
- isSaving: select( editPostStore ).isSavingMetaBoxes(),
- isPostSaveable: select( editorStore ).isEditedPostSaveable(),
- isViewable: postType?.viewable ?? false,
- deviceType:
- select( editPostStore ).__experimentalGetPreviewDeviceType(),
- };
- }, [] );
+ return {
+ hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(),
+ isPostSaveable: select( editorStore ).isEditedPostSaveable(),
+ isViewable: postType?.viewable ?? false,
+ deviceType:
+ select(
+ editPostStore
+ ).__experimentalGetPreviewDeviceType(),
+ };
+ }, [] );
const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } =
useDispatch( editPostStore );
@@ -46,26 +42,26 @@ export default function DevicePreview() {
setDeviceType={ setPreviewDeviceType }
label={ __( 'Preview' ) }
>
- { isViewable && (
-
-
-
- { __( 'Preview in new tab' ) }
-
- >
- }
- />
-
-
- ) }
+ { ( { onClose } ) =>
+ isViewable && (
+
+
+
+ { __( 'Preview in new tab' ) }
+
+ >
+ }
+ onPreview={ onClose }
+ />
+
+
+ )
+ }
);
}
diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js
index 3f42d4736f57bb..9c9462a641dd33 100644
--- a/packages/edit-post/src/components/header/index.js
+++ b/packages/edit-post/src/components/header/index.js
@@ -32,22 +32,17 @@ const slideX = {
function Header( { setEntitiesSavedStatesCallback } ) {
const isLargeViewport = useViewportMatch( 'large' );
- const {
- hasActiveMetaboxes,
- isPublishSidebarOpened,
- isSaving,
- showIconLabels,
- } = useSelect(
- ( select ) => ( {
- hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(),
- isPublishSidebarOpened:
- select( editPostStore ).isPublishSidebarOpened(),
- isSaving: select( editPostStore ).isSavingMetaBoxes(),
- showIconLabels:
- select( editPostStore ).isFeatureActive( 'showIconLabels' ),
- } ),
- []
- );
+ const { hasActiveMetaboxes, isPublishSidebarOpened, showIconLabels } =
+ useSelect(
+ ( select ) => ( {
+ hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(),
+ isPublishSidebarOpened:
+ select( editPostStore ).isPublishSidebarOpened(),
+ showIconLabels:
+ select( editPostStore ).isFeatureActive( 'showIconLabels' ),
+ } ),
+ []
+ );
return (
@@ -82,19 +77,14 @@ function Header( { setEntitiesSavedStatesCallback } ) {
// when the publish sidebar has been closed.
) }
-
+
{
- return {
+ } = useSelect(
+ ( select ) => ( {
publishSidebarOpened:
select( editPostStore ).isPublishSidebarOpened(),
hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(),
- isSavingMetaBoxes: select( editPostStore ).isSavingMetaBoxes(),
hasNonPostEntityChanges:
select( editorStore ).hasNonPostEntityChanges(),
- };
- }, [] );
+ } ),
+ []
+ );
const openEntitiesSavedStates = useCallback(
() => setEntitiesSavedStatesCallback( true ),
@@ -57,7 +56,6 @@ export default function ActionsPanel( {
diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js
index f938a9837516e0..0ee1efb62b02ea 100644
--- a/packages/edit-post/src/store/actions.js
+++ b/packages/edit-post/src/store/actions.js
@@ -11,6 +11,7 @@ import { store as coreStore } from '@wordpress/core-data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as editorStore } from '@wordpress/editor';
import deprecated from '@wordpress/deprecated';
+import { addFilter } from '@wordpress/hooks';
/**
* Internal dependencies
@@ -567,33 +568,23 @@ export const initializeMetaBoxes =
metaBoxesInitialized = true;
- let wasSavingPost = registry.select( editorStore ).isSavingPost();
- let wasAutosavingPost = registry
- .select( editorStore )
- .isAutosavingPost();
-
- // Save metaboxes when performing a full save on the post.
- registry.subscribe( async () => {
- const isSavingPost = registry.select( editorStore ).isSavingPost();
- const isAutosavingPost = registry
- .select( editorStore )
- .isAutosavingPost();
-
- // Save metaboxes on save completion, except for autosaves.
- const shouldTriggerMetaboxesSave =
- wasSavingPost &&
- ! wasAutosavingPost &&
- ! isSavingPost &&
- select.hasMetaBoxes();
-
- // Save current state for next inspection.
- wasSavingPost = isSavingPost;
- wasAutosavingPost = isAutosavingPost;
-
- if ( shouldTriggerMetaboxesSave ) {
- await dispatch.requestMetaBoxUpdates();
- }
- } );
+ // Save metaboxes on save completion, except for autosaves.
+ addFilter(
+ 'editor.__unstableSavePost',
+ 'core/edit-post/save-metaboxes',
+ ( previous, options ) =>
+ previous.then( () => {
+ if ( options.isAutosave ) {
+ return;
+ }
+
+ if ( ! select.hasMetaBoxes() ) {
+ return;
+ }
+
+ return dispatch.requestMetaBoxUpdates();
+ } )
+ );
dispatch( {
type: 'META_BOXES_INITIALIZED',
diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js
index 40ffc805565e6a..dbe985734567f2 100644
--- a/packages/edit-site/src/components/header-edit-mode/index.js
+++ b/packages/edit-site/src/components/header-edit-mode/index.js
@@ -323,21 +323,24 @@ export default function HeaderEditMode() {
setDeviceType={ setPreviewDeviceType }
label={ __( 'View' ) }
>
-
-
-
+ { ( { onClose } ) => (
+
+
+
+ ) }
) }
diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js
index b6500e8fbd37d4..9a9aa92d210c3d 100644
--- a/packages/editor/src/components/post-preview-button/index.js
+++ b/packages/editor/src/components/post-preview-button/index.js
@@ -1,16 +1,10 @@
-/**
- * External dependencies
- */
-import classnames from 'classnames';
-
/**
* WordPress dependencies
*/
-import { Component, createRef, renderToString } from '@wordpress/element';
+import { renderToString } from '@wordpress/element';
import { Button, Path, SVG, VisuallyHidden } from '@wordpress/components';
import { __, _x } from '@wordpress/i18n';
-import { withSelect, withDispatch } from '@wordpress/data';
-import { ifCondition, compose } from '@wordpress/compose';
+import { useSelect, useDispatch } from '@wordpress/data';
import { applyFilters } from '@wordpress/hooks';
import { store as coreStore } from '@wordpress/core-data';
@@ -105,48 +99,40 @@ function writeInterstitialMessage( targetDocument ) {
targetDocument.close();
}
-export class PostPreviewButton extends Component {
- constructor() {
- super( ...arguments );
-
- this.buttonRef = createRef();
-
- this.openPreviewWindow = this.openPreviewWindow.bind( this );
- }
-
- componentDidUpdate( prevProps ) {
- const { previewLink } = this.props;
- // This relies on the window being responsible to unset itself when
- // navigation occurs or a new preview window is opened, to avoid
- // unintentional forceful redirects.
- if ( previewLink && ! prevProps.previewLink ) {
- this.setPreviewWindowLink( previewLink );
- }
- }
-
- /**
- * Sets the preview window's location to the given URL, if a preview window
- * exists and is not closed.
- *
- * @param {string} url URL to assign as preview window location.
- */
- setPreviewWindowLink( url ) {
- const { previewWindow } = this;
-
- if ( previewWindow && ! previewWindow.closed ) {
- previewWindow.location = url;
- if ( this.buttonRef.current ) {
- this.buttonRef.current.focus();
- }
- }
+export default function PostPreviewButton( {
+ className,
+ textContent,
+ forceIsAutosaveable,
+ role,
+ onPreview,
+} ) {
+ const { postId, currentPostLink, previewLink, isSaveable, isViewable } =
+ useSelect( ( select ) => {
+ const editor = select( editorStore );
+ const core = select( coreStore );
+
+ const postType = core.getPostType(
+ editor.getCurrentPostType( 'type' )
+ );
+
+ return {
+ postId: editor.getCurrentPostId(),
+ currentPostLink: editor.getCurrentPostAttribute( 'link' ),
+ previewLink: editor.getEditedPostPreviewLink(),
+ isSaveable: editor.isEditedPostSaveable(),
+ isViewable: postType?.viewable ?? false,
+ };
+ }, [] );
+
+ const { __unstableSaveForPreview } = useDispatch( editorStore );
+
+ if ( ! isViewable ) {
+ return null;
}
- getWindowTarget() {
- const { postId } = this.props;
- return `wp-preview-${ postId }`;
- }
+ const targetId = `wp-preview-${ postId }`;
- openPreviewWindow( event ) {
+ const openPreviewWindow = async ( event ) => {
// Our Preview button has its 'href' and 'target' set correctly for a11y
// purposes. Unfortunately, though, we can't rely on the default 'click'
// handler since sometimes it incorrectly opens a new tab instead of reusing
@@ -155,117 +141,48 @@ export class PostPreviewButton extends Component {
event.preventDefault();
// Open up a Preview tab if needed. This is where we'll show the preview.
- if ( ! this.previewWindow || this.previewWindow.closed ) {
- this.previewWindow = window.open( '', this.getWindowTarget() );
- }
+ const previewWindow = window.open( '', targetId );
// Focus the Preview tab. This might not do anything, depending on the browser's
// and user's preferences.
// https://html.spec.whatwg.org/multipage/interaction.html#dom-window-focus
- this.previewWindow.focus();
-
- if (
- // If we don't need to autosave the post before previewing, then we simply
- // load the Preview URL in the Preview tab.
- ! this.props.isAutosaveable ||
- // Do not save or overwrite the post, if the post is already locked.
- this.props.isPostLocked
- ) {
- this.setPreviewWindowLink( event.target.href );
- return;
- }
-
- // Request an autosave. This happens asynchronously and causes the component
- // to update when finished.
- if ( this.props.isDraft ) {
- this.props.savePost( { isPreview: true } );
- } else {
- this.props.autosave( { isPreview: true } );
- }
-
- // Display a 'Generating preview' message in the Preview tab while we wait for the
- // autosave to finish.
- writeInterstitialMessage( this.previewWindow.document );
- }
-
- render() {
- const { previewLink, currentPostLink, isSaveable, role } = this.props;
-
- // Link to the `?preview=true` URL if we have it, since this lets us see
- // changes that were autosaved since the post was last published. Otherwise,
- // just link to the post's URL.
- const href = previewLink || currentPostLink;
-
- const classNames = classnames(
- {
- 'editor-post-preview': ! this.props.className,
- },
- this.props.className
- );
-
- return (
-
- );
- }
+ previewWindow.focus();
+
+ writeInterstitialMessage( previewWindow.document );
+
+ const link = await __unstableSaveForPreview( { forceIsAutosaveable } );
+
+ previewWindow.location = link;
+
+ onPreview?.();
+ };
+
+ // Link to the `?preview=true` URL if we have it, since this lets us see
+ // changes that were autosaved since the post was last published. Otherwise,
+ // just link to the post's URL.
+ const href = previewLink || currentPostLink;
+
+ return (
+
+ );
}
-
-export default compose( [
- withSelect( ( select, { forcePreviewLink, forceIsAutosaveable } ) => {
- const {
- getCurrentPostId,
- getCurrentPostAttribute,
- getEditedPostAttribute,
- isEditedPostSaveable,
- isEditedPostAutosaveable,
- getEditedPostPreviewLink,
- isPostLocked,
- } = select( editorStore );
- const { getPostType } = select( coreStore );
-
- const previewLink = getEditedPostPreviewLink();
- const postType = getPostType( getEditedPostAttribute( 'type' ) );
-
- return {
- postId: getCurrentPostId(),
- currentPostLink: getCurrentPostAttribute( 'link' ),
- previewLink:
- forcePreviewLink !== undefined ? forcePreviewLink : previewLink,
- isSaveable: isEditedPostSaveable(),
- isAutosaveable: forceIsAutosaveable || isEditedPostAutosaveable(),
- isViewable: postType?.viewable ?? false,
- isDraft:
- [ 'draft', 'auto-draft' ].indexOf(
- getEditedPostAttribute( 'status' )
- ) !== -1,
- isPostLocked: isPostLocked(),
- };
- } ),
- withDispatch( ( dispatch ) => ( {
- autosave: dispatch( editorStore ).autosave,
- savePost: dispatch( editorStore ).savePost,
- } ) ),
- ifCondition( ( { isViewable } ) => isViewable ),
-] )( PostPreviewButton );
diff --git a/packages/editor/src/components/post-preview-button/test/index.js b/packages/editor/src/components/post-preview-button/test/index.js
index 184550d25e4aa1..e34c05caa178bd 100644
--- a/packages/editor/src/components/post-preview-button/test/index.js
+++ b/packages/editor/src/components/post-preview-button/test/index.js
@@ -4,10 +4,39 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+/**
+ * WordPress dependencies
+ */
+import { useSelect, useDispatch } from '@wordpress/data';
+
/**
* Internal dependencies
*/
-import { PostPreviewButton } from '../';
+import PostPreviewButton from '..';
+
+jest.useRealTimers();
+
+jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() );
+jest.mock( '@wordpress/data/src/components/use-dispatch/use-dispatch', () =>
+ jest.fn()
+);
+
+function mockUseSelect( overrides ) {
+ useSelect.mockImplementation( ( map ) =>
+ map( () => ( {
+ getPostType: () => ( { viewable: true } ),
+ getCurrentPostId: () => 123,
+ getCurrentPostType: () => 'post',
+ getCurrentPostAttribute: () => undefined,
+ getEditedPostPreviewLink: () => undefined,
+ isEditedPostSaveable: () => false,
+ ...overrides,
+ } ) )
+ );
+ useDispatch.mockImplementation( () => ( {
+ __unstableSaveForPreview: () => Promise.resolve(),
+ } ) );
+}
describe( 'PostPreviewButton', () => {
const documentWrite = jest.fn();
@@ -42,33 +71,39 @@ describe( 'PostPreviewButton', () => {
} );
it( 'should render with `editor-post-preview` class if no `className` is specified.', () => {
+ mockUseSelect();
+
render( );
- expect( screen.getByRole( 'button' ) ).toHaveClass(
- 'editor-post-preview'
- );
+ const button = screen.getByRole( 'button' );
+ expect( button ).toHaveClass( 'editor-post-preview' );
} );
it( 'should render with a custom class and not `editor-post-preview` if `className` is specified.', () => {
+ mockUseSelect();
+
render( );
const button = screen.getByRole( 'button' );
-
expect( button ).toHaveClass( 'foo-bar' );
expect( button ).not.toHaveClass( 'editor-post-preview' );
} );
it( 'should render a tertiary button if no classname is specified.', () => {
+ mockUseSelect();
+
render( );
- expect( screen.getByRole( 'button' ) ).toHaveClass( 'is-tertiary' );
+ const button = screen.getByRole( 'button' );
+ expect( button ).toHaveClass( 'is-tertiary' );
} );
it( 'should render the button in its default variant if a custom classname is specified.', () => {
+ mockUseSelect();
+
render( );
const button = screen.getByRole( 'button' );
-
expect( button ).not.toHaveClass( 'is-primary' );
expect( button ).not.toHaveClass( 'is-secondary' );
expect( button ).not.toHaveClass( 'is-tertiary' );
@@ -76,12 +111,13 @@ describe( 'PostPreviewButton', () => {
} );
it( 'should render `textContent` if specified.', () => {
+ mockUseSelect();
+
const textContent = 'Foo bar';
render( );
const button = screen.getByRole( 'button' );
-
expect( button ).toHaveTextContent( textContent );
expect(
within( button ).queryByText( 'Preview' )
@@ -92,213 +128,113 @@ describe( 'PostPreviewButton', () => {
} );
it( 'should render `Preview` with accessibility text if `textContent` not specified.', () => {
+ mockUseSelect();
+
render( );
const button = screen.getByRole( 'button' );
-
expect( within( button ).getByText( 'Preview' ) ).toBeVisible();
expect(
within( button ).getByText( '(opens in a new tab)' )
).toBeInTheDocument();
} );
- it( 'should be disabled if post is not saveable.', async () => {
- render( );
+ it( 'should be disabled if post is not saveable.', () => {
+ mockUseSelect( { isEditedPostSaveable: () => false } );
+
+ render( );
expect( screen.getByRole( 'button' ) ).toBeDisabled();
} );
- it( 'should not be disabled if post is saveable.', async () => {
- render( );
+ it( 'should not be disabled if post is saveable.', () => {
+ mockUseSelect( { isEditedPostSaveable: () => true } );
+
+ render( );
expect( screen.getByRole( 'button' ) ).toBeEnabled();
} );
- it( 'should set `href` to `previewLink` if `previewLink` is specified.', async () => {
+ it( 'should set `href` to edited post preview link if specified.', () => {
const url = 'https://wordpress.org';
+ mockUseSelect( {
+ getEditedPostPreviewLink: () => url,
+ isEditedPostSaveable: () => true,
+ } );
- render(
-
- );
+ render( );
expect( screen.getByRole( 'link' ) ).toHaveAttribute( 'href', url );
} );
- it( 'should set `href` to `currentPostLink` if `currentPostLink` is specified.', async () => {
+ it( 'should set `href` to current post link if specified.', () => {
const url = 'https://wordpress.org';
+ mockUseSelect( {
+ getCurrentPostAttribute: () => url,
+ isEditedPostSaveable: () => true,
+ } );
- render(
-
- );
+ render( );
expect( screen.getByRole( 'link' ) ).toHaveAttribute( 'href', url );
} );
- it( 'should prioritize `previewLink` if both `previewLink` and `currentPostLink` are specified.', async () => {
+ it( 'should prioritize preview link if both preview link and link attribute are specified.', () => {
const url1 = 'https://wordpress.org';
const url2 = 'https://wordpress.com';
+ mockUseSelect( {
+ getEditedPostPreviewLink: () => url1,
+ getCurrentPostAttribute: () => url2,
+ isEditedPostSaveable: () => true,
+ } );
- render(
-
- );
+ render( );
expect( screen.getByRole( 'link' ) ).toHaveAttribute( 'href', url1 );
} );
- it( 'should properly set target to `wp-preview-${ postId }`.', async () => {
- const postId = 123;
- const url = 'https://wordpress.org';
+ it( 'should properly set link target', () => {
+ mockUseSelect( {
+ getEditedPostPreviewLink: () => 'https://wordpress.org',
+ isEditedPostSaveable: () => true,
+ } );
- render(
-
- );
+ render( );
expect( screen.getByRole( 'link' ) ).toHaveAttribute(
'target',
- `wp-preview-${ postId }`
- );
- } );
-
- it( 'should save post if `isDraft` is `true`', async () => {
- const user = userEvent.setup();
- const url = 'https://wordpress.org';
- const savePost = jest.fn();
- const autosave = jest.fn();
-
- render(
-
- );
-
- await user.click( screen.getByRole( 'link' ) );
-
- expect( savePost ).toHaveBeenCalledWith(
- expect.objectContaining( { isPreview: true } )
- );
- expect( autosave ).not.toHaveBeenCalled();
- } );
-
- it( 'should autosave post if `isDraft` is `false`', async () => {
- const user = userEvent.setup();
- const url = 'https://wordpress.org';
- const savePost = jest.fn();
- const autosave = jest.fn();
-
- render(
-
- );
-
- await user.click( screen.getByRole( 'link' ) );
-
- expect( savePost ).not.toHaveBeenCalled();
- expect( autosave ).toHaveBeenCalledWith(
- expect.objectContaining( { isPreview: true } )
+ 'wp-preview-123'
);
} );
it( 'should open a window with the specified target', async () => {
const user = userEvent.setup();
- const postId = 123;
- const url = 'https://wordpress.org';
- render(
-
- );
+ mockUseSelect( {
+ getEditedPostPreviewLink: () => 'https://wordpress.org',
+ isEditedPostSaveable: () => true,
+ } );
+
+ render( );
await user.click( screen.getByRole( 'link' ) );
- expect( global.open ).toHaveBeenCalledWith(
- '',
- `wp-preview-${ postId }`
- );
+ expect( global.open ).toHaveBeenCalledWith( '', 'wp-preview-123' );
} );
- it( 'should set the location in the window properly', async () => {
+ it( 'should display a `Generating preview` message while waiting for autosaving', async () => {
const user = userEvent.setup();
- const postId = 123;
- const url = 'https://wordpress.org';
-
- const { rerender } = render(
-
- );
-
- await user.click( screen.getByRole( 'button' ) );
- expect( setLocation ).toHaveBeenCalledWith( undefined );
+ mockUseSelect( {
+ getEditedPostPreviewLink: () => 'https://wordpress.org',
+ isEditedPostSaveable: () => true,
+ } );
- rerender(
-
- );
+ render( );
- expect( setLocation ).toHaveBeenCalledWith( url );
- } );
+ await user.click( screen.getByRole( 'link' ) );
- it( 'should display a `Generating preview` message while waiting for autosaving', async () => {
- const user = userEvent.setup();
const previewText = 'Generating preview…';
- const url = 'https://wordpress.org';
- const savePost = jest.fn();
- const autosave = jest.fn();
-
- render(
-
- );
-
- await user.click( screen.getByRole( 'link' ) );
expect( documentWrite ).toHaveBeenCalledWith(
expect.stringContaining( previewText )
diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js
index b4ea3a07258455..b9140b733c9d30 100644
--- a/packages/editor/src/components/post-publish-button/index.js
+++ b/packages/editor/src/components/post-publish-button/index.js
@@ -102,7 +102,6 @@ export class PostPublishButton extends Component {
render() {
const {
forceIsDirty,
- forceIsSaving,
hasPublishAction,
isBeingScheduled,
isOpen,
@@ -124,7 +123,6 @@ export class PostPublishButton extends Component {
const isButtonDisabled =
( isSaving ||
- forceIsSaving ||
! isSaveable ||
isPostSavingLocked ||
( ! isPublishable && ! forceIsDirty ) ) &&
@@ -133,7 +131,6 @@ export class PostPublishButton extends Component {
const isToggleDisabled =
( isPublished ||
isSaving ||
- forceIsSaving ||
! isSaveable ||
( ! isPublishable && ! forceIsDirty ) ) &&
( ! hasNonPostEntityChanges || isSavingNonPostEntityChanges );
@@ -187,7 +184,6 @@ export class PostPublishButton extends Component {
: __( 'Publish' );
const buttonChildren = (
);
@@ -231,10 +227,9 @@ export default compose( [
hasNonPostEntityChanges,
isSavingNonPostEntityChanges,
} = select( editorStore );
- const _isAutoSaving = isAutosavingPost();
return {
- isSaving: isSavingPost() || _isAutoSaving,
- isAutoSaving: _isAutoSaving,
+ isSaving: isSavingPost(),
+ isAutoSaving: isAutosavingPost(),
isBeingScheduled: isEditedPostBeingScheduled(),
visibility: getEditedPostVisibility(),
isSaveable: isEditedPostSaveable(),
diff --git a/packages/editor/src/components/post-publish-button/label.js b/packages/editor/src/components/post-publish-button/label.js
index 05c7eb0f633e5b..0a0fdd30d8500d 100644
--- a/packages/editor/src/components/post-publish-button/label.js
+++ b/packages/editor/src/components/post-publish-button/label.js
@@ -44,7 +44,7 @@ export function PublishButtonLabel( {
}
export default compose( [
- withSelect( ( select, { forceIsSaving } ) => {
+ withSelect( ( select ) => {
const {
isCurrentPostPublished,
isEditedPostBeingScheduled,
@@ -57,7 +57,7 @@ export default compose( [
return {
isPublished: isCurrentPostPublished(),
isBeingScheduled: isEditedPostBeingScheduled(),
- isSaving: forceIsSaving || isSavingPost(),
+ isSaving: isSavingPost(),
isPublishing: isPublishingPost(),
hasPublishAction:
getCurrentPost()._links?.[ 'wp:action-publish' ] ?? false,
diff --git a/packages/editor/src/components/post-publish-button/test/index.js b/packages/editor/src/components/post-publish-button/test/index.js
index f1177c0a0288a5..b0a8c3c5129c98 100644
--- a/packages/editor/src/components/post-publish-button/test/index.js
+++ b/packages/editor/src/components/post-publish-button/test/index.js
@@ -19,16 +19,6 @@ describe( 'PostPublishButton', () => {
).toHaveAttribute( 'aria-disabled', 'true' );
} );
- it( 'should be true if forceIsSaving is true', () => {
- render(
-
- );
-
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveAttribute( 'aria-disabled', 'true' );
- } );
-
it( 'should be true if post is not publishable and not forceIsDirty', () => {
render(
diff --git a/packages/editor/src/components/post-saved-state/index.js b/packages/editor/src/components/post-saved-state/index.js
index 24b88d4d96dee1..ed3115d5a6c54a 100644
--- a/packages/editor/src/components/post-saved-state/index.js
+++ b/packages/editor/src/components/post-saved-state/index.js
@@ -29,14 +29,11 @@ import { store as editorStore } from '../../store';
* @param {Object} props Component props.
* @param {?boolean} props.forceIsDirty Whether to force the post to be marked
* as dirty.
- * @param {?boolean} props.forceIsSaving Whether to force the post to be marked
- * as being saved.
* @param {?boolean} props.showIconLabels Whether interface buttons show labels instead of icons
* @return {import('@wordpress/element').WPComponent} The component.
*/
export default function PostSavedState( {
forceIsDirty,
- forceIsSaving,
showIconLabels = false,
} ) {
const [ forceSavedMessage, setForceSavedMessage ] = useState( false );
@@ -72,14 +69,14 @@ export default function PostSavedState( {
isNew: isEditedPostNew(),
isPending: 'pending' === getEditedPostAttribute( 'status' ),
isPublished: isCurrentPostPublished(),
- isSaving: forceIsSaving || isSavingPost(),
+ isSaving: isSavingPost(),
isSaveable: isEditedPostSaveable(),
isScheduled: isCurrentPostScheduled(),
hasPublishAction:
getCurrentPost()?._links?.[ 'wp:action-publish' ] ?? false,
};
},
- [ forceIsDirty, forceIsSaving ]
+ [ forceIsDirty ]
);
const { savePost } = useDispatch( editorStore );
diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js
index 390c19571542dc..ece8d62f689a56 100644
--- a/packages/editor/src/store/actions.js
+++ b/packages/editor/src/store/actions.js
@@ -11,6 +11,7 @@ import {
import { store as noticesStore } from '@wordpress/notices';
import { store as coreStore } from '@wordpress/core-data';
import { store as blockEditorStore } from '@wordpress/block-editor';
+import { applyFilters } from '@wordpress/hooks';
import { store as preferencesStore } from '@wordpress/preferences';
/**
@@ -177,15 +178,26 @@ export const savePost =
edits,
options
);
- dispatch( { type: 'REQUEST_POST_UPDATE_FINISH', options } );
- const error = registry
+ let error = registry
.select( coreStore )
.getLastEntitySaveError(
'postType',
previousRecord.type,
previousRecord.id
);
+
+ if ( ! error ) {
+ await applyFilters(
+ 'editor.__unstableSavePost',
+ Promise.resolve(),
+ options
+ ).catch( ( err ) => {
+ error = err;
+ } );
+ }
+ dispatch( { type: 'REQUEST_POST_UPDATE_FINISH', options } );
+
if ( error ) {
const args = getNotificationArgumentsForSaveFail( {
post: previousRecord,
@@ -289,6 +301,26 @@ export const autosave =
}
};
+export const __unstableSaveForPreview =
+ ( { forceIsAutosaveable } ) =>
+ async ( { select, dispatch } ) => {
+ if (
+ ( forceIsAutosaveable || select.isEditedPostAutosaveable() ) &&
+ ! select.isPostLocked()
+ ) {
+ const isDraft = [ 'draft', 'auto-draft' ].includes(
+ select.getEditedPostAttribute( 'status' )
+ );
+ if ( isDraft ) {
+ await dispatch.savePost( { isPreview: true } );
+ } else {
+ await dispatch.autosave( { isPreview: true } );
+ }
+ }
+
+ return select.getEditedPostPreviewLink();
+ };
+
/**
* Action that restores last popped state in undo history.
*/
diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js
index 51b307dce9e7c4..a533851751b183 100644
--- a/packages/editor/src/store/selectors.js
+++ b/packages/editor/src/store/selectors.js
@@ -681,15 +681,9 @@ export function isDeletingPost( state ) {
*
* @return {boolean} Whether post is being saved.
*/
-export const isSavingPost = createRegistrySelector( ( select ) => ( state ) => {
- const postType = getCurrentPostType( state );
- const postId = getCurrentPostId( state );
- return select( coreStore ).isSavingEntityRecord(
- 'postType',
- postType,
- postId
- );
-} );
+export function isSavingPost( state ) {
+ return !! state.saving.pending;
+}
/**
* Returns true if non-post entities are currently being saved, or false otherwise.
@@ -760,10 +754,7 @@ export const didPostSaveRequestFail = createRegistrySelector(
* @return {boolean} Whether the post is autosaving.
*/
export function isAutosavingPost( state ) {
- if ( ! isSavingPost( state ) ) {
- return false;
- }
- return Boolean( state.saving.options?.isAutosave );
+ return isSavingPost( state ) && Boolean( state.saving.options?.isAutosave );
}
/**
@@ -774,10 +765,7 @@ export function isAutosavingPost( state ) {
* @return {boolean} Whether the post is being previewed.
*/
export function isPreviewingPost( state ) {
- if ( ! isSavingPost( state ) ) {
- return false;
- }
- return Boolean( state.saving.options?.isPreview );
+ return isSavingPost( state ) && Boolean( state.saving.options?.isPreview );
}
/**
diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js
index 4b29e251f80c02..2a22b4523444ba 100644
--- a/packages/editor/src/store/test/selectors.js
+++ b/packages/editor/src/store/test/selectors.js
@@ -75,10 +75,6 @@ selectorNames.forEach( ( name ) => {
};
},
- isSavingEntityRecord() {
- return state.saving && state.saving.requesting;
- },
-
getLastEntitySaveError() {
const saving = state.saving;
const successful = saving && saving.successful;
@@ -1254,7 +1250,7 @@ describe( 'selectors', () => {
title: 'sassel',
},
saving: {
- requesting: true,
+ pending: true,
},
};
@@ -1403,9 +1399,8 @@ describe( 'selectors', () => {
currentPost: {
title: 'sassel',
},
- saving: {
- requesting: true,
- },
+ postAutosavingLock: {},
+ saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return false;
@@ -1434,9 +1429,8 @@ describe( 'selectors', () => {
currentPost: {
title: 'sassel',
},
- saving: {
- requesting: true,
- },
+ postAutosavingLock: {},
+ saving: {},
getCurrentUser() {},
hasFetchedAutosaves() {
return true;
@@ -2017,7 +2011,7 @@ describe( 'selectors', () => {
it( 'should return true if the post is currently being saved', () => {
const state = {
saving: {
- requesting: true,
+ pending: true,
},
};
@@ -2027,7 +2021,7 @@ describe( 'selectors', () => {
it( 'should return false if the post is not currently being saved', () => {
const state = {
saving: {
- requesting: false,
+ pending: false,
},
};