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
19 changes: 19 additions & 0 deletions docs/reference-guides/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,25 @@ _Returns_

- `boolean`: Whether there is a next edit or not.

### hasRevision

Returns true if a revision has been received for the given set of parameters, or false otherwise.

Note: This does not trigger a request for the revision from the API if it's not available in the local state.

_Parameters_

- _state_ `State`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _recordKey_ `EntityRecordKey`: The key of the entity record whose revision you want to check.
- _revisionKey_ `EntityRecordKey`: The revision's key.
- _query_ `GetRecordsHttpQuery`: Optional query.

_Returns_

- `boolean`: Whether a revision has been received.

### hasUndo

Returns true if there is a previous edit from the current undo offset for the entity records edits history, and false otherwise.
Expand Down
19 changes: 19 additions & 0 deletions packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,25 @@ _Returns_

- `boolean`: Whether there is a next edit or not.

### hasRevision

Returns true if a revision has been received for the given set of parameters, or false otherwise.

Note: This does not trigger a request for the revision from the API if it's not available in the local state.

_Parameters_

- _state_ `State`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
- _recordKey_ `EntityRecordKey`: The key of the entity record whose revision you want to check.
- _revisionKey_ `EntityRecordKey`: The revision's key.
- _query_ `GetRecordsHttpQuery`: Optional query.

_Returns_

- `boolean`: Whether a revision has been received.

### hasUndo

Returns true if there is a previous edit from the current undo offset for the entity records edits history, and false otherwise.
Expand Down
5 changes: 1 addition & 4 deletions packages/core-data/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1114,10 +1114,7 @@ export const receiveRevisions =
const entityConfig = configs.find(
( config ) => config.kind === kind && config.name === name
);
const key =
entityConfig && entityConfig?.revisionKey
? entityConfig.revisionKey
: DEFAULT_ENTITY_KEY;
const key = entityConfig?.revisionKey ?? DEFAULT_ENTITY_KEY;

dispatch( {
type: 'RECEIVE_ITEM_REVISIONS',
Expand Down
204 changes: 122 additions & 82 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
ALLOWED_RESOURCE_ACTIONS,
RECEIVE_INTERMEDIATE_RESULTS,
isNumericID,
normalizeQueryForResolution,
} from './utils';
import { fetchBlockPatterns } from './fetch';
import { restoreSelection, getSelectionHistory } from './utils/crdt-selection';
Expand Down Expand Up @@ -350,18 +351,14 @@ export const getEntityRecords =
const key = entityConfig.key || DEFAULT_ENTITY_KEY;

function getResolutionsArgs( records, recordsQuery ) {
const queryArgs = Object.fromEntries(
Object.entries( recordsQuery ).filter( ( [ k, v ] ) => {
return [ 'context', '_fields' ].includes( k ) && !! v;
} )
);
const normalizedQuery = normalizeQueryForResolution( recordsQuery );
return records
.filter( ( record ) => record?.[ key ] )
.map( ( record ) => [
kind,
name,
record[ key ],
Object.keys( queryArgs ).length > 0 ? queryArgs : undefined,
normalizedQuery,
] );
}

Expand Down Expand Up @@ -1036,93 +1033,105 @@ export const getRevisions =
return;
}

if ( query._fields ) {
// If requesting specific fields, items and query association to said
// records are stored by ID reference. Thus, fields must always include
// the ID.
query = {
...query,
_fields: [
...new Set( [
...( getNormalizedCommaSeparable( query._fields ) ||
[] ),
entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
] ),
].join(),
};
}

const path = addQueryArgs(
entityConfig.getRevisionsUrl( recordKey ),
query
const rawQuery = { ...query };
const lock = await dispatch.__unstableAcquireStoreLock(
STORE_NAME,
[ 'entities', 'records', kind, name, recordKey, 'revisions' ],
{ exclusive: false }
);

let records, response;
const meta = {};
const isPaginated =
entityConfig.supportsPagination && query.per_page !== -1;
try {
response = await apiFetch( { path, parse: ! isPaginated } );
} catch ( error ) {
// Do nothing if our request comes back with an API error.
return;
}

if ( response ) {
if ( isPaginated ) {
records = Object.values( await response.json() );
meta.totalItems = parseInt(
response.headers.get( 'X-WP-Total' )
);
} else {
records = Object.values( response );
if ( query._fields ) {
// If requesting specific fields, items and query association to said
// records are stored by ID reference. Thus, fields must always include
// the ID.
query = {
...query,
_fields: [
...new Set( [
...( getNormalizedCommaSeparable( query._fields ) ||
[] ),
entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
] ),
].join(),
};
}

// If we request fields but the result doesn't contain the fields,
// explicitly set these fields as "undefined"
// that way we consider the query "fulfilled".
if ( query._fields ) {
records = records.map( ( record ) => {
query._fields.split( ',' ).forEach( ( field ) => {
if ( ! record.hasOwnProperty( field ) ) {
record[ field ] = undefined;
}
} );
const path = addQueryArgs(
entityConfig.getRevisionsUrl( recordKey ),
query
);

return record;
} );
let records, response;
const meta = {};
const isPaginated =
entityConfig.supportsPagination && query.per_page !== -1;
try {
response = await apiFetch( { path, parse: ! isPaginated } );
} catch ( error ) {
// Do nothing if our request comes back with an API error.
return;
}

registry.batch( () => {
dispatch.receiveRevisions(
kind,
name,
recordKey,
records,
query,
false,
meta
);
if ( response ) {
if ( isPaginated ) {
records = Object.values( await response.json() );
meta.totalItems = parseInt(
response.headers.get( 'X-WP-Total' )
);
} else {
records = Object.values( response );
}

// If we request fields but the result doesn't contain the fields,
// explicitly set these fields as "undefined"
// that way we consider the query "fulfilled".
if ( query._fields ) {
records = records.map( ( record ) => {
query._fields.split( ',' ).forEach( ( field ) => {
if ( ! record.hasOwnProperty( field ) ) {
record[ field ] = undefined;
}
} );

return record;
} );
}

registry.batch( () => {
dispatch.receiveRevisions(
kind,
name,
recordKey,
records,
query,
false,
meta
);

// When requesting all fields, the list of results can be used to
// resolve the `getRevision` selector in addition to `getRevisions`.
if ( ! query?._fields && ! query.context ) {
// Mark individual getRevision resolutions as done so that
// subsequent getRevision calls skip redundant API fetches.
const key = entityConfig.revisionKey || DEFAULT_ENTITY_KEY;
const normalizedQuery =
normalizeQueryForResolution( rawQuery );
const resolutionsArgs = records
.filter( ( record ) => record[ key ] )
.map( ( record ) => [
kind,
name,
recordKey,
record[ key ],
normalizedQuery,
] );

dispatch.finishResolutions(
'getRevision',
resolutionsArgs
);
}
} );
} );
}
} finally {
dispatch.__unstableReleaseStoreLock( lock );
}
};

Expand All @@ -1147,7 +1156,7 @@ getRevisions.shouldInvalidate = ( action, kind, name, recordKey ) =>
*/
export const getRevision =
( kind, name, recordKey, revisionKey, query ) =>
async ( { dispatch, resolveSelect } ) => {
async ( { select, dispatch, resolveSelect } ) => {
const configs = await resolveSelect.getEntitiesConfig( kind );
const entityConfig = configs.find(
( config ) => config.name === name && config.kind === kind
Expand All @@ -1172,21 +1181,52 @@ export const getRevision =
].join(),
};
}
const path = addQueryArgs(
entityConfig.getRevisionsUrl( recordKey, revisionKey ),
query

const lock = await dispatch.__unstableAcquireStoreLock(
STORE_NAME,
[
'entities',
'records',
kind,
name,
recordKey,
'revisions',
revisionKey,
],
{ exclusive: false }
);

let record;
try {
record = await apiFetch( { path } );
} catch ( error ) {
// Do nothing if our request comes back with an API error.
return;
}
if (
select.hasRevision( kind, name, recordKey, revisionKey, query )
) {
return;
}

if ( record ) {
dispatch.receiveRevisions( kind, name, recordKey, record, query );
const path = addQueryArgs(
entityConfig.getRevisionsUrl( recordKey, revisionKey ),
query
);

let record;
try {
record = await apiFetch( { path } );
} catch ( error ) {
// Do nothing if our request comes back with an API error.
return;
}

if ( record ) {
dispatch.receiveRevisions(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a note to say I tested using getRevision in the `getCurrentRevision' selector on this branch, and the revisions UI was working fine in Chrome and Safari.

Context:

#74771 (comment)

https://github.com/WordPress/gutenberg/pull/74771/changes#diff-a1cdb9f3f4ce98ee4ac08627daff3c4cb003766468d4bb3780fe0be22a0ad87eR345

kind,
name,
recordKey,
record,
query
);
}
} finally {
dispatch.__unstableReleaseStoreLock( lock );
}
};

Expand Down
56 changes: 56 additions & 0 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,62 @@ export const getRevisions = (
return getQueriedItems( queriedStateRevisions, query );
};

/**
* Returns true if a revision has been received for the given set of parameters,
* or false otherwise.
*
* Note: This does not trigger a request for the revision from the API
* if it's not available in the local state.
*
* @param state State tree
* @param kind Entity kind.
* @param name Entity name.
* @param recordKey The key of the entity record whose revision you want to check.
* @param revisionKey The revision's key.
* @param query Optional query.
*
* @return Whether a revision has been received.
*/
export function hasRevision(
state: State,
kind: string,
name: string,
recordKey: EntityRecordKey,
revisionKey: EntityRecordKey,
query?: GetRecordsHttpQuery
): boolean {
const queriedState =
state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ];
if ( ! queriedState ) {
return false;
}
const context = query?.context ?? 'default';

if ( ! query || ! query._fields ) {
return !! queriedState.itemIsComplete[ context ]?.[ revisionKey ];
}

const item = queriedState.items[ context ]?.[ revisionKey ];
if ( ! item ) {
return false;
}

const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
for ( let i = 0; i < fields.length; i++ ) {
const path = fields[ i ].split( '.' );
let value = item;
for ( let p = 0; p < path.length; p++ ) {
const part = path[ p ];
if ( ! value || ! Object.hasOwn( value, part ) ) {
return false;
}
value = value[ part ];
}
}

return true;
}

/**
* Returns a single, specific revision of a parent entity.
*
Expand Down
Loading
Loading