diff --git a/components/button/index.js b/components/button/index.js
index 079038a4297bb..98a076fc3d8d7 100644
--- a/components/button/index.js
+++ b/components/button/index.js
@@ -37,6 +37,7 @@ class Button extends Component {
isLarge,
isSmall,
isToggled,
+ isBusy,
className,
disabled,
...additionalProps
@@ -47,6 +48,7 @@ class Button extends Component {
'button-large': isLarge,
'button-small': isSmall,
'is-toggled': isToggled,
+ 'is-busy': isBusy,
} );
const tag = href !== undefined && ! disabled ? 'a' : 'button';
diff --git a/components/button/style.scss b/components/button/style.scss
index eb986809e3a6a..821e942699db0 100644
--- a/components/button/style.scss
+++ b/components/button/style.scss
@@ -21,4 +21,24 @@
&:focus {
@include button-style__focus-active;
}
+
+ &.is-busy,
+ &.is-busy[disabled] {
+ animation: components-button__busy-animation 2500ms infinite linear;
+ background-size: 100px 100% !important;
+ background-image: linear-gradient( -45deg, $light-gray-500 28%, $white 28%, $white 72%, $light-gray-500 72%) !important;
+ opacity: 1;
+ }
+
+ &.button-primary.is-busy,
+ &.button-primary.is-busy[disabled] {
+ color: $white !important;
+ background-size: 100px 100% !important;
+ background-image: linear-gradient( -45deg, $blue-medium-500 28%, $blue-dark-900 28%, $blue-dark-900 72%, $blue-medium-500 72%) !important;
+ border-color: $blue-dark-900 !important;
+ }
+}
+
+@keyframes components-button__busy-animation {
+ 0% { background-position: 200px 0; }
}
diff --git a/editor/components/post-publish-button/label.js b/editor/components/post-publish-button/label.js
index 17f3bd861f9c3..33a0c4f6fa964 100644
--- a/editor/components/post-publish-button/label.js
+++ b/editor/components/post-publish-button/label.js
@@ -17,15 +17,25 @@ import './style.scss';
import {
isCurrentPostPublished,
isEditedPostBeingScheduled,
+ isSavingPost,
+ isPublishingPost,
} from '../../selectors';
export function PublishButtonLabel( {
isPublished,
isBeingScheduled,
+ isSaving,
+ isPublishing,
user,
} ) {
const isContributor = user.data && ! user.data.capabilities.publish_posts;
+ if ( isPublishing ) {
+ return __( 'Publishing…' );
+ } else if ( isSaving ) {
+ return __( 'Updating…' );
+ }
+
if ( isContributor ) {
return __( 'Submit for Review' );
} else if ( isPublished ) {
@@ -41,6 +51,9 @@ const applyConnect = connect(
( state ) => ( {
isPublished: isCurrentPostPublished( state ),
isBeingScheduled: isEditedPostBeingScheduled( state ),
+ isSaving: isSavingPost( state ),
+ // Need a selector
+ isPublishing: isPublishingPost( state ),
} )
);
diff --git a/editor/components/post-publish-button/test/label.js b/editor/components/post-publish-button/test/label.js
index eadaf36dc6c6f..1b11633f71de0 100644
--- a/editor/components/post-publish-button/test/label.js
+++ b/editor/components/post-publish-button/test/label.js
@@ -26,6 +26,16 @@ describe( 'PublishButtonLabel', () => {
},
} );
+ it( 'should show publishing if publishing in progress', () => {
+ const label = PublishButtonLabel( { user, isPublishing: true } );
+ expect( label ).toBe( 'Publishing…' );
+ } );
+
+ it( 'should show updating if saving in progress', () => {
+ const label = PublishButtonLabel( { user, isSaving: true } );
+ expect( label ).toBe( 'Updating…' );
+ } );
+
it( 'should show publish if user unknown', () => {
const label = PublishButtonLabel( { user: {} } );
expect( label ).toBe( 'Publish' );
diff --git a/editor/components/post-publish-with-dropdown/index.js b/editor/components/post-publish-with-dropdown/index.js
index b5f5c1832f4c9..c112234a9371c 100644
--- a/editor/components/post-publish-with-dropdown/index.js
+++ b/editor/components/post-publish-with-dropdown/index.js
@@ -18,9 +18,10 @@ import {
isSavingPost,
isEditedPostSaveable,
isEditedPostPublishable,
+ isCurrentPostPublished,
} from '../../selectors';
-function PostPublishWithDropdown( { isSaving, isPublishable, isSaveable } ) {
+function PostPublishWithDropdown( { isSaving, isPublishable, isSaveable, isPublished } ) {
const isButtonEnabled = ! isSaving && isPublishable && isSaveable;
return (
@@ -34,6 +35,7 @@ function PostPublishWithDropdown( { isSaving, isPublishable, isSaveable } ) {
onClick={ onToggle }
aria-expanded={ isOpen }
disabled={ ! isButtonEnabled }
+ isBusy={ isSaving && isPublished }
>
@@ -49,5 +51,6 @@ export default connect(
isSaving: isSavingPost( state ),
isSaveable: isEditedPostSaveable( state ),
isPublishable: isEditedPostPublishable( state ),
+ isPublished: isCurrentPostPublished( state ),
} ),
)( PostPublishWithDropdown );
diff --git a/editor/components/post-publish-with-dropdown/style.scss b/editor/components/post-publish-with-dropdown/style.scss
index 9a1c1efc332f0..d9fb32e1b110e 100644
--- a/editor/components/post-publish-with-dropdown/style.scss
+++ b/editor/components/post-publish-with-dropdown/style.scss
@@ -9,4 +9,8 @@
.wp-core-ui .editor-post-publish-with-dropdown__button.button-primary {
display: inline-flex;
align-items: center;
+
+ &.is-busy .dashicon {
+ display: none;
+ }
}
diff --git a/editor/effects.js b/editor/effects.js
index 6a9aeddbda673..942fb5c5675bd 100644
--- a/editor/effects.js
+++ b/editor/effects.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
-import { get, uniqueId, map, filter, some, castArray } from 'lodash';
+import { get, map, filter, some, castArray } from 'lodash';
/**
* WordPress dependencies
@@ -54,6 +54,7 @@ import {
const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID';
const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID';
const SAVE_REUSABLE_BLOCK_NOTICE_ID = 'SAVE_REUSABLE_BLOCK_NOTICE_ID';
+export const POST_UPDATE_TRANSACTION_ID = 'post-update';
export default {
REQUEST_POST_UPDATE( action, store ) {
@@ -66,12 +67,11 @@ export default {
content: getEditedPostContent( state ),
id: post.id,
};
- const transactionId = uniqueId();
dispatch( {
type: 'UPDATE_POST',
edits: toSend,
- optimist: { type: BEGIN, id: transactionId },
+ optimist: { type: BEGIN, id: POST_UPDATE_TRANSACTION_ID },
} );
dispatch( removeNotice( SAVE_POST_NOTICE_ID ) );
const Model = wp.api.getPostTypeModel( getCurrentPostType( state ) );
@@ -84,7 +84,7 @@ export default {
type: 'REQUEST_POST_UPDATE_SUCCESS',
previousPost: post,
post: newPost,
- optimist: { type: COMMIT, id: transactionId },
+ optimist: { type: COMMIT, id: POST_UPDATE_TRANSACTION_ID },
} );
} ).fail( ( err ) => {
dispatch( {
@@ -95,7 +95,7 @@ export default {
} ),
post,
edits,
- optimist: { type: REVERT, id: transactionId },
+ optimist: { type: REVERT, id: POST_UPDATE_TRANSACTION_ID },
} );
} );
},
diff --git a/editor/selectors.js b/editor/selectors.js
index a3e231995f062..e631062dd9a30 100644
--- a/editor/selectors.js
+++ b/editor/selectors.js
@@ -11,6 +11,7 @@ import {
keys,
without,
compact,
+ find,
} from 'lodash';
import createSelector from 'rememo';
@@ -21,6 +22,11 @@ import { serialize, getBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { POST_UPDATE_TRANSACTION_ID } from './effects';
+
/***
* Module constants
*/
@@ -1072,3 +1078,49 @@ export function isSavingReusableBlock( state, ref ) {
export function getReusableBlocks( state ) {
return Object.values( state.reusableBlocks.data );
}
+
+/**
+ * Returns state object prior to a specified optimist transaction ID, or `null`
+ * if the transaction corresponding to the given ID cannot be found.
+ *
+ * @param {Object} state Current global application state
+ * @param {Object} transactionId Optimist transaction ID
+ * @return {Object} Global application state prior to transaction
+ */
+export function getStateBeforeOptimisticTransaction( state, transactionId ) {
+ const transaction = find( state.optimist, ( entry ) => (
+ entry.beforeState &&
+ get( entry.action, [ 'optimist', 'id' ] ) === transactionId
+ ) );
+
+ return transaction ? transaction.beforeState : null;
+}
+
+/**
+ * Returns true if the post is being published, or false otherwise
+ *
+ * @param {Object} state Global application state
+ * @return {Boolean} Whether post is being published
+ */
+export function isPublishingPost( state ) {
+ if ( ! isSavingPost( state ) ) {
+ return false;
+ }
+
+ // Saving is optimistic, so assume that current post would be marked as
+ // published if publishing
+ if ( ! isCurrentPostPublished( state ) ) {
+ return false;
+ }
+
+ // Use post update transaction ID to retrieve the state prior to the
+ // optimistic transaction
+ const stateBeforeRequest = getStateBeforeOptimisticTransaction(
+ state,
+ POST_UPDATE_TRANSACTION_ID
+ );
+
+ // Consider as publishing when current post prior to request was not
+ // considered published
+ return !! stateBeforeRequest && ! isCurrentPostPublished( stateBeforeRequest );
+}
diff --git a/editor/test/selectors.js b/editor/test/selectors.js
index 787a22cfb552e..96f2ffdda488f 100644
--- a/editor/test/selectors.js
+++ b/editor/test/selectors.js
@@ -77,7 +77,10 @@ import {
getReusableBlock,
isSavingReusableBlock,
getReusableBlocks,
+ getStateBeforeOptimisticTransaction,
+ isPublishingPost,
} from '../selectors';
+import { POST_UPDATE_TRANSACTION_ID } from '../effects';
describe( 'selectors', () => {
beforeAll( () => {
@@ -2187,4 +2190,175 @@ describe( 'selectors', () => {
expect( reusableBlocks ).toEqual( [] );
} );
} );
+
+ describe( 'getStateBeforeOptimisticTransaction', () => {
+ it( 'should return null if no transaction can be found', () => {
+ const beforeState = getStateBeforeOptimisticTransaction( {
+ optimist: [],
+ }, 'foo' );
+
+ expect( beforeState ).toBe( null );
+ } );
+
+ it( 'should return null if a transaction with ID can be found, but lacks before state', () => {
+ const beforeState = getStateBeforeOptimisticTransaction( {
+ optimist: [
+ {
+ action: {
+ optimist: {
+ id: 'foo',
+ },
+ },
+ },
+ ],
+ }, 'foo' );
+
+ expect( beforeState ).toBe( null );
+ } );
+
+ it( 'should return the before state matching the given transaction id', () => {
+ const expectedBeforeState = {};
+ const beforeState = getStateBeforeOptimisticTransaction( {
+ optimist: [
+ {
+ beforeState: expectedBeforeState,
+ action: {
+ optimist: {
+ id: 'foo',
+ },
+ },
+ },
+ ],
+ }, 'foo' );
+
+ expect( beforeState ).toBe( expectedBeforeState );
+ } );
+ } );
+
+ describe( 'isPublishingPost', () => {
+ it( 'should return false if the post is not being saved', () => {
+ const isPublishing = isPublishingPost( {
+ optimist: [],
+ saving: {
+ requesting: false,
+ },
+ editor: {
+ edits: {},
+ },
+ currentPost: {
+ status: 'publish',
+ },
+ } );
+
+ expect( isPublishing ).toBe( false );
+ } );
+
+ it( 'should return false if the current post is not considered published', () => {
+ const isPublishing = isPublishingPost( {
+ optimist: [],
+ saving: {
+ requesting: true,
+ },
+ editor: {
+ edits: {},
+ },
+ currentPost: {
+ status: 'draft',
+ },
+ } );
+
+ expect( isPublishing ).toBe( false );
+ } );
+
+ it( 'should return false if the optimistic transaction cannot be found', () => {
+ const isPublishing = isPublishingPost( {
+ optimist: [],
+ saving: {
+ requesting: true,
+ },
+ editor: {
+ edits: {},
+ },
+ currentPost: {
+ status: 'publish',
+ },
+ } );
+
+ expect( isPublishing ).toBe( false );
+ } );
+
+ it( 'should return false if the current post prior to request was already published', () => {
+ const isPublishing = isPublishingPost( {
+ optimist: [
+ {
+ beforeState: {
+ saving: {
+ requesting: false,
+ },
+ editor: {
+ edits: {},
+ },
+ currentPost: {
+ status: 'publish',
+ },
+ },
+ action: {
+ optimist: {
+ id: POST_UPDATE_TRANSACTION_ID,
+ },
+ },
+ },
+ ],
+ saving: {
+ requesting: true,
+ },
+ editor: {
+ edits: {},
+ },
+ currentPost: {
+ status: 'publish',
+ },
+ } );
+
+ expect( isPublishing ).toBe( false );
+ } );
+
+ it( 'should return true if the current post prior to request was not published', () => {
+ const isPublishing = isPublishingPost( {
+ optimist: [
+ {
+ beforeState: {
+ saving: {
+ requesting: false,
+ },
+ editor: {
+ edits: {
+ status: 'publish',
+ },
+ },
+ currentPost: {
+ status: 'draft',
+ },
+ },
+ action: {
+ optimist: {
+ id: POST_UPDATE_TRANSACTION_ID,
+ },
+ },
+ },
+ ],
+ saving: {
+ requesting: true,
+ },
+ editor: {
+ edits: {},
+ },
+ currentPost: {
+ status: 'publish',
+ },
+ } );
+
+ expect( isPublishing ).toBe( true );
+ } );
+ } );
} );