diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 34016e7153662..69059845b2ad9 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -193,11 +193,17 @@ saved state of the post. Post attribute value. -### getAutosaveAttribute +### getAutosaveAttribute (deprecated) Returns an attribute value of the current autosave revision for a post, or null if there is no autosave for the post. +*Deprecated* + +Deprecated since 5.6. Callers should use the `getAutosave( postType, postId, userId )` selector + from the '@wordpress/core-data' package and access properties on the returned + autosave object using getPostRawValue. + *Parameters* * state: Global application state. @@ -303,17 +309,23 @@ Returns true if the post can be autosaved, or false otherwise. *Parameters* * state: Global application state. + * autosave: A raw autosave object from the REST API. *Returns* Whether the post can be autosaved. -### getAutosave +### getAutosave (deprecated) Returns the current autosave, or null if one is not set (i.e. if the post has yet to be autosaved, or has been saved or published since the last autosave). +*Deprecated* + +Deprecated since 5.6. Callers should use the `getAutosave( postType, postId, userId )` + selector from the '@wordpress/core-data' package. + *Parameters* * state: Editor state. @@ -322,10 +334,15 @@ autosave). Current autosave, if exists. -### hasAutosave +### hasAutosave (deprecated) Returns the true if there is an existing autosave, otherwise false. +*Deprecated* + +Deprecated since 5.6. Callers should use the `getAutosave( postType, postId, userId )` selector + from the '@wordpress/core-data' package and check for a truthy value. + *Parameters* * state: Global application state. @@ -752,14 +769,19 @@ post has been received, either by initialization or save. * post: Post object. -### resetAutosave +### resetAutosave (deprecated) Returns an action object used in signalling that the latest autosave of the post has been received, by initialization or autosave. +*Deprecated* + +Deprecated since 5.6. Callers should use the `receiveAutosaves( postId, autosave )` + selector from the '@wordpress/core-data' package. + *Parameters* - * post: Autosave post object. + * newAutosave: Autosave post object. ### __experimentalRequestPostUpdateStart diff --git a/docs/designers-developers/developers/data/data-core.md b/docs/designers-developers/developers/data/data-core.md index 9b01fe325c295..57308660d4d63 100644 --- a/docs/designers-developers/developers/data/data-core.md +++ b/docs/designers-developers/developers/data/data-core.md @@ -24,6 +24,18 @@ Returns all available authors. Authors list. +### getCurrentUser + +Returns the current user. + +*Parameters* + + * state: Data state. + +*Returns* + +Current user object. + ### getUserQueryResults Returns all the users returned by a query ID. @@ -181,6 +193,52 @@ https://developer.wordpress.org/rest-api/reference/ Whether or not the user can perform the action, or `undefined` if the OPTIONS request is still being made. +### getAutosaves + +Returns the latest autosaves for the post. + +May return multiple autosaves since the backend stores one autosave per +author for each post. + +*Parameters* + + * state: State tree. + * postType: The type of the parent post. + * postId: The id of the parent post. + +*Returns* + +An array of autosaves for the post, or undefined if there is none. + +### getAutosave + +Returns the autosave for the post and author. + +*Parameters* + + * state: State tree. + * postType: The type of the parent post. + * postId: The id of the parent post. + * authorId: The id of the author. + +*Returns* + +The autosave for the post and author. + +### hasFetchedAutosaves + +Returns true if the REST request for autosaves has completed. + +*Parameters* + + * state: State tree. + * postType: The type of the parent post. + * postId: The id of the parent post. + +*Returns* + +True if the REST request was completed. False otherwise. + ## Actions ### receiveUserQuery @@ -192,6 +250,14 @@ Returns an action object used in signalling that authors have been received. * queryID: Query ID. * users: Users received. +### receiveCurrentUser + +Returns an action used in signalling that the current user has been received. + +*Parameters* + + * currentUser: Current user object. + ### addEntities Returns an action object used in adding new entities. @@ -256,4 +322,14 @@ permission to perform an action on a REST resource. *Parameters* * key: A key that represents the action and REST resource. - * isAllowed: Whether or not the user can perform the action. \ No newline at end of file + * isAllowed: Whether or not the user can perform the action. + +### receiveAutosaves + +Returns an action object used in signalling that the autosaves for a +post have been received. + +*Parameters* + + * postId: The id of the post that is parent to the autosave. + * autosaves: An array of autosaves or singular autosave object. \ No newline at end of file diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index 5b1952e7ac6fa..c1466915ec294 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.1.0 (Unreleased) + +### New features +- The `getAutosave`, `getAutosaves` and `getCurrentUser` selectors have been added. +- The `receiveAutosaves` and `receiveCurrentUser` actions have been added. + ## 2.0.16 (2019-01-03) ### Bug Fixes diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index c15bd647b9fcd..f9c7940d09db5 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -29,6 +29,20 @@ export function receiveUserQuery( queryID, users ) { }; } +/** + * Returns an action used in signalling that the current user has been received. + * + * @param {Object} currentUser Current user object. + * + * @return {Object} Action object. + */ +export function receiveCurrentUser( currentUser ) { + return { + type: 'RECEIVE_CURRENT_USER', + currentUser, + }; +} + /** * Returns an action object used in adding new entities. * @@ -159,3 +173,20 @@ export function receiveUserPermission( key, isAllowed ) { isAllowed, }; } + +/** + * Returns an action object used in signalling that the autosaves for a + * post have been received. + * + * @param {number} postId The id of the post that is parent to the autosave. + * @param {Array|Object} autosaves An array of autosaves or singular autosave object. + * + * @return {Object} Action object. + */ +export function receiveAutosaves( postId, autosaves ) { + return { + type: 'RECEIVE_AUTOSAVES', + postId, + autosaves: castArray( autosaves ), + }; +} diff --git a/packages/core-data/src/controls.js b/packages/core-data/src/controls.js index d7544e5b30c9b..6df3d092094f8 100644 --- a/packages/core-data/src/controls.js +++ b/packages/core-data/src/controls.js @@ -32,6 +32,23 @@ export function select( selectorName, ...args ) { }; } +/** + * Dispatches a control action for triggering a registry select that has a + * resolver. + * + * @param {string} selectorName + * @param {Array} args Arguments for the select. + * + * @return {Object} control descriptor. + */ +export function resolveSelect( selectorName, ...args ) { + return { + type: 'RESOLVE_SELECT', + selectorName, + args, + }; +} + const controls = { API_FETCH( { request } ) { return triggerApiFetch( request ); @@ -40,6 +57,30 @@ const controls = { SELECT: createRegistryControl( ( registry ) => ( { selectorName, args } ) => { return registry.select( 'core' )[ selectorName ]( ...args ); } ), + + RESOLVE_SELECT: createRegistryControl( + ( registry ) => ( { selectorName, args } ) => { + return new Promise( ( resolve ) => { + const hasFinished = () => registry.select( 'core/data' ) + .hasFinishedResolution( 'core', selectorName, args ); + const getResult = () => registry.select( 'core' )[ selectorName ] + .apply( null, args ); + + // trigger the selector (to trigger the resolver) + const result = getResult(); + if ( hasFinished() ) { + return resolve( result ); + } + + const unsubscribe = registry.subscribe( () => { + if ( hasFinished() ) { + unsubscribe(); + resolve( getResult() ); + } + } ); + } ); + } + ), }; export default controls; diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 7785667822d68..272ab20e2693e 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -64,6 +64,23 @@ export function users( state = { byId: {}, queries: {} }, action ) { return state; } +/** + * Reducer managing current user state. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function currentUser( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_CURRENT_USER': + return action.currentUser; + } + + return state; +} + /** * Reducer managing taxonomies. * @@ -238,12 +255,36 @@ export function userPermissions( state = {}, action ) { return state; } +/** + * Reducer returning autosaves keyed by their parent's post id. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function autosaves( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_AUTOSAVES': + const { postId, autosaves: autosavesData } = action; + + return { + ...state, + [ postId ]: autosavesData, + }; + } + + return state; +} + export default combineReducers( { terms, users, + currentUser, taxonomies, themeSupports, entities, embedPreviews, userPermissions, + autosaves, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 009fe1b7d5b42..8edfdbf895cde 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -14,13 +14,15 @@ import deprecated from '@wordpress/deprecated'; */ import { receiveUserQuery, + receiveCurrentUser, receiveEntityRecords, receiveThemeSupports, receiveEmbedPreview, receiveUserPermission, + receiveAutosaves, } from './actions'; import { getKindEntities } from './entities'; -import { apiFetch } from './controls'; +import { apiFetch, resolveSelect } from './controls'; /** * Requests authors from the REST API. @@ -30,6 +32,14 @@ export function* getAuthors() { yield receiveUserQuery( 'authors', users ); } +/** + * Requests the current user from the REST API. + */ +export function* getCurrentUser() { + const currentUser = yield apiFetch( { path: '/wp/v2/users/me' } ); + yield receiveCurrentUser( currentUser ); +} + /** * Requests an entity's record from the REST API. * @@ -169,3 +179,31 @@ export function* canUser( action, resource, id ) { const isAllowed = includes( allowHeader, method ); yield receiveUserPermission( key, isAllowed ); } + +/** + * Request autosave data from the REST API. + * + * @param {string} postType The type of the parent post. + * @param {number} postId The id of the parent post. + */ +export function* getAutosaves( postType, postId ) { + const { rest_base: restBase } = yield resolveSelect( 'getPostType', postType ); + const autosaves = yield apiFetch( { path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit` } ); + + if ( autosaves && autosaves.length ) { + yield receiveAutosaves( postId, autosaves ); + } +} + +/** + * Request autosave data from the REST API. + * + * This resolver exists to ensure the underlying autosaves are fetched via + * `getAutosaves` when a call to the `getAutosave` selector is made. + * + * @param {string} postType The type of the parent post. + * @param {number} postId The id of the parent post. + */ +export function* getAutosave( postType, postId ) { + yield resolveSelect( 'getAutosaves', postType, postId ); +} diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 54f9ba4adbe49..42e4a1f23d982 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -40,6 +40,17 @@ export function getAuthors( state ) { return getUserQueryResults( state, 'authors' ); } +/** + * Returns the current user. + * + * @param {Object} state Data state. + * + * @return {Object} Current user object. + */ +export function getCurrentUser( state ) { + return state.currentUser; +} + /** * Returns all the users returned by a query ID. * @@ -202,3 +213,51 @@ export function canUser( state, action, resource, id ) { const key = compact( [ action, resource, id ] ).join( '/' ); return get( state, [ 'userPermissions', key ] ); } + +/** + * Returns the latest autosaves for the post. + * + * May return multiple autosaves since the backend stores one autosave per + * author for each post. + * + * @param {Object} state State tree. + * @param {string} postType The type of the parent post. + * @param {number} postId The id of the parent post. + * + * @return {?Array} An array of autosaves for the post, or undefined if there is none. + */ +export function getAutosaves( state, postType, postId ) { + return state.autosaves[ postId ]; +} + +/** + * Returns the autosave for the post and author. + * + * @param {Object} state State tree. + * @param {string} postType The type of the parent post. + * @param {number} postId The id of the parent post. + * @param {number} authorId The id of the author. + * + * @return {?Object} The autosave for the post and author. + */ +export function getAutosave( state, postType, postId, authorId ) { + if ( authorId === undefined ) { + return; + } + + const autosaves = state.autosaves[ postId ]; + return find( autosaves, { author: authorId } ); +} + +/** + * Returns true if the REST request for autosaves has completed. + * + * @param {Object} state State tree. + * @param {string} postType The type of the parent post. + * @param {number} postId The id of the parent post. + * + * @return {boolean} True if the REST request was completed. False otherwise. + */ +export const hasFetchedAutosaves = createRegistrySelector( ( select ) => ( state, postType, postId ) => { + return select( REDUCER_KEY ).hasFinishedResolution( 'getAutosaves', [ postType, postId ] ); +} ); diff --git a/packages/core-data/src/test/actions.js b/packages/core-data/src/test/actions.js index 85c94eeaa5224..7ef1abd0dc43c 100644 --- a/packages/core-data/src/test/actions.js +++ b/packages/core-data/src/test/actions.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { saveEntityRecord, receiveEntityRecords, receiveUserPermission } from '../actions'; +import { saveEntityRecord, receiveEntityRecords, receiveUserPermission, receiveAutosaves, receiveCurrentUser } from '../actions'; describe( 'saveEntityRecord', () => { it( 'triggers a POST request for a new record', async () => { @@ -68,3 +68,46 @@ describe( 'receiveUserPermission', () => { } ); } ); } ); + +describe( 'receiveAutosaves', () => { + it( 'builds an action object', () => { + const postId = 1; + const autosaves = [ + { + content: 'test 1', + }, + { + content: 'test 2', + }, + ]; + + expect( receiveAutosaves( postId, autosaves ) ).toEqual( { + type: 'RECEIVE_AUTOSAVES', + postId, + autosaves, + } ); + } ); + + it( 'converts singular autosaves into an array', () => { + const postId = 1; + const autosave = { + content: 'test 1', + }; + + expect( receiveAutosaves( postId, autosave ) ).toEqual( { + type: 'RECEIVE_AUTOSAVES', + postId, + autosaves: [ autosave ], + } ); + } ); +} ); + +describe( 'receiveCurrentUser', () => { + it( 'builds an action object', () => { + const currentUser = { id: 1 }; + expect( receiveCurrentUser( currentUser ) ).toEqual( { + type: 'RECEIVE_CURRENT_USER', + currentUser, + } ); + } ); +} ); diff --git a/packages/core-data/src/test/reducer.js b/packages/core-data/src/test/reducer.js index 51bc4611ad7d9..ec8349d0a65d3 100644 --- a/packages/core-data/src/test/reducer.js +++ b/packages/core-data/src/test/reducer.js @@ -7,7 +7,7 @@ import { filter } from 'lodash'; /** * Internal dependencies */ -import { terms, entities, embedPreviews, userPermissions } from '../reducer'; +import { terms, entities, embedPreviews, userPermissions, autosaves, currentUser } from '../reducer'; describe( 'terms()', () => { it( 'returns an empty object by default', () => { @@ -140,3 +140,117 @@ describe( 'userPermissions()', () => { } ); } ); } ); + +describe( 'autosaves', () => { + it( 'returns an empty object by default', () => { + const state = autosaves( undefined, {} ); + + expect( state ).toEqual( {} ); + } ); + + it( 'returns the current state with the new autosaves merged in, keyed by the parent post id', () => { + const existingAutosaves = [ { + title: { + raw: 'Some', + }, + content: { + raw: 'other', + }, + excerpt: { + raw: 'autosave', + }, + status: 'publish', + } ]; + + const newAutosaves = [ { + title: { + raw: 'The Title', + }, + content: { + raw: 'The Content', + }, + excerpt: { + raw: 'The Excerpt', + }, + status: 'draft', + } ]; + + const state = autosaves( { 1: existingAutosaves }, { + type: 'RECEIVE_AUTOSAVES', + postId: 2, + autosaves: newAutosaves, + } ); + + expect( state ).toEqual( { + 1: existingAutosaves, + 2: newAutosaves, + } ); + } ); + + it( 'overwrites any existing state if new autosaves are received with the same post id', () => { + const existingAutosaves = [ { + title: { + raw: 'Some', + }, + content: { + raw: 'other', + }, + excerpt: { + raw: 'autosave', + }, + status: 'publish', + } ]; + + const newAutosaves = [ { + title: { + raw: 'The Title', + }, + content: { + raw: 'The Content', + }, + excerpt: { + raw: 'The Excerpt', + }, + status: 'draft', + } ]; + + const state = autosaves( { 1: existingAutosaves }, { + type: 'RECEIVE_AUTOSAVES', + postId: 1, + autosaves: newAutosaves, + } ); + + expect( state ).toEqual( { + 1: newAutosaves, + } ); + } ); +} ); + +describe( 'currentUser', () => { + it( 'returns an empty object by default', () => { + const state = currentUser( undefined, {} ); + expect( state ).toEqual( {} ); + } ); + + it( 'returns the current user', () => { + const currentUserData = { id: 1 }; + + const state = currentUser( {}, { + type: 'RECEIVE_CURRENT_USER', + currentUser: currentUserData, + } ); + + expect( state ).toEqual( currentUserData ); + } ); + + it( 'overwrites any existing current user state', () => { + const currentUserData = { id: 2 }; + + const state = currentUser( { id: 1 }, { + type: 'RECEIVE_CURRENT_USER', + currentUser: currentUserData, + } ); + + expect( state ).toEqual( currentUserData ); + } ); +} ); diff --git a/packages/core-data/src/test/resolvers.js b/packages/core-data/src/test/resolvers.js index 325e4ce9c322a..c91babdfb5956 100644 --- a/packages/core-data/src/test/resolvers.js +++ b/packages/core-data/src/test/resolvers.js @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import { getEntityRecord, getEntityRecords, getEmbedPreview, canUser } from '../resolvers'; -import { receiveEntityRecords, receiveEmbedPreview, receiveUserPermission } from '../actions'; +import { getEntityRecord, getEntityRecords, getEmbedPreview, canUser, getAutosaves, getCurrentUser } from '../resolvers'; +import { receiveEntityRecords, receiveEmbedPreview, receiveUserPermission, receiveAutosaves, receiveCurrentUser } from '../actions'; import { apiFetch } from '../controls'; describe( 'getEntityRecord', () => { @@ -159,3 +159,68 @@ describe( 'canUser', () => { expect( received.value ).toBeUndefined(); } ); } ); + +describe( 'getAutosaves', () => { + const SUCCESSFUL_RESPONSE = [ { + title: 'test title', + excerpt: 'test excerpt', + content: 'test content', + } ]; + + it( 'yields with fetched autosaves', async () => { + const postType = 'post'; + const postId = 1; + const restBase = 'posts'; + const postEntity = { rest_base: restBase }; + const fulfillment = getAutosaves( postType, postId ); + + // Trigger generator + fulfillment.next(); + + // Trigger generator with the postEntity and assert that correct path is formed + // in the apiFetch request. + const { value: apiFetchAction } = fulfillment.next( postEntity ); + expect( apiFetchAction.request ).toEqual( { path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit` } ); + + // Provide apiFetch response and trigger Action + const received = ( await fulfillment.next( SUCCESSFUL_RESPONSE ) ).value; + expect( received ).toEqual( receiveAutosaves( 1, SUCCESSFUL_RESPONSE ) ); + } ); + + it( ' yields undefined if no autosaves exist for the post', async () => { + const postType = 'post'; + const postId = 1; + const restBase = 'posts'; + const postEntity = { rest_base: restBase }; + const fulfillment = getAutosaves( postType, postId ); + + // Trigger generator + fulfillment.next(); + + // Trigger generator with the postEntity and assert that correct path is formed + // in the apiFetch request. + const { value: apiFetchAction } = fulfillment.next( postEntity ); + expect( apiFetchAction.request ).toEqual( { path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit` } ); + + // Provide apiFetch response and trigger Action + const received = ( await fulfillment.next( [] ) ).value; + expect( received ).toBeUndefined(); + } ); +} ); + +describe( 'getCurrentUser', () => { + const SUCCESSFUL_RESPONSE = { + id: 1, + }; + + it( 'yields with fetched user', async () => { + const fulfillment = getCurrentUser(); + + // Trigger generator + fulfillment.next(); + + // Provide apiFetch response and trigger Action + const received = ( await fulfillment.next( SUCCESSFUL_RESPONSE ) ).value; + expect( received ).toEqual( receiveCurrentUser( SUCCESSFUL_RESPONSE ) ); + } ); +} ); diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js index f2a2885e77662..1335b744e9052 100644 --- a/packages/core-data/src/test/selectors.js +++ b/packages/core-data/src/test/selectors.js @@ -12,6 +12,9 @@ import { getEmbedPreview, isPreviewEmbedFallback, canUser, + getAutosave, + getAutosaves, + getCurrentUser, } from '../selectors'; describe( 'getEntityRecord', () => { @@ -145,3 +148,122 @@ describe( 'canUser', () => { expect( canUser( state, 'create', 'media', 123 ) ).toBe( false ); } ); } ); + +describe( 'getAutosave', () => { + const testAutosave = { author: 1, title: { raw: '' }, excerpt: { raw: '' }, content: { raw: '' } }; + + it( 'returns undefined if no autosaves exist for the post id in state', () => { + const postType = 'post'; + const postId = 2; + const author = 2; + const state = { + autosaves: { + 1: [ testAutosave ], + 2: [ testAutosave ], + }, + }; + + const result = getAutosave( state, postType, postId, author ); + + expect( result ).toBeUndefined(); + } ); + + it( 'returns undefined if an authorId is not provided (or undefined)', () => { + const postType = 'post'; + const postId = 1; + const state = { + autosaves: { + 1: [ testAutosave ], + }, + }; + + const result = getAutosave( state, postType, postId ); + + expect( result ).toBeUndefined(); + } ); + + it( 'returns undefined if there are autosaves for the post id, but none matching the autosave for the author', () => { + const postType = 'post'; + const postId = 1; + const author = 2; + const state = { + autosaves: { + [ postId ]: [ testAutosave ], + 2: [ testAutosave ], + }, + }; + + const result = getAutosave( state, postType, postId, author ); + + expect( result ).toBeUndefined(); + } ); + + it( 'returns the autosave for the post id and author when it exists in state', () => { + const postType = 'post'; + const postId = 1; + const author = 2; + const expectedAutosave = { author, title: { raw: '' }, excerpt: { raw: '' }, content: { raw: '' } }; + const state = { + autosaves: { + [ postId ]: [ testAutosave, expectedAutosave ], + 2: [ testAutosave ], + }, + }; + + const result = getAutosave( state, postType, postId, author ); + + expect( result ).toEqual( expectedAutosave ); + } ); +} ); + +describe( 'getAutosaves', () => { + it( 'returns undefined for the provided post id if no autosaves exist for it in state', () => { + const postType = 'post'; + const postId = 2; + const autosaves = [ { title: { raw: '' }, excerpt: { raw: '' }, content: { raw: '' } } ]; + const state = { + autosaves: { + 1: autosaves, + }, + }; + + const result = getAutosaves( state, postType, postId ); + + expect( result ).toBeUndefined(); + } ); + + it( 'returns the autosaves for the provided post id when they exist in state', () => { + const postType = 'post'; + const postId = 1; + const autosaves = [ { title: { raw: '' }, excerpt: { raw: '' }, content: { raw: '' } } ]; + const state = { + autosaves: { + 1: autosaves, + }, + }; + + const result = getAutosaves( state, postType, postId ); + + expect( result ).toEqual( autosaves ); + } ); +} ); + +describe( 'getCurrentUser', () => { + it( 'returns undefined if no user exists in state', () => { + const state = {}; + + expect( getCurrentUser( state ) ).toBeUndefined(); + } ); + + it( 'returns the user object when a user exists in state', () => { + const currentUser = { + id: 1, + }; + + const state = { + currentUser, + }; + + expect( getCurrentUser( state ) ).toEqual( currentUser ); + } ); +} ); diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index cefab057402b6..da86f36a1209a 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,4 +1,8 @@ -## 9.1.1 (Unreleased) +## 9.2.0 (Unreleased) + +### Deprecations +- The `getAutosave`, `getAutosaveAttribute`, and `hasAutosave` selectors are deprecated. Please use the `getAutosave` selector in the `@wordpress/core-data` package. +- The `resetAutosave` action is deprecated. An equivalent action `receiveAutosaves` has been added to the `@wordpress/core-data` package. ### Internal diff --git a/packages/editor/src/components/post-preview-button/index.js b/packages/editor/src/components/post-preview-button/index.js index 265daa1a7db3f..b09f50b431269 100644 --- a/packages/editor/src/components/post-preview-button/index.js +++ b/packages/editor/src/components/post-preview-button/index.js @@ -215,6 +215,7 @@ export default compose( [ const previewLink = getEditedPostPreviewLink(); const postType = getPostType( getEditedPostAttribute( 'type' ) ); + return { postId: getCurrentPostId(), currentPostLink: getCurrentPostAttribute( 'link' ), diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 32f3bed210e10..2f80e0d488309 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -1,9 +1,14 @@ /** * External dependencies */ -import { castArray, pick, has } from 'lodash'; +import { castArray, pick, mapValues, has } from 'lodash'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + /** * Internal dependencies */ @@ -13,11 +18,15 @@ import { resolveSelect, apiFetch, } from './controls'; +import { + getPostRawValue, +} from './reducer'; import { STORE_KEY, POST_UPDATE_TRANSACTION_ID, SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID, + AUTOSAVE_PROPERTIES, } from './constants'; import { getNotificationArgumentsForSaveSuccess, @@ -90,15 +99,23 @@ export function resetPost( post ) { * Returns an action object used in signalling that the latest autosave of the * post has been received, by initialization or autosave. * - * @param {Object} post Autosave post object. + * @deprecated since 5.6. Callers should use the `receiveAutosaves( postId, autosave )` + * selector from the '@wordpress/core-data' package. + * + * @param {Object} newAutosave Autosave post object. * * @return {Object} Action object. */ -export function resetAutosave( post ) { - return { - type: 'RESET_AUTOSAVE', - post, - }; +export function* resetAutosave( newAutosave ) { + deprecated( 'resetAutosave action (`core/editor` store)', { + alternative: 'receiveAutosaves action (`core` store)', + plugin: 'Gutenberg', + } ); + + const postId = yield select( STORE_KEY, 'getCurrentPostId' ); + yield dispatch( 'core', 'receiveAutosaves', postId, newAutosave ); + + return { type: '__INERT__' }; } /** @@ -262,7 +279,7 @@ export function* savePost( options = {} ) { const isAutosave = !! options.isAutosave; if ( isAutosave ) { - edits = pick( edits, [ 'title', 'content', 'excerpt' ] ); + edits = pick( edits, AUTOSAVE_PROPERTIES ); } const isEditedPostNew = yield select( @@ -330,15 +347,16 @@ export function* savePost( options = {} ) { let path = `/wp/v2/${ postType.rest_base }/${ post.id }`; let method = 'PUT'; if ( isAutosave ) { - const autoSavePost = yield select( - STORE_KEY, - 'getAutosave', - ); + const currentUser = yield resolveSelect( 'core', 'getCurrentUser' ); + const currentUserId = currentUser ? currentUser.id : undefined; + const autosavePost = yield resolveSelect( 'core', 'getAutosave', post.type, post.id, currentUserId ); + const mappedAutosavePost = mapValues( pick( autosavePost, AUTOSAVE_PROPERTIES ), getPostRawValue ); + // Ensure autosaves contain all expected fields, using autosave or // post values as fallback if not otherwise included in edits. toSend = { - ...pick( post, [ 'title', 'content', 'excerpt' ] ), - ...autoSavePost, + ...pick( post, AUTOSAVE_PROPERTIES ), + ...mappedAutosavePost, ...toSend, }; path += '/autosaves'; @@ -362,9 +380,12 @@ export function* savePost( options = {} ) { method, data: toSend, } ); - const resetAction = isAutosave ? 'resetAutosave' : 'resetPost'; - yield dispatch( STORE_KEY, resetAction, newPost ); + if ( isAutosave ) { + yield dispatch( 'core', 'receiveAutosaves', post.id, newPost ); + } else { + yield dispatch( STORE_KEY, 'resetPost', newPost ); + } yield dispatch( STORE_KEY, diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js index 8f8f1bd0afcef..8b9645c8d8309 100644 --- a/packages/editor/src/store/constants.js +++ b/packages/editor/src/store/constants.js @@ -19,3 +19,4 @@ export const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; export const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; export const ONE_MINUTE_IN_MS = 60 * 1000; +export const AUTOSAVE_PROPERTIES = [ 'title', 'excerpt', 'content' ]; diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index b6a4759503ce7..fffa264757882 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -749,38 +749,10 @@ export const blockListSettings = ( state = {}, action ) => { return state; }; -/** - * Reducer returning the most recent autosave. - * - * @param {Object} state The autosave object. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function autosave( state = null, action ) { - switch ( action.type ) { - case 'RESET_AUTOSAVE': - const { post } = action; - const [ title, excerpt, content ] = [ - 'title', - 'excerpt', - 'content', - ].map( ( field ) => getPostRawValue( post[ field ] ) ); - - return { - title, - excerpt, - content, - }; - } - - return state; -} - /** * Reducer returning the post preview link. * - * @param {string?} state The preview link + * @param {string?} state The preview link. * @param {Object} action Dispatched action. * * @return {string?} Updated state. @@ -855,7 +827,6 @@ export default optimist( combineReducers( { postLock, reusableBlocks, template, - autosave, previewLink, postSavingLock, isReady, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index b840175a5cdd9..7ebb18b63832d 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -6,6 +6,9 @@ import { get, has, map, + pick, + mapValues, + includes, } from 'lodash'; import createSelector from 'rememo'; @@ -22,6 +25,7 @@ import { isInTheFuture, getDate } from '@wordpress/date'; import { removep } from '@wordpress/autop'; import { addQueryArgs } from '@wordpress/url'; import { createRegistrySelector } from '@wordpress/data'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -32,7 +36,9 @@ import { POST_UPDATE_TRANSACTION_ID, PERMALINK_POSTNAME_REGEX, ONE_MINUTE_IN_MS, + AUTOSAVE_PROPERTIES, } from './constants'; +import { getPostRawValue } from './reducer'; /** * Shared reference to an empty object for cases where it is important to avoid @@ -320,21 +326,34 @@ export function getEditedPostAttribute( state, attributeName ) { * Returns an attribute value of the current autosave revision for a post, or * null if there is no autosave for the post. * + * @deprecated since 5.6. Callers should use the `getAutosave( postType, postId, userId )` selector + * from the '@wordpress/core-data' package and access properties on the returned + * autosave object using getPostRawValue. + * * @param {Object} state Global application state. * @param {string} attributeName Autosave attribute name. * * @return {*} Autosave attribute value. */ -export function getAutosaveAttribute( state, attributeName ) { - if ( ! hasAutosave( state ) ) { - return null; +export const getAutosaveAttribute = createRegistrySelector( ( select ) => ( state, attributeName ) => { + deprecated( '`wp.data.select( \'core/editor\' ).getAutosaveAttribute( attributeName )`', { + alternative: '`wp.data.select( \'core\' ).getAutosave( postType, postId, userId )`', + plugin: 'Gutenberg', + } ); + + if ( ! includes( AUTOSAVE_PROPERTIES, attributeName ) ) { + return; } - const autosave = getAutosave( state ); - if ( autosave.hasOwnProperty( attributeName ) ) { - return autosave[ attributeName ]; + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + const currentUserId = get( select( 'core' ).getCurrentUser(), [ 'id' ] ); + const autosave = select( 'core' ).getAutosave( postType, postId, currentUserId ); + + if ( autosave ) { + return getPostRawValue( autosave[ attributeName ] ); } -} +} ); /** * Returns the current visibility of the post being edited, preferring the @@ -495,18 +514,36 @@ export function isEditedPostEmpty( state ) { /** * Returns true if the post can be autosaved, or false otherwise. * - * @param {Object} state Global application state. + * @param {Object} state Global application state. + * @param {Object} autosave A raw autosave object from the REST API. * * @return {boolean} Whether the post can be autosaved. */ -export function isEditedPostAutosaveable( state ) { +export const isEditedPostAutosaveable = createRegistrySelector( ( select ) => function( state ) { // A post must contain a title, an excerpt, or non-empty content to be valid for autosaving. if ( ! isEditedPostSaveable( state ) ) { return false; } + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + const hasFetchedAutosave = select( 'core' ).hasFetchedAutosaves( postType, postId ); + const currentUserId = get( select( 'core' ).getCurrentUser(), [ 'id' ] ); + + // Disable reason - this line causes the side-effect of fetching the autosave + // via a resolver, moving below the return would result in the autosave never + // being fetched. + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const autosave = select( 'core' ).getAutosave( postType, postId, currentUserId ); + + // If any existing autosaves have not yet been fetched, this function is + // unable to determine if the post is autosaveable, so return false. + if ( ! hasFetchedAutosave ) { + return false; + } + // If we don't already have an autosave, the post is autosaveable. - if ( ! hasAutosave( state ) ) { + if ( ! autosave ) { return true; } @@ -518,36 +555,58 @@ export function isEditedPostAutosaveable( state ) { return true; } - // If the title, excerpt or content has changed, the post is autosaveable. - const autosave = getAutosave( state ); + // If the title or excerpt has changed, the post is autosaveable. return [ 'title', 'excerpt' ].some( ( field ) => ( - autosave[ field ] !== getEditedPostAttribute( state, field ) + getPostRawValue( autosave[ field ] ) !== getEditedPostAttribute( state, field ) ) ); -} +} ); /** * Returns the current autosave, or null if one is not set (i.e. if the post * has yet to be autosaved, or has been saved or published since the last * autosave). * + * @deprecated since 5.6. Callers should use the `getAutosave( postType, postId, userId )` + * selector from the '@wordpress/core-data' package. + * * @param {Object} state Editor state. * * @return {?Object} Current autosave, if exists. */ -export function getAutosave( state ) { - return state.autosave; -} +export const getAutosave = createRegistrySelector( ( select ) => ( state ) => { + deprecated( '`wp.data.select( \'core/editor\' ).getAutosave()`', { + alternative: '`wp.data.select( \'core\' ).getAutosave( postType, postId, userId )`', + plugin: 'Gutenberg', + } ); + + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + const currentUserId = get( select( 'core' ).getCurrentUser(), [ 'id' ] ); + const autosave = select( 'core' ).getAutosave( postType, postId, currentUserId ); + return mapValues( pick( autosave, AUTOSAVE_PROPERTIES ), getPostRawValue ); +} ); /** * Returns the true if there is an existing autosave, otherwise false. * + * @deprecated since 5.6. Callers should use the `getAutosave( postType, postId, userId )` selector + * from the '@wordpress/core-data' package and check for a truthy value. + * * @param {Object} state Global application state. * * @return {boolean} Whether there is an existing autosave. */ -export function hasAutosave( state ) { - return !! getAutosave( state ); -} +export const hasAutosave = createRegistrySelector( ( select ) => ( state ) => { + deprecated( '`wp.data.select( \'core/editor\' ).hasAutosave()`', { + alternative: '`!! wp.data.select( \'core\' ).getAutosave( postType, postId, userId )`', + plugin: 'Gutenberg', + } ); + + const postType = getCurrentPostType( state ); + const postId = getCurrentPostId( state ); + const currentUserId = get( select( 'core' ).getCurrentUser(), [ 'id' ] ); + return !! select( 'core' ).getAutosave( postType, postId, currentUserId ); +} ); /** * Return true if the post being edited is being scheduled. Preferring the @@ -574,7 +633,7 @@ export function isEditedPostBeingScheduled( state ) { * infer that a post is set to publish "Immediately" we check whether the date * and modified date are the same. * - * @param {Object} state Editor state. + * @param {Object} state Editor state. * * @return {boolean} Whether the edited post has a floating date value. */ diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 3f0e453b384eb..af7e7f60a2210 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -55,7 +55,9 @@ const postType = { item_published: 'Post published', }, }; +const postId = 44; const postTypeSlug = 'post'; +const userId = 1; describe( 'Post generator actions', () => { describe( 'savePost()', () => { @@ -63,6 +65,7 @@ describe( 'Post generator actions', () => { edits, currentPost, currentPostStatus, + currentUser, editPostToSendOptimistic, autoSavePost, autoSavePostToSend, @@ -85,12 +88,14 @@ describe( 'Post generator actions', () => { return postObject; }; currentPost = () => ( { - id: 44, + id: postId, + type: postTypeSlug, title: 'bar', content: 'bar', excerpt: 'crackers', status: currentPostStatus, } ); + currentUser = { id: userId }; editPostToSendOptimistic = () => { const postObject = { ...edits(), @@ -106,13 +111,7 @@ describe( 'Post generator actions', () => { return postObject; }; autoSavePost = { status: 'autosave', bar: 'foo' }; - autoSavePostToSend = () => ( - { - ...editPostToSendOptimistic(), - bar: 'foo', - status: 'autosave', - } - ); + autoSavePostToSend = () => editPostToSendOptimistic(); savedPost = () => ( { ...currentPost(), @@ -138,6 +137,7 @@ describe( 'Post generator actions', () => { fulfillment.next( postType ); fulfillment.next(); if ( isAutosaving ) { + fulfillment.next( currentUser ); fulfillment.next(); } else { fulfillment.next(); @@ -273,14 +273,27 @@ describe( 'Post generator actions', () => { }, ], [ - 'yield action for selecting the autoSavePost', + 'yields action for selecting the currentUser', ( isAutosaving ) => isAutosaving, () => { const { value } = fulfillment.next(); expect( value ).toEqual( - select( - STORE_KEY, - 'getAutosave' + resolveSelect( 'core', 'getCurrentUser' ) + ); + }, + ], + [ + 'yields action for selecting the autosavePost', + ( isAutosaving ) => isAutosaving, + () => { + const { value } = fulfillment.next( currentUser ); + expect( value ).toEqual( + resolveSelect( + 'core', + 'getAutosave', + postTypeSlug, + postId, + userId ) ); }, @@ -357,13 +370,12 @@ describe( 'Post generator actions', () => { 'yields action for dispatch the appropriate reset action', () => { const { value } = fulfillment.next( savedPost() ); - expect( value ).toEqual( - dispatch( - STORE_KEY, - isAutosave ? 'resetAutosave' : 'resetPost', - savedPost() - ) - ); + + if ( isAutosave ) { + expect( value ).toEqual( dispatch( 'core', 'receiveAutosaves', postId, savedPost() ) ); + } else { + expect( value ).toEqual( dispatch( STORE_KEY, 'resetPost', savedPost() ) ); + } }, ], [ @@ -667,17 +679,6 @@ describe( 'Editor actions', () => { } ); } ); - describe( 'resetAutosave', () => { - it( 'should return the RESET_AUTOSAVE action', () => { - const post = {}; - const result = actions.resetAutosave( post ); - expect( result ).toEqual( { - type: 'RESET_AUTOSAVE', - post, - } ); - } ); - } ); - describe( 'requestPostUpdateStart', () => { it( 'should return the REQUEST_POST_UPDATE_START action', () => { const result = actions.__experimentalRequestPostUpdateStart(); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 751563e77ecc3..d964c72c0f77a 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -17,7 +17,6 @@ import { preferences, saving, reusableBlocks, - autosave, postSavingLock, previewLink, } from '../reducer'; @@ -803,38 +802,6 @@ describe( 'state', () => { } ); } ); - describe( 'autosave', () => { - it( 'returns null by default', () => { - const state = autosave( undefined, {} ); - - expect( state ).toBe( null ); - } ); - - it( 'returns subset of received autosave post properties', () => { - const state = autosave( undefined, { - type: 'RESET_AUTOSAVE', - post: { - title: { - raw: 'The Title', - }, - content: { - raw: 'The Content', - }, - excerpt: { - raw: 'The Excerpt', - }, - status: 'draft', - }, - } ); - - expect( state ).toEqual( { - title: 'The Title', - content: 'The Content', - excerpt: 'The Excerpt', - } ); - } ); - } ); - describe( 'postSavingLock', () => { it( 'returns empty object by default', () => { const state = postSavingLock( undefined, {} ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 1740302fe73d6..f2e30f17372e1 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -44,15 +44,12 @@ const { isCurrentPostScheduled, isEditedPostPublishable, isEditedPostSaveable, - isEditedPostAutosaveable, - getAutosave, - hasAutosave, + isEditedPostAutosaveable: isEditedPostAutosaveableRegistrySelector, isEditedPostEmpty, isEditedPostBeingScheduled, isEditedPostDateFloating, getCurrentPostAttribute, getEditedPostAttribute, - getAutosaveAttribute, isSavingPost, didPostSaveRequestSucceed, didPostSaveRequestFail, @@ -616,42 +613,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'getAutosaveAttribute', () => { - it( 'returns null if there is no autosave', () => { - const state = { - autosave: null, - }; - - expect( getAutosaveAttribute( state, 'title' ) ).toBeNull(); - } ); - - it( 'returns undefined for an attribute which is not set', () => { - const state = { - autosave: {}, - }; - - expect( getAutosaveAttribute( state, 'foo' ) ).toBeUndefined(); - } ); - - it( 'returns undefined for object prototype member', () => { - const state = { - autosave: {}, - }; - - expect( getAutosaveAttribute( state, 'valueOf' ) ).toBeUndefined(); - } ); - - it( 'returns the attribute value', () => { - const state = { - autosave: { - title: 'Hello World', - }, - }; - - expect( getAutosaveAttribute( state, 'title' ) ).toBe( 'Hello World' ); - } ); - } ); - describe( 'getCurrentPostLastRevisionId', () => { it( 'should return null if the post has not yet been saved', () => { const state = { @@ -1305,7 +1266,19 @@ describe( 'selectors', () => { } ); describe( 'isEditedPostAutosaveable', () => { - it( 'should return false if the post is not saveable', () => { + it( 'should return false if existing autosaves have not yet been fetched', () => { + const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { + getCurrentUser() {}, + hasFetchedAutosaves() { + return false; + }, + getAutosave() { + return { + title: 'sassel', + }; + }, + } ) ); + const state = { editor: { present: { @@ -1322,15 +1295,54 @@ describe( 'selectors', () => { saving: { requesting: true, }, - autosave: { + }; + + expect( isEditedPostAutosaveable( state ) ).toBe( false ); + } ); + + it( 'should return false if the post is not saveable', () => { + const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() { + return { + title: 'sassel', + }; + }, + } ) ); + + const state = { + editor: { + present: { + blocks: { + value: [], + }, + edits: {}, + }, + }, + initialEdits: {}, + currentPost: { title: 'sassel', }, + saving: { + requesting: true, + }, }; expect( isEditedPostAutosaveable( state ) ).toBe( false ); } ); - it( 'should return true if there is not yet an autosave', () => { + it( 'should return true if there is no autosave', () => { + const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() {}, + } ) ); + const state = { editor: { present: { @@ -1345,13 +1357,25 @@ describe( 'selectors', () => { title: 'sassel', }, saving: {}, - autosave: null, }; expect( isEditedPostAutosaveable( state ) ).toBe( true ); } ); it( 'should return false if none of title, excerpt, or content have changed', () => { + const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() { + return { + title: 'foo', + excerpt: 'foo', + }; + }, + } ) ); + const state = { editor: { present: { @@ -1368,16 +1392,25 @@ describe( 'selectors', () => { excerpt: 'foo', }, saving: {}, - autosave: { - title: 'foo', - excerpt: 'foo', - }, }; expect( isEditedPostAutosaveable( state ) ).toBe( false ); } ); it( 'should return true if content has changes', () => { + const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() { + return { + title: 'foo', + excerpt: 'foo', + }; + }, + } ) ); + const state = { editor: { present: { @@ -1393,10 +1426,6 @@ describe( 'selectors', () => { excerpt: 'foo', }, saving: {}, - autosave: { - title: 'foo', - excerpt: 'foo', - }, }; expect( isEditedPostAutosaveable( state ) ).toBe( true ); @@ -1405,6 +1434,19 @@ describe( 'selectors', () => { it( 'should return true if title or excerpt have changed', () => { for ( const variantField of [ 'title', 'excerpt' ] ) { for ( const constantField of without( [ 'title', 'excerpt' ], variantField ) ) { + const isEditedPostAutosaveable = isEditedPostAutosaveableRegistrySelector( () => ( { + getCurrentUser() {}, + hasFetchedAutosaves() { + return true; + }, + getAutosave() { + return { + [ constantField ]: 'foo', + [ variantField ]: 'bar', + }; + }, + } ) ); + const state = { editor: { present: { @@ -1421,10 +1463,6 @@ describe( 'selectors', () => { content: 'foo', }, saving: {}, - autosave: { - [ constantField ]: 'foo', - [ variantField ]: 'bar', - }, }; expect( isEditedPostAutosaveable( state ) ).toBe( true ); @@ -1433,49 +1471,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'getAutosave', () => { - it( 'returns null if there is no autosave', () => { - const state = { - autosave: null, - }; - - const result = getAutosave( state ); - - expect( result ).toBe( null ); - } ); - - it( 'returns the autosave', () => { - const autosave = { title: '', excerpt: '', content: '' }; - const state = { autosave }; - - const result = getAutosave( state ); - - expect( result ).toEqual( autosave ); - } ); - } ); - - describe( 'hasAutosave', () => { - it( 'returns false if there is no autosave', () => { - const state = { - autosave: null, - }; - - const result = hasAutosave( state ); - - expect( result ).toBe( false ); - } ); - - it( 'returns true if there is a autosave', () => { - const state = { - autosave: { title: '', excerpt: '', content: '' }, - }; - - const result = hasAutosave( state ); - - expect( result ).toBe( true ); - } ); - } ); - describe( 'isEditedPostEmpty', () => { it( 'should return true if no blocks and no content', () => { const state = {