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' ) } > - - - { __( 'View site' ) } - - { - /* translators: accessibility text */ - __( '(opens in a new tab)' ) - } - - - + { ( { onClose } ) => ( + + + { __( 'View site' ) } + + { + /* translators: accessibility text */ + __( '(opens in a new tab)' ) + } + + + + ) }
) } 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, }, };