diff --git a/editor/header/saved-state/index.js b/editor/header/saved-state/index.js index b8ed533d4a591..4fe5aa6b651e5 100644 --- a/editor/header/saved-state/index.js +++ b/editor/header/saved-state/index.js @@ -19,11 +19,16 @@ import { isEditedPostNew, isEditedPostDirty, isSavingPost, + isEditedPostSaveable, getCurrentPost, getEditedPostAttribute, } from '../../selectors'; -function SavedState( { isNew, isDirty, isSaving, status, onStatusChange, onSave } ) { +function SavedState( { isNew, isDirty, isSaving, isSaveable, status, onStatusChange, onSave } ) { + if ( ! isSaveable ) { + return null; + } + const className = 'editor-saved-state'; if ( isSaving ) { @@ -60,6 +65,7 @@ export default connect( isNew: isEditedPostNew( state ), isDirty: isEditedPostDirty( state ), isSaving: isSavingPost( state ), + isSaveable: isEditedPostSaveable( state ), status: getEditedPostAttribute( state, 'status' ), } ), { diff --git a/editor/header/tools/publish-button.js b/editor/header/tools/publish-button.js index d05705f2e9279..dff17dc9b0474 100644 --- a/editor/header/tools/publish-button.js +++ b/editor/header/tools/publish-button.js @@ -19,6 +19,7 @@ import { isEditedPostPublished, isEditedPostBeingScheduled, getEditedPostVisibility, + isEditedPostSaveable, isEditedPostPublishable, } from '../../selectors'; @@ -30,8 +31,9 @@ function PublishButton( { isBeingScheduled, visibility, isPublishable, + isSaveable, } ) { - const buttonEnabled = ! isSaving && isPublishable; + const buttonEnabled = ! isSaving && isPublishable && isSaveable; let buttonText; if ( isPublished ) { buttonText = __( 'Update' ); @@ -76,6 +78,7 @@ export default connect( isPublished: isEditedPostPublished( state ), isBeingScheduled: isEditedPostBeingScheduled( state ), visibility: getEditedPostVisibility( state ), + isSaveable: isEditedPostSaveable( state ), isPublishable: isEditedPostPublishable( state ), } ), { diff --git a/editor/selectors.js b/editor/selectors.js index 29b73e36b676a..7fe2ece8c7e45 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -166,6 +166,21 @@ export function isEditedPostPublishable( state ) { return isEditedPostDirty( state ) || [ 'publish', 'private', 'future' ].indexOf( post.status ) === -1; } +/** + * Returns true if the post can be saved, or false otherwise. A post must + * contain a title, an excerpt, or non-empty content to be valid for save. + * + * @param {Object} state Global application state + * @return {Boolean} Whether the post can be saved + */ +export function isEditedPostSaveable( state ) { + return ( + getBlockCount( state ) > 0 || + !! getEditedPostTitle( state ) || + !! getEditedPostExcerpt( state ) + ); +} + /** * Return true if the post being edited is being scheduled. Preferring the * unsaved status values. @@ -251,6 +266,16 @@ export const getBlocks = createSelector( ] ); +/** + * Returns the number of blocks currently present in the post. + * + * @param {Object} state Global application state + * @return {Object} Number of blocks in the post + */ +export function getBlockCount( state ) { + return getBlockUids( state ).length; +} + /** * Returns the currently selected block, or null if there is no selected block. * diff --git a/editor/test/selectors.js b/editor/test/selectors.js index 183bac48953e6..42bc6f11cde66 100644 --- a/editor/test/selectors.js +++ b/editor/test/selectors.js @@ -22,10 +22,12 @@ import { getEditedPostVisibility, isEditedPostPublished, isEditedPostPublishable, + isEditedPostSaveable, isEditedPostBeingScheduled, getEditedPostPreviewLink, getBlock, getBlocks, + getBlockCount, getSelectedBlock, getMultiSelectedBlockUids, getMultiSelectedBlocksStartUid, @@ -455,6 +457,66 @@ describe( 'selectors', () => { } ); } ); + describe( 'isEditedPostSaveable', () => { + it( 'should return false if the post has no title, excerpt, content', () => { + const state = { + editor: { + blocksByUid: {}, + blockOrder: [], + edits: {}, + }, + currentPost: {}, + }; + + expect( isEditedPostSaveable( state ) ).to.be.false(); + } ); + + it( 'should return true if the post has a title', () => { + const state = { + editor: { + blocksByUid: {}, + blockOrder: [], + edits: {}, + }, + currentPost: { + title: { raw: 'sassel' }, + }, + }; + + expect( isEditedPostSaveable( state ) ).to.be.true(); + } ); + + it( 'should return true if the post has an excerpt', () => { + const state = { + editor: { + blocksByUid: {}, + blockOrder: [], + edits: {}, + }, + currentPost: { + excerpt: { raw: 'sassel' }, + }, + }; + + expect( isEditedPostSaveable( state ) ).to.be.true(); + } ); + + it( 'should return true if the post has content', () => { + const state = { + editor: { + blocksByUid: { + 123: { uid: 123, name: 'core/text' }, + }, + blockOrder: [ 123 ], + edits: {}, + }, + currentPost: {}, + }; + + expect( isEditedPostSaveable( state ) ).to.be.true(); + } ); + } ); + describe( 'isEditedPostBeingScheduled', () => { it( 'should return true for posts with a future date', () => { const state = { @@ -530,6 +592,22 @@ describe( 'selectors', () => { } ); } ); + describe( 'getBlockCount', () => { + it( 'should return the number of blocks in the post', () => { + const state = { + editor: { + blocksByUid: { + 23: { uid: 23, name: 'core/heading' }, + 123: { uid: 123, name: 'core/text' }, + }, + blockOrder: [ 123, 23 ], + }, + }; + + expect( getBlockCount( state ) ).to.equal( 2 ); + } ); + } ); + describe( 'getSelectedBlock', () => { it( 'should return null if no block is selected', () => { const state = {