diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index ea97ce28e4d85c..6084eff930394a 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -420,6 +420,39 @@ _Returns_ - A value whose reference will change only when an edit occurs. +### getRevision + +Returns a single, specific revision of a parent entity. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _revisionKey_ `EntityRecordKey`: The revision's key. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [entity kind]". + +_Returns_ + +- `RevisionRecord | Record< PropertyKey, never > | undefined`: Record. + +### getRevisions + +Returns an entity's revisions. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [Entity kind]". + +_Returns_ + +- `RevisionRecord[] | null`: Record. + ### getThemeSupports Return theme supports data in the index. @@ -704,6 +737,24 @@ _Returns_ - `Object`: Action object. +### receiveRevisions + +Returns an action object used in signalling that revisions have been received. + +_Parameters_ + +- _kind_ `string`: Kind of the received entity record revisions. +- _name_ `string`: Name of the received entity record revisions. +- _recordKey_ `number|string`: The key of the entity record whose revisions you want to fetch. +- _records_ `Array|Object`: Revisions received. +- _query_ `?Object`: Query Object. +- _invalidateCache_ `?boolean`: Should invalidate query caches. +- _meta_ `?Object`: Meta information about pagination. + +_Returns_ + +- `Object`: Action object. + ### receiveThemeSupports > **Deprecated** since WP 5.9, this is not useful anymore, use the selector direclty. diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index d6ee7486934332..0e6ecc2a9db4ba 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -9,6 +9,7 @@ ## Enhancements - Add `getEntityRecordsTotalItems` and `getEntityRecordsTotalPages` selectors. [#55164](https://github.com/WordPress/gutenberg/pull/55164). +- Revisions: add new selectors, `getRevisions` and `getRevision`, to fetch entity revisions. [#54046](https://github.com/WordPress/gutenberg/pull/54046). ## 6.20.0 (2023-10-05) diff --git a/packages/core-data/README.md b/packages/core-data/README.md index ef5d9c1197f099..f7a177b5c55872 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -123,7 +123,7 @@ The package provides general methods to interact with the entities (`getEntityRe ```js // Get the record collection for the user entity. -wp.data.select( 'core' ).getEntityRecords( 'root' 'user' ); +wp.data.select( 'core' ).getEntityRecords( 'root', 'user' ); // Get a single record for the user entity. wp.data.select( 'core' ).getEntityRecord( 'root', 'user', recordId ); @@ -138,7 +138,7 @@ In addition to the general utilities (`getEntityRecords`, `getEntityRecord`, etc ```js // Collection -wp.data.select( 'core' ).getEntityRecords( 'root' 'user' ); +wp.data.select( 'core' ).getEntityRecords( 'root', 'user' ); wp.data.select( 'core' ).getUsers(); // Single record @@ -248,6 +248,24 @@ _Returns_ - `Object`: Action object. +### receiveRevisions + +Returns an action object used in signalling that revisions have been received. + +_Parameters_ + +- _kind_ `string`: Kind of the received entity record revisions. +- _name_ `string`: Name of the received entity record revisions. +- _recordKey_ `number|string`: The key of the entity record whose revisions you want to fetch. +- _records_ `Array|Object`: Revisions received. +- _query_ `?Object`: Query Object. +- _invalidateCache_ `?boolean`: Should invalidate query caches. +- _meta_ `?Object`: Meta information about pagination. + +_Returns_ + +- `Object`: Action object. + ### receiveThemeSupports > **Deprecated** since WP 5.9, this is not useful anymore, use the selector direclty. @@ -727,6 +745,39 @@ _Returns_ - A value whose reference will change only when an edit occurs. +### getRevision + +Returns a single, specific revision of a parent entity. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _revisionKey_ `EntityRecordKey`: The revision's key. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [entity kind]". + +_Returns_ + +- `RevisionRecord | Record< PropertyKey, never > | undefined`: Record. + +### getRevisions + +Returns an entity's revisions. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [Entity kind]". + +_Returns_ + +- `RevisionRecord[] | null`: Record. + ### getThemeSupports Return theme supports data in the index. diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 9e7277f35a62a7..d71c5d6120089e 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -924,3 +924,36 @@ export function receiveDefaultTemplateId( query, templateId ) { templateId, }; } + +/** + * Returns an action object used in signalling that revisions have been received. + * + * @param {string} kind Kind of the received entity record revisions. + * @param {string} name Name of the received entity record revisions. + * @param {number|string} recordKey The key of the entity record whose revisions you want to fetch. + * @param {Array|Object} records Revisions received. + * @param {?Object} query Query Object. + * @param {?boolean} invalidateCache Should invalidate query caches. + * @param {?Object} meta Meta information about pagination. + * @return {Object} Action object. + */ +export function receiveRevisions( + kind, + name, + recordKey, + records, + query, + invalidateCache = false, + meta +) { + return { + type: 'RECEIVE_ITEM_REVISIONS', + items: Array.isArray( records ) ? records : [ records ], + recordKey, + meta, + query, + kind, + name, + invalidateCache, + }; +} diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index af8829d0bc852c..e85673492ef56e 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -19,6 +19,10 @@ export const DEFAULT_ENTITY_KEY = 'id'; const POST_RAW_ATTRIBUTES = [ 'title', 'excerpt', 'content' ]; +// A hardcoded list of post types that support revisions. +// @TODO: Ideally this should be fetched from the `/types` REST API's view context. +const POST_TYPES_WITH_REVISIONS_SUPPORT = [ 'post', 'page' ]; + export const rootEntitiesConfig = [ { label: __( 'Base' ), @@ -207,6 +211,12 @@ export const rootEntitiesConfig = [ baseURLParams: { context: 'edit' }, plural: 'globalStylesVariations', // Should be different than name. getTitle: ( record ) => record?.title?.rendered || record?.title, + getRevisionsUrl: ( parentId ) => + `/wp/v2/global-styles/${ parentId }/revisions`, + supports: { + revisions: true, + }, + supportsPagination: true, }, { label: __( 'Themes' ), @@ -295,6 +305,11 @@ async function loadPostTypeEntities() { selection: true, }, mergedEdits: { meta: true }, + supports: { + revisions: POST_TYPES_WITH_REVISIONS_SUPPORT.includes( + postType?.slug + ), + }, rawAttributes: POST_RAW_ATTRIBUTES, getTitle: ( record ) => record?.title?.rendered || @@ -328,6 +343,12 @@ async function loadPostTypeEntities() { syncObjectType: 'postType/' + postType.name, getSyncObjectId: ( id ) => id, supportsPagination: true, + getRevisionsUrl: ( parentId, revisionId ) => + `/${ namespace }/${ + postType.rest_base + }/${ parentId }/revisions${ + revisionId ? '/' + revisionId : '' + }`, }; } ); } diff --git a/packages/core-data/src/entity-types/global-styles-revision.ts b/packages/core-data/src/entity-types/global-styles-revision.ts new file mode 100644 index 00000000000000..1a89c164e313b0 --- /dev/null +++ b/packages/core-data/src/entity-types/global-styles-revision.ts @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import type { Context, ContextualField, OmitNevers } from './helpers'; + +import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; + +declare module './base-entity-records' { + export namespace BaseEntityRecords { + export interface GlobalStylesRevision< C extends Context > { + /** + * The ID for the author of the global styles revision. + */ + author: number; + /** + * The date the post global styles revision published, in the site's timezone. + */ + date: string | null; + /** + * The date the global styles revision was published, as GMT. + */ + date_gmt: ContextualField< string | null, 'view' | 'edit', C >; + /** + * Unique identifier for the revision. + */ + id: number; + /** + * The date the global styles revision was last modified, in the site's timezone. + */ + modified: ContextualField< string, 'view' | 'edit', C >; + /** + * The date the global styles revision was last modified, as GMT. + */ + modified_gmt: ContextualField< string, 'view' | 'edit', C >; + /** + * Identifier for the parent of the revision. + */ + parent: number; + styles: Record< string, Object >; + settings: Record< string, Object >; + } + } +} + +export type GlobalStylesRevision< C extends Context = 'view' > = OmitNevers< + _BaseEntityRecords.GlobalStylesRevision< C > +>; diff --git a/packages/core-data/src/entity-types/index.ts b/packages/core-data/src/entity-types/index.ts index 19d10a28ad698e..0e601137cbcb6c 100644 --- a/packages/core-data/src/entity-types/index.ts +++ b/packages/core-data/src/entity-types/index.ts @@ -4,12 +4,14 @@ import type { Context, Updatable } from './helpers'; import type { Attachment } from './attachment'; import type { Comment } from './comment'; +import type { GlobalStylesRevision } from './global-styles-revision'; import type { MenuLocation } from './menu-location'; import type { NavMenu } from './nav-menu'; import type { NavMenuItem } from './nav-menu-item'; import type { Page } from './page'; import type { Plugin } from './plugin'; import type { Post } from './post'; +import type { PostRevision } from './post-revision'; import type { Settings } from './settings'; import type { Sidebar } from './sidebar'; import type { Taxonomy } from './taxonomy'; @@ -27,12 +29,14 @@ export type { Attachment, Comment, Context, + GlobalStylesRevision, MenuLocation, NavMenu, NavMenuItem, Page, Plugin, Post, + PostRevision, Settings, Sidebar, Taxonomy, @@ -82,12 +86,14 @@ export interface PerPackageEntityRecords< C extends Context > { core: | Attachment< C > | Comment< C > + | GlobalStylesRevision< C > | MenuLocation< C > | NavMenu< C > | NavMenuItem< C > | Page< C > | Plugin< C > | Post< C > + | PostRevision< C > | Settings< C > | Sidebar< C > | Taxonomy< C > diff --git a/packages/core-data/src/entity-types/post-revision.ts b/packages/core-data/src/entity-types/post-revision.ts new file mode 100644 index 00000000000000..354a3fc02af704 --- /dev/null +++ b/packages/core-data/src/entity-types/post-revision.ts @@ -0,0 +1,93 @@ +/** + * Internal dependencies + */ +import type { + Context, + ContextualField, + RenderedText, + OmitNevers, +} from './helpers'; + +import type { BaseEntityRecords as _BaseEntityRecords } from './base-entity-records'; + +declare module './base-entity-records' { + export namespace BaseEntityRecords { + export interface PostRevision< C extends Context > { + /** + * The ID for the author of the post revision. + */ + author: number; + /** + * The content for the post. + */ + content: ContextualField< + RenderedText< C > & { + /** + * Whether the content is protected with a password. + */ + is_protected: boolean; + /** + * Version of the content block format used by the post. + */ + block_version: ContextualField< string, 'edit', C >; + }, + 'view' | 'edit', + C + >; + /** + * The date the post was published, in the site's timezone. + */ + date: string | null; + /** + * The date the post was published, as GMT. + */ + date_gmt: ContextualField< string | null, 'view' | 'edit', C >; + /** + * The excerpt for the post revision. + */ + excerpt: RenderedText< C > & { + protected: boolean; + }; + /** + * The globally unique identifier for the post. + */ + guid: ContextualField< RenderedText< C >, 'view' | 'edit', C >; + /** + * Unique identifier for the revision. + */ + id: number; + /** + * Meta fields. + */ + meta: ContextualField< + Record< string, string >, + 'view' | 'edit', + C + >; + /** + * The date the post was last modified, in the site's timezone. + */ + modified: ContextualField< string, 'view' | 'edit', C >; + /** + * The date the post revision was last modified, as GMT. + */ + modified_gmt: ContextualField< string, 'view' | 'edit', C >; + /** + * Identifier for the parent of the revision. + */ + parent: number; + /** + * An alphanumeric identifier for the post unique to its type. + */ + slug: string; + /** + * The title for the post revision. + */ + title: RenderedText< C >; + } + } +} + +export type PostRevision< C extends Context = 'view' > = OmitNevers< + _BaseEntityRecords.PostRevision< C > +>; diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index a21623d8ba89d3..34558fcfbb142e 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -245,7 +245,6 @@ function entity( entityConfig ) { ] )( combineReducers( { queriedData: queriedDataReducer, - edits: ( state = {}, action ) => { switch ( action.type ) { case 'RECEIVE_ITEMS': @@ -355,6 +354,52 @@ function entity( entityConfig ) { return state; }, + + // Add revisions to the state tree if the post type supports it. + ...( entityConfig?.supports?.revisions + ? { + revisions: ( state = {}, action ) => { + // Use the same queriedDataReducer shape for revisions. + if ( action.type === 'RECEIVE_ITEM_REVISIONS' ) { + const recordKey = action.recordKey; + delete action.recordKey; + const newState = queriedDataReducer( + state[ recordKey ], + { + ...action, + type: 'RECEIVE_ITEMS', + } + ); + return { + ...state, + [ recordKey ]: newState, + }; + } + + if ( action.type === 'REMOVE_ITEMS' ) { + return Object.fromEntries( + Object.entries( state ).filter( + ( [ id ] ) => + ! action.itemIds.some( + ( itemId ) => { + if ( + Number.isInteger( + itemId + ) + ) { + return itemId === +id; + } + return itemId === id; + } + ) + ) + ); + } + + return state; + }, + } + : {} ), } ) ); } diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index cd2a65a60b0139..8735764a880b8b 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -244,7 +244,7 @@ export const getEntityRecords = // If we request fields but the result doesn't contain the fields, // explicitly set these fields as "undefined" - // that way we consider the query "fullfilled". + // that way we consider the query "fulfilled". if ( query._fields ) { records = records.map( ( record ) => { query._fields.split( ',' ).forEach( ( field ) => { @@ -322,7 +322,7 @@ export const getCurrentTheme = export const getThemeSupports = forwardResolver( 'getCurrentTheme' ); /** - * Requests a preview from the from the Embed API. + * Requests a preview from the Embed API. * * @param {string} url URL to get the preview for. */ @@ -718,3 +718,146 @@ export const getDefaultTemplateId = dispatch.receiveDefaultTemplateId( query, template.id ); } }; + +/** + * Requests an entity's revisions from the REST API. + * + * @param {string} kind Entity kind. + * @param {string} name Entity name. + * @param {number|string} recordKey The key of the entity record whose revisions you want to fetch. + * @param {Object|undefined} query Optional object of query parameters to + * include with request. If requesting specific + * fields, fields must always include the ID. + */ +export const getRevisions = + ( kind, name, recordKey, query = {} ) => + async ( { dispatch } ) => { + const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); + const entityConfig = configs.find( + ( config ) => config.name === name && config.kind === kind + ); + + if ( + ! entityConfig || + entityConfig?.__experimentalNoFetch || + ! entityConfig?.supports?.revisions + ) { + 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 ) || + [] ), + DEFAULT_ENTITY_KEY, + ] ), + ].join(), + }; + } + + const path = addQueryArgs( + entityConfig.getRevisionsUrl( recordKey ), + query + ); + + let records, meta; + if ( entityConfig.supportsPagination && query.per_page !== -1 ) { + const response = await apiFetch( { path, parse: false } ); + records = Object.values( await response.json() ); + meta = { + totalItems: parseInt( response.headers.get( 'X-WP-Total' ) ), + }; + } else { + records = Object.values( await apiFetch( { path } ) ); + } + + // 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; + } ); + } + + dispatch.receiveRevisions( + kind, + name, + recordKey, + records, + query, + false, + meta + ); + }; + +// Invalidate cache when a new revision is created. +getRevisions.shouldInvalidate = ( action, kind, name, recordKey ) => + action.type === 'SAVE_ENTITY_RECORD_FINISH' && + name === action.name && + kind === action.kind && + ! action.error && + recordKey === action.recordId; + +/** + * Requests a specific Entity revision from the REST API. + * + * @param {string} kind Entity kind. + * @param {string} name Entity name. + * @param {number|string} recordKey The key of the entity record whose revisions you want to fetch. + * @param {number|string} revisionKey The revision's key. + * @param {Object|undefined} query Optional object of query parameters to + * include with request. If requesting specific + * fields, fields must always include the ID. + */ +export const getRevision = + ( kind, name, recordKey, revisionKey, query = {} ) => + async ( { dispatch } ) => { + const configs = await dispatch( getOrLoadEntitiesConfig( kind ) ); + const entityConfig = configs.find( + ( config ) => config.name === name && config.kind === kind + ); + + if ( + ! entityConfig || + entityConfig?.__experimentalNoFetch || + ! entityConfig?.supports?.revisions + ) { + return; + } + + if ( query !== undefined && 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 ) || + [] ), + DEFAULT_ENTITY_KEY, + ] ), + ].join(), + }; + } + const path = addQueryArgs( + entityConfig.getRevisionsUrl( recordKey, revisionKey ), + query + ); + + const record = await apiFetch( { path } ); + dispatch.receiveRevisions( kind, name, recordKey, record, query ); + }; diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 2a046941611c7d..4a893f5557d862 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -62,6 +62,16 @@ interface QueriedData { queries: Record< ET.Context, Record< string, Array< number > > >; } +type RevisionRecord = + | Record< ET.Context, Record< number, ET.PostRevision > > + | Record< ET.Context, Record< number, ET.GlobalStylesRevision > >; + +interface RevisionsQueriedData { + items: RevisionRecord; + itemIsComplete: Record< ET.Context, Record< number, boolean > >; + queries: Record< ET.Context, Record< string, Array< number > > >; +} + interface EntityState< EntityRecord extends ET.EntityRecord > { edits: Record< string, Partial< EntityRecord > >; saving: Record< @@ -70,6 +80,7 @@ interface EntityState< EntityRecord extends ET.EntityRecord > { >; deleting: Record< string, Partial< { pending: boolean; error: Error } > >; queriedData: QueriedData; + revisions?: RevisionsQueriedData; } interface EntityConfig { @@ -1373,3 +1384,103 @@ export function getDefaultTemplateId( ): string { return state.defaultTemplates[ JSON.stringify( query ) ]; } + +/** + * Returns an entity's revisions. + * + * @param state State tree + * @param kind Entity kind. + * @param name Entity name. + * @param recordKey The key of the entity record whose revisions you want to fetch. + * @param query Optional query. If requesting specific + * fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [Entity kind]". + * + * @return Record. + */ +export const getRevisions = ( + state: State, + kind: string, + name: string, + recordKey: EntityRecordKey, + query?: GetRecordsHttpQuery +): RevisionRecord[] | null => { + const queriedStateRevisions = + state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ]; + if ( ! queriedStateRevisions ) { + return null; + } + + return getQueriedItems( queriedStateRevisions, query ); +}; + +/** + * Returns a single, specific revision of a parent entity. + * + * @param state State tree + * @param kind Entity kind. + * @param name Entity name. + * @param recordKey The key of the entity record whose revisions you want to fetch. + * @param revisionKey The revision's key. + * @param query Optional query. If requesting specific + * fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [entity kind]". + * + * @return Record. + */ +export const getRevision = createSelector( + ( + state: State, + kind: string, + name: string, + recordKey: EntityRecordKey, + revisionKey: EntityRecordKey, + query?: GetRecordsHttpQuery + ): RevisionRecord | Record< PropertyKey, never > | undefined => { + const queriedState = + state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ + recordKey + ]; + + if ( ! queriedState ) { + return undefined; + } + + const context = query?.context ?? 'default'; + + if ( query === undefined ) { + // If expecting a complete item, validate that completeness. + if ( ! queriedState.itemIsComplete[ context ]?.[ revisionKey ] ) { + return undefined; + } + + return queriedState.items[ context ][ revisionKey ]; + } + + const item = queriedState.items[ context ]?.[ revisionKey ]; + if ( item && query._fields ) { + const filteredItem = {}; + const fields = getNormalizedCommaSeparable( query._fields ) ?? []; + + for ( let f = 0; f < fields.length; f++ ) { + const field = fields[ f ].split( '.' ); + let value = item; + field.forEach( ( fieldName ) => { + value = value?.[ fieldName ]; + } ); + setNestedValue( filteredItem, field, value ); + } + + return filteredItem; + } + + return item; + }, + ( state: State, kind, name, recordKey, revisionKey, query ) => { + const context = query?.context ?? 'default'; + return [ + state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ] + ?.items?.[ context ]?.[ revisionKey ], + state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ] + ?.itemIsComplete?.[ context ]?.[ revisionKey ], + ]; + } +); diff --git a/packages/core-data/src/test/entities.js b/packages/core-data/src/test/entities.js index 9afbbb8de055e1..c9a432c92c1442 100644 --- a/packages/core-data/src/test/entities.js +++ b/packages/core-data/src/test/entities.js @@ -80,6 +80,9 @@ describe( 'getKindEntities', () => { labels: { singular_name: 'post', }, + supports: { + revisions: true, + }, }, ]; const dispatch = jest.fn(); @@ -95,6 +98,12 @@ describe( 'getKindEntities', () => { expect( dispatch.mock.calls[ 0 ][ 0 ].entities[ 0 ].baseURL ).toBe( '/wp/v2/posts' ); + expect( + dispatch.mock.calls[ 0 ][ 0 ].entities[ 0 ].getRevisionsUrl( 1 ) + ).toBe( '/wp/v2/posts/1/revisions' ); + expect( + dispatch.mock.calls[ 0 ][ 0 ].entities[ 0 ].getRevisionsUrl( 1, 2 ) + ).toBe( '/wp/v2/posts/1/revisions/2' ); } ); } ); diff --git a/packages/core-data/src/test/reducer.js b/packages/core-data/src/test/reducer.js index 4142f65af4c7c4..d5d5bc5c8692fa 100644 --- a/packages/core-data/src/test/reducer.js +++ b/packages/core-data/src/test/reducer.js @@ -139,6 +139,241 @@ describe( 'entities', () => { .map( ( [ , cfg ] ) => cfg ) ).toEqual( [ { kind: 'postType', name: 'posts' } ] ); } ); + + describe( 'entity revisions', () => { + const stateWithConfig = entities( undefined, { + type: 'ADD_ENTITIES', + entities: [ + { + kind: 'root', + name: 'postType', + supports: { revisions: true }, + }, + ], + } ); + it( 'appends revisions state', () => { + expect( stateWithConfig.records.root.postType ).toHaveProperty( + 'revisions', + {} + ); + } ); + + it( 'returns with received revisions', () => { + const initialState = deepFreeze( { + config: stateWithConfig.config, + records: {}, + } ); + const state = entities( initialState, { + type: 'RECEIVE_ITEM_REVISIONS', + items: [ { id: 1, parent: 2 } ], + kind: 'root', + name: 'postType', + recordKey: 2, + } ); + expect( state.records.root.postType.revisions ).toEqual( { + 2: { + items: { + default: { + 1: { id: 1, parent: 2 }, + }, + }, + itemIsComplete: { + default: { + 1: true, + }, + }, + queries: {}, + }, + } ); + } ); + + it( 'returns with appended received revisions at the parent level', () => { + const initialState = deepFreeze( { + config: stateWithConfig.config, + records: { + root: { + postType: { + revisions: { + 2: { + items: { + default: { + 1: { id: 1, parent: 2 }, + }, + }, + itemIsComplete: { + default: { + 1: true, + }, + }, + queries: {}, + }, + }, + }, + }, + }, + } ); + const state = entities( initialState, { + type: 'RECEIVE_ITEM_REVISIONS', + items: [ { id: 3, parent: 4 } ], + kind: 'root', + name: 'postType', + recordKey: 4, + } ); + expect( state.records.root.postType.revisions ).toEqual( { + 2: { + items: { + default: { + 1: { id: 1, parent: 2 }, + }, + }, + itemIsComplete: { + default: { + 1: true, + }, + }, + queries: {}, + }, + 4: { + items: { + default: { + 3: { id: 3, parent: 4 }, + }, + }, + itemIsComplete: { + default: { + 3: true, + }, + }, + queries: {}, + }, + } ); + } ); + + it( 'returns with appended received revision items', () => { + const initialState = deepFreeze( { + config: stateWithConfig.config, + records: { + root: { + postType: { + revisions: { + 2: { + items: { + default: { + 1: { id: 1, parent: 2 }, + }, + }, + itemIsComplete: { + default: { + 1: true, + }, + }, + queries: {}, + }, + }, + }, + }, + }, + } ); + const state = entities( initialState, { + type: 'RECEIVE_ITEM_REVISIONS', + items: [ { id: 7, parent: 2 } ], + kind: 'root', + name: 'postType', + recordKey: 2, + } ); + expect( state.records.root.postType.revisions ).toEqual( { + 2: { + items: { + default: { + 1: { id: 1, parent: 2 }, + 7: { id: 7, parent: 2 }, + }, + }, + itemIsComplete: { + default: { + 1: true, + 7: true, + }, + }, + queries: {}, + }, + } ); + } ); + + it( 'returns with removed revision items', () => { + const initialState = deepFreeze( { + config: stateWithConfig.config, + records: { + root: { + postType: { + revisions: { + 2: { + items: { + default: { + 1: { id: 1, parent: 2 }, + }, + }, + itemIsComplete: { + default: { + 1: true, + }, + }, + queries: {}, + }, + 4: { + items: { + default: { + 3: { id: 3, parent: 4 }, + }, + }, + itemIsComplete: { + default: { + 3: true, + }, + }, + queries: {}, + }, + 6: { + items: { + default: { + 9: { id: 11, parent: 6 }, + }, + }, + itemIsComplete: { + default: { + 9: true, + }, + }, + queries: {}, + }, + }, + }, + }, + }, + } ); + const state = entities( initialState, { + type: 'REMOVE_ITEMS', + itemIds: [ 4, 6 ], + kind: 'root', + name: 'postType', + } ); + expect( state.records.root.postType.revisions ).toEqual( { + 2: { + items: { + default: { + 1: { id: 1, parent: 2 }, + }, + }, + itemIsComplete: { + default: { + 1: true, + }, + }, + queries: {}, + }, + } ); + } ); + } ); } ); describe( 'embedPreviews()', () => { diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js index b7c583098c4a5c..43c84a3e978917 100644 --- a/packages/core-data/src/test/selectors.js +++ b/packages/core-data/src/test/selectors.js @@ -22,6 +22,8 @@ import { getAutosave, getAutosaves, getCurrentUser, + getRevisions, + getRevision, } from '../selectors'; // getEntityRecord and __experimentalGetEntityRecordNoResolver selectors share the same tests. describe.each( [ @@ -896,3 +898,97 @@ describe( 'getCurrentUser', () => { expect( getCurrentUser( state ) ).toEqual( currentUser ); } ); } ); + +describe( 'getRevisions', () => { + it( 'should return revisions', () => { + const state = deepFreeze( { + entities: { + records: { + postType: { + post: { + revisions: { + 1: { + items: { + default: { + 10: { + id: 10, + content: 'chicken', + author: 'bob', + parent: 1, + }, + }, + }, + itemIsComplete: { + default: { + 10: true, + }, + }, + queries: { + default: { + '': { itemIds: [ 10 ] }, + }, + }, + }, + }, + }, + }, + }, + }, + } ); + + expect( getRevisions( state, 'postType', 'post', 1 ) ).toEqual( [ + { + id: 10, + content: 'chicken', + author: 'bob', + parent: 1, + }, + ] ); + } ); +} ); + +describe( 'getRevision', () => { + it( 'should return a specific revision', () => { + const state = deepFreeze( { + entities: { + records: { + postType: { + post: { + revisions: { + 1: { + items: { + default: { + 10: { + id: 10, + content: 'chicken', + author: 'bob', + parent: 1, + }, + }, + }, + itemIsComplete: { + default: { + 10: true, + }, + }, + queries: { + default: { + '': [ 10 ], + }, + }, + }, + }, + }, + }, + }, + }, + } ); + + expect( getRevision( state, 'postType', 'post', 1, 10 ) ).toEqual( { + id: 10, + content: 'chicken', + author: 'bob', + parent: 1, + } ); + } ); +} );