diff --git a/docs/designers-developers/developers/data/data-core.md b/docs/designers-developers/developers/data/data-core.md index c2d840ada30b8..bdfbe68438a39 100644 --- a/docs/designers-developers/developers/data/data-core.md +++ b/docs/designers-developers/developers/data/data-core.md @@ -343,6 +343,21 @@ _Returns_ - `boolean`: Whether or not the user can upload media. Defaults to `true` if the OPTIONS request is being made. +# **isAutosavingEntityRecord** + +Returns true if the specified entity record is autosaving, and false otherwise. + +_Parameters_ + +- _state_ `Object`: State tree. +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordId_ `number`: Record ID. + +_Returns_ + +- `boolean`: Whether the entity record is autosaving or not. + # **isPreviewEmbedFallback** Determines if the returned preview is an oEmbed link fallback. @@ -387,7 +402,7 @@ _Parameters_ _Returns_ -- `?Object`: Whether the entity record is saving or not. +- `boolean`: Whether the entity record is saving or not. @@ -545,6 +560,7 @@ _Parameters_ - _kind_ `string`: Kind of the entity. - _name_ `string`: Name of the entity. - _recordId_ `Object`: ID of the record. +- _options_ `Object`: Saving options. # **saveEntityRecord** @@ -555,6 +571,7 @@ _Parameters_ - _kind_ `string`: Kind of the received entity. - _name_ `string`: Name of the received entity. - _record_ `Object`: Record to be saved. +- _options_ `Object`: Saving options. # **undo** diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 73bac19d076d0..cc89ec095f511 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -191,6 +191,7 @@ _Parameters_ - _kind_ `string`: Kind of the entity. - _name_ `string`: Name of the entity. - _recordId_ `Object`: ID of the record. +- _options_ `Object`: Saving options. # **saveEntityRecord** @@ -201,6 +202,7 @@ _Parameters_ - _kind_ `string`: Kind of the received entity. - _name_ `string`: Name of the received entity. - _record_ `Object`: Record to be saved. +- _options_ `Object`: Saving options. # **undo** @@ -552,6 +554,21 @@ _Returns_ - `boolean`: Whether or not the user can upload media. Defaults to `true` if the OPTIONS request is being made. +# **isAutosavingEntityRecord** + +Returns true if the specified entity record is autosaving, and false otherwise. + +_Parameters_ + +- _state_ `Object`: State tree. +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordId_ `number`: Record ID. + +_Returns_ + +- `boolean`: Whether the entity record is autosaving or not. + # **isPreviewEmbedFallback** Determines if the returned preview is an oEmbed link fallback. @@ -596,7 +613,7 @@ _Parameters_ _Returns_ -- `?Object`: Whether the entity record is saving or not. +- `boolean`: Whether the entity record is saving or not. diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 8e78957bbb675..45e42d7b36673 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, merge, isEqual, find } from 'lodash'; +import { castArray, get, merge, isEqual, find } from 'lodash'; /** * Internal dependencies @@ -127,7 +127,11 @@ export function receiveEmbedPreview( url, preview ) { * @return {Object} Action object. */ export function* editEntityRecord( kind, name, recordId, edits ) { - const { transientEdits = {}, mergedEdits = {} } = yield select( 'getEntity', kind, name ); + const { transientEdits = {}, mergedEdits = {} } = yield select( + 'getEntity', + kind, + name + ); const record = yield select( 'getEntityRecord', kind, name, recordId ); const editedRecord = yield select( 'getEditedEntityRecord', @@ -143,10 +147,11 @@ export function* editEntityRecord( kind, name, recordId, edits ) { // Clear edits when they are equal to their persisted counterparts // so that the property is not considered dirty. edits: Object.keys( edits ).reduce( ( acc, key ) => { + const recordValue = get( record[ key ], 'raw', record[ key ] ); const value = mergedEdits[ key ] ? - merge( record[ key ], edits[ key ] ) : + merge( recordValue, edits[ key ] ) : edits[ key ]; - acc[ key ] = isEqual( record[ key ], value ) ? undefined : value; + acc[ key ] = isEqual( recordValue, value ) ? undefined : value; return acc; }, {} ), transientEdits, @@ -209,29 +214,73 @@ export function* redo() { * @param {string} kind Kind of the received entity. * @param {string} name Name of the received entity. * @param {Object} record Record to be saved. + * @param {Object} options Saving options. */ -export function* saveEntityRecord( kind, name, record ) { +export function* saveEntityRecord( + kind, + name, + record, + { isAutosave = false } = { isAutosave: false } +) { const entities = yield getKindEntities( kind ); const entity = find( entities, { kind, name } ); if ( ! entity ) { return; } - const key = entity.key || DEFAULT_ENTITY_KEY; - const recordId = record[ key ]; + const entityIdKey = entity.key || DEFAULT_ENTITY_KEY; + const recordId = record[ entityIdKey ]; - yield { type: 'SAVE_ENTITY_RECORD_START', kind, name, recordId }; + yield { type: 'SAVE_ENTITY_RECORD_START', kind, name, recordId, isAutosave }; let error; try { - const updatedRecord = yield apiFetch( { - path: `${ entity.baseURL }${ recordId ? '/' + recordId : '' }`, - method: recordId ? 'PUT' : 'POST', - data: record, - } ); - yield receiveEntityRecords( kind, name, updatedRecord, undefined, true ); + const path = `${ entity.baseURL }${ recordId ? '/' + recordId : '' }`; + if ( isAutosave ) { + const persistedRecord = yield select( + 'getEntityRecord', + kind, + name, + recordId + ); + const currentUser = yield select( 'getCurrentUser' ); + const currentUserId = currentUser ? currentUser.id : undefined; + const autosavePost = yield select( + 'getAutosave', + persistedRecord.type, + persistedRecord.id, + currentUserId + ); + let data = { ...persistedRecord, ...autosavePost, ...record }; + data = Object.keys( data ).reduce( ( acc, key ) => { + if ( key in [ 'title', 'excerpt', 'content' ] ) { + acc[ key ] = get( data[ key ], 'raw', data[ key ] ); + } + return acc; + }, {} ); + const autosave = yield apiFetch( { + path: `${ path }/autosaves`, + method: 'POST', + data, + } ); + yield receiveAutosaves( persistedRecord.id, autosave ); + } else { + const updatedRecord = yield apiFetch( { + path, + method: recordId ? 'PUT' : 'POST', + data: record, + } ); + yield receiveEntityRecords( kind, name, updatedRecord, undefined, true ); + } } catch ( _error ) { error = _error; } - yield { type: 'SAVE_ENTITY_RECORD_FINISH', kind, name, recordId, error }; + yield { + type: 'SAVE_ENTITY_RECORD_FINISH', + kind, + name, + recordId, + error, + isAutosave, + }; } /** @@ -240,8 +289,9 @@ export function* saveEntityRecord( kind, name, record ) { * @param {string} kind Kind of the entity. * @param {string} name Name of the entity. * @param {Object} recordId ID of the record. + * @param {Object} options Saving options. */ -export function* saveEditedEntityRecord( kind, name, recordId ) { +export function* saveEditedEntityRecord( kind, name, recordId, options ) { if ( ! ( yield select( 'hasEditsForEntityRecord', kind, name, recordId ) ) ) { return; } @@ -252,7 +302,7 @@ export function* saveEditedEntityRecord( kind, name, recordId ) { recordId ); const record = { id: recordId, ...edits }; - yield* saveEntityRecord( kind, name, record ); + yield* saveEntityRecord( kind, name, record, options ); } /** diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 96ed12e5ef78c..7f0972e19e481 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -213,6 +213,7 @@ function entity( entityConfig ) { [ action.recordId ]: { pending: action.type === 'SAVE_ENTITY_RECORD_START', error: action.error, + isAutosave: action.isAutosave, }, }; } diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index bc418e3482ba6..c8aec286a1107 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -207,6 +207,25 @@ export const getEditedEntityRecord = createSelector( ( state ) => [ state.entities.data ] ); +/** + * Returns true if the specified entity record is autosaving, and false otherwise. + * + * @param {Object} state State tree. + * @param {string} kind Entity kind. + * @param {string} name Entity name. + * @param {number} recordId Record ID. + * + * @return {boolean} Whether the entity record is autosaving or not. + */ +export function isAutosavingEntityRecord( state, kind, name, recordId ) { + const { pending, isAutosave } = get( + state.entities.data, + [ kind, name, 'saving', recordId ], + {} + ); + return Boolean( pending && isAutosave ); +} + /** * Returns true if the specified entity record is saving, and false otherwise. * @@ -215,14 +234,15 @@ export const getEditedEntityRecord = createSelector( * @param {string} name Entity name. * @param {number} recordId Record ID. * - * @return {Object?} Whether the entity record is saving or not. + * @return {boolean} Whether the entity record is saving or not. */ export function isSavingEntityRecord( state, kind, name, recordId ) { - return get( + const { pending, isAutosave } = get( state.entities.data, - [ kind, name, 'saving', recordId, 'pending' ], - false + [ kind, name, 'saving', recordId ], + {} ); + return Boolean( pending && ! isAutosave ); } /**