diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js
index 0de16662485ccb..64569e7a71162c 100644
--- a/packages/editor/src/components/post-publish-button/index.js
+++ b/packages/editor/src/components/post-publish-button/index.js
@@ -2,9 +2,7 @@
* WordPress dependencies
*/
import { Button } from '@wordpress/components';
-import { Component } from '@wordpress/element';
-import { withSelect, withDispatch } from '@wordpress/data';
-import { compose } from '@wordpress/compose';
+import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
@@ -14,220 +12,173 @@ import { store as editorStore } from '../../store';
const noop = () => {};
-export class PostPublishButton extends Component {
- constructor( props ) {
- super( props );
+export function PostPublishButton( {
+ forceIsDirty,
+ isOpen,
+ isToggle,
+ onSubmit = noop,
+ onToggle,
+ setEntitiesSavedStatesCallback,
+} ) {
+ const {
+ hasPublishAction,
+ isBeingScheduled,
+ isPostSavingLocked,
+ isPublishable,
+ isPublished,
+ isSaveable,
+ isSaving,
+ isAutoSaving,
+ visibility,
+ hasNonPostEntityChanges,
+ isSavingNonPostEntityChanges,
+ postStatus,
+ postStatusHasChanged,
+ postType,
+ postId,
+ } = useSelect( ( select ) => {
+ const store = select( editorStore );
+ return {
+ isSaving: store.isSavingPost(),
+ isAutoSaving: store.isAutosavingPost(),
+ isBeingScheduled: store.isEditedPostBeingScheduled(),
+ visibility: store.getEditedPostVisibility(),
+ isSaveable: store.isEditedPostSaveable(),
+ isPostSavingLocked: store.isPostSavingLocked(),
+ isPublishable: store.isEditedPostPublishable(),
+ isPublished: store.isCurrentPostPublished(),
+ hasPublishAction:
+ store.getCurrentPost()._links?.[ 'wp:action-publish' ] ?? false,
+ postType: store.getCurrentPostType(),
+ postId: store.getCurrentPostId(),
+ postStatus: store.getEditedPostAttribute( 'status' ),
+ postStatusHasChanged: store.getPostEdits()?.status,
+ hasNonPostEntityChanges: store.hasNonPostEntityChanges(),
+ isSavingNonPostEntityChanges: store.isSavingNonPostEntityChanges(),
+ };
+ }, [] );
- this.createOnClick = this.createOnClick.bind( this );
- this.closeEntitiesSavedStates =
- this.closeEntitiesSavedStates.bind( this );
+ const { editPost, savePost } = useDispatch( editorStore );
- this.state = {
- entitiesSavedStatesCallback: false,
- };
- }
+ const savePostStatus = ( status ) => {
+ editPost( { status }, { undoIgnore: true } );
+ savePost();
+ };
- createOnClick( callback ) {
- return ( ...args ) => {
- const { hasNonPostEntityChanges, setEntitiesSavedStatesCallback } =
- this.props;
+ const createOnClick =
+ ( callback ) =>
+ ( ...args ) => {
// If a post with non-post entities is published, but the user
// elects to not save changes to the non-post entities, those
// entities will still be dirty when the Publish button is clicked.
// We also need to check that the `setEntitiesSavedStatesCallback`
// prop was passed. See https://github.com/WordPress/gutenberg/pull/37383
if ( hasNonPostEntityChanges && setEntitiesSavedStatesCallback ) {
- // The modal for multiple entity saving will open,
- // hold the callback for saving/publishing the post
- // so that we can call it if the post entity is checked.
- this.setState( {
- entitiesSavedStatesCallback: () => callback( ...args ),
- } );
+ // The modal for multiple entity saving will open. If the post
+ // entity is checked when it closes, run the held callback.
+ const onClose = ( savedEntities ) => {
+ if (
+ savedEntities &&
+ savedEntities.some(
+ ( elt ) =>
+ elt.kind === 'postType' &&
+ elt.name === postType &&
+ elt.key === postId
+ )
+ ) {
+ callback( ...args );
+ }
+ };
// Open the save panel by setting its callback.
// To set a function on the useState hook, we must set it
// with another function (() => myFunction). Passing the
// function on its own will cause an error when called.
- setEntitiesSavedStatesCallback(
- () => this.closeEntitiesSavedStates
- );
+ setEntitiesSavedStatesCallback( () => onClose );
return noop;
}
return callback( ...args );
};
- }
- closeEntitiesSavedStates( savedEntities ) {
- const { postType, postId } = this.props;
- const { entitiesSavedStatesCallback } = this.state;
- this.setState( { entitiesSavedStatesCallback: false }, () => {
- if (
- savedEntities &&
- savedEntities.some(
- ( elt ) =>
- elt.kind === 'postType' &&
- elt.name === postType &&
- elt.key === postId
- )
- ) {
- // The post entity was checked, call the held callback from `createOnClick`.
- entitiesSavedStatesCallback();
- }
- } );
+ const isButtonDisabled =
+ isPostSavingLocked ||
+ ( ( isSaving ||
+ ! isSaveable ||
+ ( ! isPublishable && ! forceIsDirty ) ) &&
+ ( ! hasNonPostEntityChanges || isSavingNonPostEntityChanges ) );
+
+ const isToggleDisabled =
+ isPostSavingLocked ||
+ ( ( isPublished ||
+ isSaving ||
+ ! isSaveable ||
+ ( ! isPublishable && ! forceIsDirty ) ) &&
+ ( ! hasNonPostEntityChanges || isSavingNonPostEntityChanges ) );
+
+ // If the new status has not changed explicitly, we derive it from
+ // other factors, like having a publish action, etc.. We need to preserve
+ // this because it affects when to show the pre and post publish panels.
+ // If it has changed though explicitly, we need to respect that.
+ let publishStatus = 'publish';
+ if ( postStatusHasChanged ) {
+ publishStatus = postStatus;
+ } else if ( ! hasPublishAction ) {
+ publishStatus = 'pending';
+ } else if ( visibility === 'private' ) {
+ publishStatus = 'private';
+ } else if ( isBeingScheduled ) {
+ publishStatus = 'future';
}
- render() {
- const {
- forceIsDirty,
- hasPublishAction,
- isBeingScheduled,
- isOpen,
- isPostSavingLocked,
- isPublishable,
- isPublished,
- isSaveable,
- isSaving,
- isAutoSaving,
- isToggle,
- savePostStatus,
- onSubmit = noop,
- onToggle,
- visibility,
- hasNonPostEntityChanges,
- isSavingNonPostEntityChanges,
- postStatus,
- postStatusHasChanged,
- } = this.props;
-
- const isButtonDisabled =
- isPostSavingLocked ||
- ( ( isSaving ||
- ! isSaveable ||
- ( ! isPublishable && ! forceIsDirty ) ) &&
- ( ! hasNonPostEntityChanges || isSavingNonPostEntityChanges ) );
-
- const isToggleDisabled =
- isPostSavingLocked ||
- ( ( isPublished ||
- isSaving ||
- ! isSaveable ||
- ( ! isPublishable && ! forceIsDirty ) ) &&
- ( ! hasNonPostEntityChanges || isSavingNonPostEntityChanges ) );
-
- // If the new status has not changed explicitly, we derive it from
- // other factors, like having a publish action, etc.. We need to preserve
- // this because it affects when to show the pre and post publish panels.
- // If it has changed though explicitly, we need to respect that.
- let publishStatus = 'publish';
- if ( postStatusHasChanged ) {
- publishStatus = postStatus;
- } else if ( ! hasPublishAction ) {
- publishStatus = 'pending';
- } else if ( visibility === 'private' ) {
- publishStatus = 'private';
- } else if ( isBeingScheduled ) {
- publishStatus = 'future';
+ const onClickButton = () => {
+ if ( isButtonDisabled ) {
+ return;
}
-
- const onClickButton = () => {
- if ( isButtonDisabled ) {
- return;
- }
- onSubmit();
- savePostStatus( publishStatus );
- };
-
- // Callback to open the publish panel.
- const onClickToggle = () => {
- if ( isToggleDisabled ) {
- return;
- }
- onToggle();
- };
-
- const buttonProps = {
- 'aria-disabled': isButtonDisabled,
- className: 'editor-post-publish-button',
- isBusy: ! isAutoSaving && isSaving,
- variant: 'primary',
- onClick: this.createOnClick( onClickButton ),
- 'aria-haspopup': hasNonPostEntityChanges ? 'dialog' : undefined,
- };
-
- const toggleProps = {
- 'aria-disabled': isToggleDisabled,
- 'aria-expanded': isOpen,
- className: 'editor-post-publish-panel__toggle',
- isBusy: isSaving && isPublished,
- variant: 'primary',
- size: 'compact',
- onClick: this.createOnClick( onClickToggle ),
- 'aria-haspopup': hasNonPostEntityChanges ? 'dialog' : undefined,
- };
- const componentProps = isToggle ? toggleProps : buttonProps;
- return (
- <>
-
- >
- );
- }
+ onSubmit();
+ savePostStatus( publishStatus );
+ };
+
+ // Callback to open the publish panel.
+ const onClickToggle = () => {
+ if ( isToggleDisabled ) {
+ return;
+ }
+ onToggle();
+ };
+
+ const buttonProps = {
+ 'aria-disabled': isButtonDisabled,
+ className: 'editor-post-publish-button',
+ isBusy: ! isAutoSaving && isSaving,
+ variant: 'primary',
+ onClick: createOnClick( onClickButton ),
+ 'aria-haspopup': hasNonPostEntityChanges ? 'dialog' : undefined,
+ };
+
+ const toggleProps = {
+ 'aria-disabled': isToggleDisabled,
+ 'aria-expanded': isOpen,
+ className: 'editor-post-publish-panel__toggle',
+ isBusy: isSaving && isPublished,
+ variant: 'primary',
+ size: 'compact',
+ onClick: createOnClick( onClickToggle ),
+ 'aria-haspopup': hasNonPostEntityChanges ? 'dialog' : undefined,
+ };
+ const componentProps = isToggle ? toggleProps : buttonProps;
+ return (
+
+ );
}
/**
* Renders the publish button.
*/
-export default compose( [
- withSelect( ( select ) => {
- const {
- isSavingPost,
- isAutosavingPost,
- isEditedPostBeingScheduled,
- getEditedPostVisibility,
- isCurrentPostPublished,
- isEditedPostSaveable,
- isEditedPostPublishable,
- isPostSavingLocked,
- getCurrentPost,
- getCurrentPostType,
- getCurrentPostId,
- hasNonPostEntityChanges,
- isSavingNonPostEntityChanges,
- getEditedPostAttribute,
- getPostEdits,
- } = select( editorStore );
- return {
- isSaving: isSavingPost(),
- isAutoSaving: isAutosavingPost(),
- isBeingScheduled: isEditedPostBeingScheduled(),
- visibility: getEditedPostVisibility(),
- isSaveable: isEditedPostSaveable(),
- isPostSavingLocked: isPostSavingLocked(),
- isPublishable: isEditedPostPublishable(),
- isPublished: isCurrentPostPublished(),
- hasPublishAction:
- getCurrentPost()._links?.[ 'wp:action-publish' ] ?? false,
- postType: getCurrentPostType(),
- postId: getCurrentPostId(),
- postStatus: getEditedPostAttribute( 'status' ),
- postStatusHasChanged: getPostEdits()?.status,
- hasNonPostEntityChanges: hasNonPostEntityChanges(),
- isSavingNonPostEntityChanges: isSavingNonPostEntityChanges(),
- };
- } ),
- withDispatch( ( dispatch ) => {
- const { editPost, savePost } = dispatch( editorStore );
- return {
- savePostStatus: ( status ) => {
- editPost( { status }, { undoIgnore: true } );
- savePost();
- },
- };
- } ),
-] )( PostPublishButton );
+export default PostPublishButton;
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 e7a0465950cb85..2da07b8bace926 100644
--- a/packages/editor/src/components/post-publish-button/test/index.js
+++ b/packages/editor/src/components/post-publish-button/test/index.js
@@ -4,186 +4,209 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+/**
+ * WordPress dependencies
+ */
+import { dispatch, select } from '@wordpress/data';
+
/**
* Internal dependencies
*/
-import { PostPublishButton } from '../';
+import PostPublishButton from '../';
+import { store as editorStore } from '../../../store';
describe( 'PostPublishButton', () => {
+ beforeEach( () => {
+ jest.spyOn( select( editorStore ), 'getCurrentPost' ).mockReturnValue( {
+ _links: {},
+ } );
+ jest.spyOn( dispatch( editorStore ), 'editPost' ).mockReturnValue();
+ jest.spyOn( dispatch( editorStore ), 'savePost' ).mockReturnValue();
+ } );
+
+ afterEach( () => {
+ jest.restoreAllMocks();
+ } );
+
+ function mockSelector( name, value ) {
+ jest.spyOn( select( editorStore ), name ).mockReturnValue( value );
+ }
+
+ function mockHasPublishAction( hasPublishAction ) {
+ jest.spyOn( select( editorStore ), 'getCurrentPost' ).mockReturnValue( {
+ _links: hasPublishAction ? { 'wp:action-publish': true } : {},
+ } );
+ }
+
describe( 'aria-disabled', () => {
it( 'should be true if post is currently saving', () => {
- render( );
+ mockSelector( 'isEditedPostPublishable', true );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isSavingPost', true );
+
+ render( );
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveAttribute( 'aria-disabled', 'true' );
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
} );
it( 'should be true if post is not publishable and not forceIsDirty', () => {
- render(
-
- );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isEditedPostPublishable', false );
+
+ render( );
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveAttribute( 'aria-disabled', 'true' );
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
} );
it( 'should be true if post is not saveable', () => {
- render( );
+ mockSelector( 'isEditedPostPublishable', true );
+ mockSelector( 'isEditedPostSaveable', false );
+
+ render( );
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveAttribute( 'aria-disabled', 'true' );
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
} );
it( 'should be true if post saving is locked', () => {
- render(
-
- );
+ mockSelector( 'isEditedPostPublishable', true );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isPostSavingLocked', true );
+
+ render( );
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveAttribute( 'aria-disabled', 'true' );
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'true'
+ );
} );
it( 'should be false if post is saveable but not publishable and forceIsDirty is true', () => {
- render(
-
- );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isEditedPostPublishable', false );
+
+ render( );
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveAttribute( 'aria-disabled', 'false' );
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'false'
+ );
} );
it( 'should be false if post is publishave and saveable', () => {
- render( );
+ mockSelector( 'isEditedPostPublishable', true );
+ mockSelector( 'isEditedPostSaveable', true );
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveAttribute( 'aria-disabled', 'false' );
+ render( );
+
+ expect( screen.getByRole( 'button' ) ).toHaveAttribute(
+ 'aria-disabled',
+ 'false'
+ );
} );
} );
describe( 'publish status', () => {
it( 'should be pending for contributor', async () => {
const user = userEvent.setup();
- const savePostStatus = jest.fn();
- render(
-
- );
+ mockHasPublishAction( false );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isEditedPostPublishable', true );
- await user.click(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- );
+ render( );
+
+ await user.click( screen.getByRole( 'button' ) );
- expect( savePostStatus ).toHaveBeenCalledWith( 'pending' );
+ expect( dispatch( editorStore ).editPost ).toHaveBeenCalledWith(
+ { status: 'pending' },
+ { undoIgnore: true }
+ );
} );
it( 'should be future for scheduled post', async () => {
const user = userEvent.setup();
- const savePostStatus = jest.fn();
- render(
-
- );
+ mockHasPublishAction( true );
+ mockSelector( 'isEditedPostBeingScheduled', true );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isEditedPostPublishable', true );
- await user.click(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- );
+ render( );
+
+ await user.click( screen.getByRole( 'button' ) );
- expect( savePostStatus ).toHaveBeenCalledWith( 'future' );
+ expect( dispatch( editorStore ).editPost ).toHaveBeenCalledWith(
+ { status: 'future' },
+ { undoIgnore: true }
+ );
} );
it( 'should be private for private visibility', async () => {
const user = userEvent.setup();
- const savePostStatus = jest.fn();
- render(
-
- );
+ mockHasPublishAction( true );
+ mockSelector( 'getEditedPostVisibility', 'private' );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isEditedPostPublishable', true );
- await user.click(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- );
+ render( );
- expect( savePostStatus ).toHaveBeenCalledWith( 'private' );
+ await user.click( screen.getByRole( 'button' ) );
+
+ expect( dispatch( editorStore ).editPost ).toHaveBeenCalledWith(
+ { status: 'private' },
+ { undoIgnore: true }
+ );
} );
it( 'should be publish otherwise', async () => {
const user = userEvent.setup();
- const savePostStatus = jest.fn();
- render(
-
- );
+ mockHasPublishAction( true );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isEditedPostPublishable', true );
- await user.click(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- );
+ render( );
+
+ await user.click( screen.getByRole( 'button' ) );
- expect( savePostStatus ).toHaveBeenCalledWith( 'publish' );
+ expect( dispatch( editorStore ).editPost ).toHaveBeenCalledWith(
+ { status: 'publish' },
+ { undoIgnore: true }
+ );
} );
} );
describe( 'click', () => {
it( 'should save with status', async () => {
const user = userEvent.setup();
- const savePostStatus = jest.fn();
- render(
-
- );
+ mockHasPublishAction( true );
+ mockSelector( 'isEditedPostSaveable', true );
+ mockSelector( 'isEditedPostPublishable', true );
- await user.click(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- );
+ render( );
+
+ await user.click( screen.getByRole( 'button' ) );
- expect( savePostStatus ).toHaveBeenCalledWith( 'publish' );
+ expect( dispatch( editorStore ).editPost ).toHaveBeenCalledWith(
+ { status: 'publish' },
+ { undoIgnore: true }
+ );
+ expect( dispatch( editorStore ).savePost ).toHaveBeenCalled();
} );
} );
it( 'should have save modifier class', () => {
- render( );
+ mockSelector( 'isSavingPost', true );
+ mockSelector( 'isCurrentPostPublished', true );
+
+ render( );
- expect(
- screen.getByRole( 'button', { name: 'Submit for Review' } )
- ).toHaveClass( 'is-busy' );
+ expect( screen.getByRole( 'button' ) ).toHaveClass( 'is-busy' );
} );
} );