Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions docs/designers-developers/developers/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,6 @@ _Returns_

- `?Array`: An array of autosaves for the post, or undefined if there is none.

<a name="getCurrentUndoOffset" href="#getCurrentUndoOffset">#</a> **getCurrentUndoOffset**

Returns the current undo offset for the
entity records edits history. The offset
represents how many items from the end
of the history stack we are at. 0 is the
last edit, -1 is the second last, and so on.

_Parameters_

- _state_ `Object`: State tree.

_Returns_

- `number`: The current undo offset.

<a name="getCurrentUser" href="#getCurrentUser">#</a> **getCurrentUser**

Returns the current user.
Expand Down Expand Up @@ -359,6 +343,21 @@ _Returns_

- `boolean`: Whether or not the user can upload media. Defaults to `true` if the OPTIONS request is being made.

<a name="isAutosavingEntityRecord" href="#isAutosavingEntityRecord">#</a> **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.

<a name="isPreviewEmbedFallback" href="#isPreviewEmbedFallback">#</a> **isPreviewEmbedFallback**

Determines if the returned preview is an oEmbed link fallback.
Expand Down Expand Up @@ -403,7 +402,7 @@ _Parameters_

_Returns_

- `?Object`: Whether the entity record is saving or not.
- `boolean`: Whether the entity record is saving or not.


<!-- END TOKEN(Autogenerated selectors) -->
Expand Down Expand Up @@ -561,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.

<a name="saveEntityRecord" href="#saveEntityRecord">#</a> **saveEntityRecord**

Expand All @@ -571,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.

<a name="undo" href="#undo">#</a> **undo**

Expand Down
35 changes: 18 additions & 17 deletions packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a name="saveEntityRecord" href="#saveEntityRecord">#</a> **saveEntityRecord**

Expand All @@ -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.

<a name="undo" href="#undo">#</a> **undo**

Expand Down Expand Up @@ -280,22 +282,6 @@ _Returns_

- `?Array`: An array of autosaves for the post, or undefined if there is none.

<a name="getCurrentUndoOffset" href="#getCurrentUndoOffset">#</a> **getCurrentUndoOffset**

Returns the current undo offset for the
entity records edits history. The offset
represents how many items from the end
of the history stack we are at. 0 is the
last edit, -1 is the second last, and so on.

_Parameters_

- _state_ `Object`: State tree.

_Returns_

- `number`: The current undo offset.

<a name="getCurrentUser" href="#getCurrentUser">#</a> **getCurrentUser**

Returns the current user.
Expand Down Expand Up @@ -568,6 +554,21 @@ _Returns_

- `boolean`: Whether or not the user can upload media. Defaults to `true` if the OPTIONS request is being made.

<a name="isAutosavingEntityRecord" href="#isAutosavingEntityRecord">#</a> **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.

<a name="isPreviewEmbedFallback" href="#isPreviewEmbedFallback">#</a> **isPreviewEmbedFallback**

Determines if the returned preview is an oEmbed link fallback.
Expand Down Expand Up @@ -612,7 +613,7 @@ _Parameters_

_Returns_

- `?Object`: Whether the entity record is saving or not.
- `boolean`: Whether the entity record is saving or not.


<!-- END TOKEN(Autogenerated selectors) -->
Expand Down
88 changes: 71 additions & 17 deletions packages/core-data/src/actions.js
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
Expand Down Expand Up @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -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 }

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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 options could make them share more code.

Should we separate them for now?

Copy link
Copy Markdown
Contributor

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 :)

) {
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',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this selector defined?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export function getAutosave( state, postType, postId, authorId ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably, they are just posts right?

persistedRecord.type,
persistedRecord.id,
currentUserId

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason we need the currentUserId to fetch the autosaved post?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this comment:

// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 autosavePost here since the edits will most likely contain these as well right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? The UI only shows the edits and not the autosaved content so I'm having trouble thinking of use-cases where autosaves have more data than edits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you would but that's an explicit action. saveEntityRecord( getAutosave() )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use this fallback mechanism in the editor store right now.

It works like this right now.

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?

It will have the updates from the previous autosave.

toSend = {
...pick( post, AUTOSAVE_PROPERTIES ),
...mappedAutosavePost,
...toSend,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works like this right now.

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' ] ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something to be configured per entity (autosaveable fields or something)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was. Should it be part of the entity config?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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,
};
}

/**
Expand All @@ -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;
}
Expand All @@ -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 );
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/core-data/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ function entity( entityConfig ) {
[ action.recordId ]: {
pending: action.type === 'SAVE_ENTITY_RECORD_START',
error: action.error,
isAutosave: action.isAutosave,
},
};
}
Expand Down
30 changes: 25 additions & 5 deletions packages/core-data/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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 );
}

/**
Expand Down Expand Up @@ -250,7 +270,7 @@ export function getLastEntitySaveError( state, kind, name, recordId ) {
*
* @return {number} The current undo offset.
*/
export function getCurrentUndoOffset( state ) {
function getCurrentUndoOffset( state ) {
return state.undo.offset;
}

Expand Down