From 511d6c102ab9d5a1d81ff9131994a173bfa46986 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 19 Jul 2017 12:10:48 -0400 Subject: [PATCH 1/3] Rename isEditedPostPublished to isCurrentPostPublished "edited post" in context of selectors should refer to current post + edited values. Current usage tests against saved post only. Needed to distinguish for "isEditedPostPublished" testing current post with edited values applied. --- editor/header/tools/publish-button.js | 4 ++-- editor/selectors.js | 4 ++-- editor/sidebar/post-status/index.js | 4 ++-- editor/test/selectors.js | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/editor/header/tools/publish-button.js b/editor/header/tools/publish-button.js index dff17dc9b04745..78a8fbcaf7fdc0 100644 --- a/editor/header/tools/publish-button.js +++ b/editor/header/tools/publish-button.js @@ -16,7 +16,7 @@ import { Button } from 'components'; import { editPost, savePost } from '../../actions'; import { isSavingPost, - isEditedPostPublished, + isCurrentPostPublished, isEditedPostBeingScheduled, getEditedPostVisibility, isEditedPostSaveable, @@ -75,7 +75,7 @@ function PublishButton( { export default connect( ( state ) => ( { isSaving: isSavingPost( state ), - isPublished: isEditedPostPublished( state ), + isPublished: isCurrentPostPublished( state ), isBeingScheduled: isEditedPostBeingScheduled( state ), visibility: getEditedPostVisibility( state ), isSaveable: isEditedPostSaveable( state ), diff --git a/editor/selectors.js b/editor/selectors.js index 9223d4a695f1e0..84cb2124bb7bb4 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -184,12 +184,12 @@ export function getEditedPostVisibility( state ) { } /** - * Return true if the post being edited has already been published. + * Return true if the current post has already been published. * * @param {Object} state Global application state * @return {Boolean} Whether the post has been published */ -export function isEditedPostPublished( state ) { +export function isCurrentPostPublished( state ) { const post = getCurrentPost( state ); return [ 'publish', 'private' ].indexOf( post.status ) !== -1 || diff --git a/editor/sidebar/post-status/index.js b/editor/sidebar/post-status/index.js index d91ba764a57199..7b4a9e18c9a45d 100644 --- a/editor/sidebar/post-status/index.js +++ b/editor/sidebar/post-status/index.js @@ -20,7 +20,7 @@ import PostSticky from '../post-sticky'; import { getEditedPostAttribute, getSuggestedPostFormat, - isEditedPostPublished, + isCurrentPostPublished, } from '../../selectors'; import { editPost } from '../../actions'; @@ -74,7 +74,7 @@ export default connect( ( state ) => ( { status: getEditedPostAttribute( state, 'status' ), suggestedFormat: getSuggestedPostFormat( state ), - isPublished: isEditedPostPublished( state ), + isPublished: isCurrentPostPublished( state ), } ), ( dispatch ) => { return { diff --git a/editor/test/selectors.js b/editor/test/selectors.js index e5471adc18ab37..f266367b35e09f 100644 --- a/editor/test/selectors.js +++ b/editor/test/selectors.js @@ -22,7 +22,7 @@ import { getDocumentTitle, getEditedPostExcerpt, getEditedPostVisibility, - isEditedPostPublished, + isCurrentPostPublished, isEditedPostPublishable, isEditedPostSaveable, isEditedPostBeingScheduled, @@ -490,7 +490,7 @@ describe( 'selectors', () => { } ); } ); - describe( 'isEditedPostPublished', () => { + describe( 'isCurrentPostPublished', () => { it( 'should return true for public posts', () => { const state = { currentPost: { @@ -498,7 +498,7 @@ describe( 'selectors', () => { }, }; - expect( isEditedPostPublished( state ) ).toBe( true ); + expect( isCurrentPostPublished( state ) ).toBe( true ); } ); it( 'should return true for private posts', () => { @@ -508,7 +508,7 @@ describe( 'selectors', () => { }, }; - expect( isEditedPostPublished( state ) ).toBe( true ); + expect( isCurrentPostPublished( state ) ).toBe( true ); } ); it( 'should return false for draft posts', () => { @@ -518,7 +518,7 @@ describe( 'selectors', () => { }, }; - expect( isEditedPostPublished( state ) ).toBe( false ); + expect( isCurrentPostPublished( state ) ).toBe( false ); } ); it( 'should return true for old scheduled posts', () => { @@ -529,7 +529,7 @@ describe( 'selectors', () => { }, }; - expect( isEditedPostPublished( state ) ).toBe( true ); + expect( isCurrentPostPublished( state ) ).toBe( true ); } ); } ); From 995f4cd880150ea2cd2b81a8af12a3485b53735b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 19 Jul 2017 12:20:35 -0400 Subject: [PATCH 2/3] Autosave drafts after debounce delay --- editor/actions.js | 23 +++++++++++++++++++++ editor/effects.js | 44 ++++++++++++++++++++++++++++++++++++++-- editor/test/effects.js | 46 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/editor/actions.js b/editor/actions.js index b607097f2c2bf0..e4d0fa0b241f79 100644 --- a/editor/actions.js +++ b/editor/actions.js @@ -110,6 +110,29 @@ export function mergeBlocks( blockA, blockB ) { }; } +/** + * Returns an action object used in signalling that the post should autosave. + * + * @return {Object} Action object + */ +export function autosave() { + return { + type: 'AUTOSAVE', + }; +} + +/** + * Returns an action object used in signalling that the post should be queued + * for autosave after a delay. + * + * @return {Object} Action object + */ +export function queueAutosave() { + return { + type: 'QUEUE_AUTOSAVE', + }; +} + /** * Returns an action object used in signalling that the blocks * corresponding to the specified UID set are to be removed. diff --git a/editor/effects.js b/editor/effects.js index 7e6df9e024ae79..d21cea89577674 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 } from 'lodash'; +import { get, uniqueId, debounce } from 'lodash'; /** * WordPress dependencies @@ -14,12 +14,23 @@ import { __ } from 'i18n'; * Internal dependencies */ import { getGutenbergURL, getWPAdminURL } from './utils/url'; -import { focusBlock, replaceBlocks, createSuccessNotice, createErrorNotice } from './actions'; +import { + focusBlock, + replaceBlocks, + createSuccessNotice, + createErrorNotice, + autosave, + queueAutosave, + savePost, +} from './actions'; import { getCurrentPost, getCurrentPostType, getBlocks, getPostEdits, + isCurrentPostPublished, + isEditedPostDirty, + isEditedPostSaveable, } from './selectors'; export default { @@ -207,4 +218,33 @@ export default { ] ) ); }, + AUTOSAVE( action, store ) { + const { getState } = store; + const state = getState(); + if ( ! isEditedPostSaveable( state ) || ! isEditedPostDirty( state ) ) { + return; + } + + if ( isCurrentPostPublished( state ) ) { + // TODO: Publish autosave. + // - Autosaves are created as revisions for published posts, but + // the necessary REST API behavior does not yet exist + // - May need to check for whether the status of the edited post + // has changed from the saved copy (i.e. published -> pending) + return; + } + + return savePost(); + }, + QUEUE_AUTOSAVE: debounce( ( action, store ) => { + store.dispatch( autosave() ); + }, 10000 ), + UPDATE_BLOCK_ATTRIBUTES: () => queueAutosave(), + INSERT_BLOCKS: () => queueAutosave(), + MOVE_BLOCKS_DOWN: () => queueAutosave(), + MOVE_BLOCKS_UP: () => queueAutosave(), + REPLACE_BLOCKS: () => queueAutosave(), + REMOVE_BLOCKS: () => queueAutosave(), + EDIT_POST: () => queueAutosave(), + MARK_DIRTY: () => queueAutosave(), }; diff --git a/editor/test/effects.js b/editor/test/effects.js index 62eecdeabf9814..570feee943da2b 100644 --- a/editor/test/effects.js +++ b/editor/test/effects.js @@ -13,10 +13,15 @@ import { getBlockTypes, unregisterBlockType, registerBlockType, createBlock } fr */ import { mergeBlocks, focusBlock, replaceBlocks } from '../actions'; import effects from '../effects'; +import * as selectors from '../selectors'; + +jest.mock( '../selectors' ); describe( 'effects', () => { const defaultBlockSettings = { save: noop }; + beforeEach( () => jest.resetAllMocks() ); + describe( '.MERGE_BLOCKS', () => { const handler = effects.MERGE_BLOCKS; @@ -145,4 +150,45 @@ describe( 'effects', () => { } ] ) ); } ); } ); + + describe( '.AUTOSAVE', () => { + const handler = effects.AUTOSAVE; + const store = { getState: () => {} }; + + it( 'should do nothing for unsaveable', () => { + selectors.isEditedPostSaveable.mockReturnValue( false ); + selectors.isEditedPostDirty.mockReturnValue( true ); + selectors.isCurrentPostPublished.mockReturnValue( false ); + + expect( handler( {}, store ) ).toBeUndefined(); + } ); + + it( 'should do nothing for clean', () => { + selectors.isEditedPostSaveable.mockReturnValue( true ); + selectors.isEditedPostDirty.mockReturnValue( false ); + selectors.isCurrentPostPublished.mockReturnValue( false ); + + expect( handler( {}, store ) ).toBeUndefined(); + } ); + + it( 'should return autosave action for saveable, dirty, published post', () => { + selectors.isEditedPostSaveable.mockReturnValue( true ); + selectors.isEditedPostDirty.mockReturnValue( true ); + selectors.isCurrentPostPublished.mockReturnValue( true ); + + expect( handler( {}, store ) ).toBeUndefined(); + // TODO: Publish autosave + // expect( handler( {}, store ) ).toEqual( { } ); + } ); + + it( 'should return update action for saveable, dirty draft', () => { + selectors.isEditedPostSaveable.mockReturnValue( true ); + selectors.isEditedPostDirty.mockReturnValue( true ); + selectors.isCurrentPostPublished.mockReturnValue( false ); + + expect( handler( {}, store ) ).toEqual( { + type: 'REQUEST_POST_UPDATE', + } ); + } ); + } ); } ); From 56d6dfaaf902dd0bb7932f4e5cb2ad9af5a68bea Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 26 Jul 2017 11:56:20 -0400 Subject: [PATCH 3/3] Set status from auto-draft to draft before save --- editor/effects.js | 11 +++++++++-- editor/test/effects.js | 36 +++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/editor/effects.js b/editor/effects.js index d21cea89577674..5b665b13a19f5a 100644 --- a/editor/effects.js +++ b/editor/effects.js @@ -22,6 +22,7 @@ import { autosave, queueAutosave, savePost, + editPost, } from './actions'; import { getCurrentPost, @@ -30,6 +31,7 @@ import { getPostEdits, isCurrentPostPublished, isEditedPostDirty, + isEditedPostNew, isEditedPostSaveable, } from './selectors'; @@ -219,7 +221,7 @@ export default { ) ); }, AUTOSAVE( action, store ) { - const { getState } = store; + const { getState, dispatch } = store; const state = getState(); if ( ! isEditedPostSaveable( state ) || ! isEditedPostDirty( state ) ) { return; @@ -234,7 +236,12 @@ export default { return; } - return savePost(); + // Change status from auto-draft to draft + if ( isEditedPostNew( state ) ) { + dispatch( editPost( { status: 'draft' } ) ); + } + + dispatch( savePost() ); }, QUEUE_AUTOSAVE: debounce( ( action, store ) => { store.dispatch( autosave() ); diff --git a/editor/test/effects.js b/editor/test/effects.js index 570feee943da2b..ec2cef6215aee4 100644 --- a/editor/test/effects.js +++ b/editor/test/effects.js @@ -11,7 +11,7 @@ import { getBlockTypes, unregisterBlockType, registerBlockType, createBlock } fr /** * Internal dependencies */ -import { mergeBlocks, focusBlock, replaceBlocks } from '../actions'; +import { mergeBlocks, focusBlock, replaceBlocks, editPost, savePost } from '../actions'; import effects from '../effects'; import * as selectors from '../selectors'; @@ -153,42 +153,60 @@ describe( 'effects', () => { describe( '.AUTOSAVE', () => { const handler = effects.AUTOSAVE; - const store = { getState: () => {} }; + const dispatch = jest.fn(); + const store = { getState: () => {}, dispatch }; it( 'should do nothing for unsaveable', () => { selectors.isEditedPostSaveable.mockReturnValue( false ); selectors.isEditedPostDirty.mockReturnValue( true ); selectors.isCurrentPostPublished.mockReturnValue( false ); + selectors.isEditedPostNew.mockReturnValue( true ); - expect( handler( {}, store ) ).toBeUndefined(); + expect( dispatch ).not.toHaveBeenCalled(); } ); it( 'should do nothing for clean', () => { selectors.isEditedPostSaveable.mockReturnValue( true ); selectors.isEditedPostDirty.mockReturnValue( false ); selectors.isCurrentPostPublished.mockReturnValue( false ); + selectors.isEditedPostNew.mockReturnValue( true ); - expect( handler( {}, store ) ).toBeUndefined(); + expect( dispatch ).not.toHaveBeenCalled(); } ); it( 'should return autosave action for saveable, dirty, published post', () => { selectors.isEditedPostSaveable.mockReturnValue( true ); selectors.isEditedPostDirty.mockReturnValue( true ); selectors.isCurrentPostPublished.mockReturnValue( true ); + selectors.isEditedPostNew.mockReturnValue( true ); - expect( handler( {}, store ) ).toBeUndefined(); // TODO: Publish autosave - // expect( handler( {}, store ) ).toEqual( { } ); + expect( dispatch ).not.toHaveBeenCalled(); + } ); + + it( 'should set auto-draft to draft before save', () => { + selectors.isEditedPostSaveable.mockReturnValue( true ); + selectors.isEditedPostDirty.mockReturnValue( true ); + selectors.isCurrentPostPublished.mockReturnValue( false ); + selectors.isEditedPostNew.mockReturnValue( true ); + + handler( {}, store ); + + expect( dispatch ).toHaveBeenCalledTimes( 2 ); + expect( dispatch ).toHaveBeenCalledWith( editPost( { status: 'draft' } ) ); + expect( dispatch ).toHaveBeenCalledWith( savePost() ); } ); it( 'should return update action for saveable, dirty draft', () => { selectors.isEditedPostSaveable.mockReturnValue( true ); selectors.isEditedPostDirty.mockReturnValue( true ); selectors.isCurrentPostPublished.mockReturnValue( false ); + selectors.isEditedPostNew.mockReturnValue( false ); - expect( handler( {}, store ) ).toEqual( { - type: 'REQUEST_POST_UPDATE', - } ); + handler( {}, store ); + + expect( dispatch ).toHaveBeenCalledTimes( 1 ); + expect( dispatch ).toHaveBeenCalledWith( savePost() ); } ); } ); } );