Repository navigation
Core Data: Add support for autosaving entities. #16903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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,77 @@ 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', | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where is this selector defined?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. gutenberg/packages/core-data/src/selectors.js Line 440 in 9291845
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh so it was defined before hand? Do you think a user can access autosaves of another user?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably, they are just posts right? |
||||||||||||
| persistedRecord.type, | ||||||||||||
| persistedRecord.id, | ||||||||||||
| currentUserId | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason we need the currentUserId to fetch the autosaved post?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Autosaves are author-specific. |
||||||||||||
| ); | ||||||||||||
| // Autosaves need all expected fields to be present. | ||||||||||||
| // So we fallback to the previous autosave and then | ||||||||||||
| // to the actual persisted entity if the edits don't | ||||||||||||
| // have a value. | ||||||||||||
| let data = { ...persistedRecord, ...autosavePost, ...record }; | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm having trouble understanding which data we're sending. Maybe a comment could help. Like do we send just specific record properties, everything, why are we merging these three records, and why that order.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added this comment:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I'm wondering why we need the three steps merge. If I'm undersanding properly, we don't really need
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not always. It has 3 items because we have 2 fallbacks. First, the previous autosave, and then the persisted values.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really? The UI only shows the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you would but that's an explicit action.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would not work, you would need to merge the properties like in this PR. Why complicate things when this will be the only usage of the API. When would you want to clear autosaved properties without changes?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does it work today in the editor screen without the refactoring? Say I have an autosave but I ignore it and I continue editing my post, another autosave will trigger right without the updates from the previous autosave? How would you do that in this new approach? To be honest, I don't want to complicate anything. I actually think I'm simplifying things. Anyway, I won't block that PR for this, I feel we can tweak this later if needed but still cuirious about the answer to the question above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It works like this right now.
It will have the updates from the previous autosave. gutenberg/packages/editor/src/store/actions.js Lines 524 to 528 in 9291845
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ok let's keep it but it feels like a bug to me. cc @adamsilverstein if you know whether an autosave should be created based on the previous autosave or based on the things that are shown in the UI (previous saved state + edits) |
||||||||||||
| data = Object.keys( data ).reduce( ( acc, key ) => { | ||||||||||||
| if ( key in [ 'title', 'excerpt', 'content' ] ) { | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this something to be configured per entity (autosaveable fields or something)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, good idea. |
||||||||||||
| acc[ key ] = get( data[ key ], 'raw', data[ key ] ); | ||||||||||||
| } | ||||||||||||
| return acc; | ||||||||||||
| }, {} ); | ||||||||||||
| const autosave = yield apiFetch( { | ||||||||||||
| path: `${ path }/autosaves`, | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think this is a convention for all entities autosaves? Do we need to check whether a given entity supports autosaves?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought it was. Should it be part of the entity config?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can start this way if it's consistent across WP entities, I was just asking. |
||||||||||||
| 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 +293,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 +306,7 @@ export function* saveEditedEntityRecord( kind, name, recordId ) { | |||||||||||
| recordId | ||||||||||||
| ); | ||||||||||||
| const record = { id: recordId, ...edits }; | ||||||||||||
| yield* saveEntityRecord( kind, name, record ); | ||||||||||||
| yield* saveEntityRecord( kind, name, record, options ); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on the code changes, it feels that the common code is very small between autosaves and saves, which makes me wonder if they should be two separate action creators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that maybe future
optionscould make them share more code.Should we separate them for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine either way, you're in a better position to judge as you worked on it :)