@wordpress/core-data 6.23.0 → 6.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +51 -2
  3. package/build/actions.js +38 -0
  4. package/build/actions.js.map +1 -1
  5. package/build/entities.js +7 -3
  6. package/build/entities.js.map +1 -1
  7. package/build/entity-types/global-styles-revision.js +6 -0
  8. package/build/entity-types/global-styles-revision.js.map +1 -0
  9. package/build/entity-types/index.js.map +1 -1
  10. package/build/entity-types/post-revision.js +6 -0
  11. package/build/entity-types/post-revision.js.map +1 -0
  12. package/build/footnotes/get-footnotes-order.js +2 -9
  13. package/build/footnotes/get-footnotes-order.js.map +1 -1
  14. package/build/footnotes/index.js +5 -10
  15. package/build/footnotes/index.js.map +1 -1
  16. package/build/hooks/use-entity-record.js +16 -5
  17. package/build/hooks/use-entity-record.js.map +1 -1
  18. package/build/queried-data/reducer.js +1 -1
  19. package/build/queried-data/reducer.js.map +1 -1
  20. package/build/reducer.js +26 -2
  21. package/build/reducer.js.map +1 -1
  22. package/build/resolvers.js +133 -3
  23. package/build/resolvers.js.map +1 -1
  24. package/build/selectors.js +79 -1
  25. package/build/selectors.js.map +1 -1
  26. package/build-module/actions.js +36 -0
  27. package/build-module/actions.js.map +1 -1
  28. package/build-module/entities.js +7 -3
  29. package/build-module/entities.js.map +1 -1
  30. package/build-module/entity-types/global-styles-revision.js +2 -0
  31. package/build-module/entity-types/global-styles-revision.js.map +1 -0
  32. package/build-module/entity-types/index.js.map +1 -1
  33. package/build-module/entity-types/post-revision.js +2 -0
  34. package/build-module/entity-types/post-revision.js.map +1 -0
  35. package/build-module/footnotes/get-footnotes-order.js +2 -9
  36. package/build-module/footnotes/get-footnotes-order.js.map +1 -1
  37. package/build-module/footnotes/index.js +6 -11
  38. package/build-module/footnotes/index.js.map +1 -1
  39. package/build-module/hooks/use-entity-record.js +16 -5
  40. package/build-module/hooks/use-entity-record.js.map +1 -1
  41. package/build-module/queried-data/reducer.js +1 -1
  42. package/build-module/queried-data/reducer.js.map +1 -1
  43. package/build-module/reducer.js +26 -2
  44. package/build-module/reducer.js.map +1 -1
  45. package/build-module/resolvers.js +130 -2
  46. package/build-module/resolvers.js.map +1 -1
  47. package/build-module/selectors.js +76 -1
  48. package/build-module/selectors.js.map +1 -1
  49. package/build-types/actions.d.ts +5 -0
  50. package/build-types/actions.d.ts.map +1 -1
  51. package/build-types/entities.d.ts +11 -1
  52. package/build-types/entities.d.ts.map +1 -1
  53. package/build-types/entity-types/global-styles-revision.d.ts +43 -0
  54. package/build-types/entity-types/global-styles-revision.d.ts.map +1 -0
  55. package/build-types/entity-types/index.d.ts +4 -2
  56. package/build-types/entity-types/index.d.ts.map +1 -1
  57. package/build-types/entity-types/post-revision.d.ts +76 -0
  58. package/build-types/entity-types/post-revision.d.ts.map +1 -0
  59. package/build-types/footnotes/get-footnotes-order.d.ts.map +1 -1
  60. package/build-types/footnotes/index.d.ts.map +1 -1
  61. package/build-types/hooks/use-entity-record.d.ts.map +1 -1
  62. package/build-types/index.d.ts +13 -4
  63. package/build-types/index.d.ts.map +1 -1
  64. package/build-types/reducer.d.ts.map +1 -1
  65. package/build-types/resolvers.d.ts +9 -0
  66. package/build-types/resolvers.d.ts.map +1 -1
  67. package/build-types/selectors.d.ts +39 -3
  68. package/build-types/selectors.d.ts.map +1 -1
  69. package/package.json +17 -17
  70. package/src/actions.js +45 -0
  71. package/src/entities.js +13 -1
  72. package/src/entity-types/global-styles-revision.ts +47 -0
  73. package/src/entity-types/index.ts +6 -0
  74. package/src/entity-types/post-revision.ts +93 -0
  75. package/src/footnotes/get-footnotes-order.js +5 -12
  76. package/src/footnotes/index.js +14 -8
  77. package/src/hooks/test/use-entity-record.js +41 -0
  78. package/src/hooks/use-entity-record.ts +30 -18
  79. package/src/queried-data/reducer.js +1 -1
  80. package/src/reducer.js +33 -2
  81. package/src/resolvers.js +182 -2
  82. package/src/selectors.ts +119 -1
  83. package/src/test/entities.js +9 -0
  84. package/src/test/reducer.js +235 -0
  85. package/src/test/selectors.js +96 -0
  86. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
- import { create, toHTMLString } from '@wordpress/rich-text';
4
+ import { RichTextData, create, toHTMLString } from '@wordpress/rich-text';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -53,15 +53,18 @@ export function updateFootnotesFromMeta( blocks, meta ) {
53
53
  continue;
54
54
  }
55
55
 
56
- if ( typeof value !== 'string' ) {
56
+ // To do, remove support for string values?
57
+ if (
58
+ typeof value !== 'string' &&
59
+ ! ( value instanceof RichTextData )
60
+ ) {
57
61
  continue;
58
62
  }
59
63
 
60
- if ( value.indexOf( 'data-fn' ) === -1 ) {
61
- continue;
62
- }
63
-
64
- const richTextValue = create( { html: value } );
64
+ const richTextValue =
65
+ typeof value === 'string'
66
+ ? RichTextData.fromHTMLString( value )
67
+ : value;
65
68
 
66
69
  richTextValue.replacements.forEach( ( replacement ) => {
67
70
  if ( replacement.type === 'core/footnote' ) {
@@ -78,7 +81,10 @@ export function updateFootnotesFromMeta( blocks, meta ) {
78
81
  }
79
82
  } );
80
83
 
81
- attributes[ key ] = toHTMLString( { value: richTextValue } );
84
+ attributes[ key ] =
85
+ typeof value === 'string'
86
+ ? richTextValue.toHTMLString()
87
+ : richTextValue;
82
88
  }
83
89
 
84
90
  return attributes;
@@ -23,6 +23,7 @@ describe( 'useEntityRecord', () => {
23
23
  beforeEach( () => {
24
24
  registry = createRegistry();
25
25
  registry.register( coreDataStore );
26
+ triggerFetch.mockReset();
26
27
  } );
27
28
 
28
29
  const TEST_RECORD = { id: 1, hello: 'world' };
@@ -113,4 +114,44 @@ describe( 'useEntityRecord', () => {
113
114
  expect( widget.editedRecord ).toEqual( { hello: 'foo', id: 1 } );
114
115
  expect( widget.edits ).toEqual( { hello: 'foo' } );
115
116
  } );
117
+
118
+ it( 'does not resolve entity record when disabled via options', async () => {
119
+ triggerFetch.mockImplementation( () => TEST_RECORD );
120
+
121
+ let data;
122
+ const TestComponent = ( { enabled } ) => {
123
+ data = useEntityRecord( 'root', 'widget', 1, { enabled } );
124
+ return <div />;
125
+ };
126
+ const UI = ( { enabled } ) => (
127
+ <RegistryProvider value={ registry }>
128
+ <TestComponent enabled={ enabled } />
129
+ </RegistryProvider>
130
+ );
131
+
132
+ const { rerender } = render( <UI enabled={ true } /> );
133
+
134
+ // A minimum delay for a fetch request. The same delay is used again as a control.
135
+ await act(
136
+ () => new Promise( ( resolve ) => setTimeout( resolve, 0 ) )
137
+ );
138
+ expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
139
+
140
+ rerender( <UI enabled={ false } /> );
141
+
142
+ expect( data ).toEqual( {
143
+ edit: expect.any( Function ),
144
+ editedRecord: {},
145
+ hasEdits: false,
146
+ edits: {},
147
+ record: null,
148
+ save: expect.any( Function ),
149
+ } );
150
+
151
+ // The same delay.
152
+ await act(
153
+ () => new Promise( ( resolve ) => setTimeout( resolve, 0 ) )
154
+ );
155
+ expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
156
+ } );
116
157
  } );
@@ -56,6 +56,8 @@ export interface Options {
56
56
  enabled: boolean;
57
57
  }
58
58
 
59
+ const EMPTY_OBJECT = {};
60
+
59
61
  /**
60
62
  * Resolves the specified entity record.
61
63
  *
@@ -167,24 +169,34 @@ export default function useEntityRecord< RecordType >(
167
169
  );
168
170
 
169
171
  const { editedRecord, hasEdits, edits } = useSelect(
170
- ( select ) => ( {
171
- editedRecord: select( coreStore ).getEditedEntityRecord(
172
- kind,
173
- name,
174
- recordId
175
- ),
176
- hasEdits: select( coreStore ).hasEditsForEntityRecord(
177
- kind,
178
- name,
179
- recordId
180
- ),
181
- edits: select( coreStore ).getEntityRecordNonTransientEdits(
182
- kind,
183
- name,
184
- recordId
185
- ),
186
- } ),
187
- [ kind, name, recordId ]
172
+ ( select ) => {
173
+ if ( ! options.enabled ) {
174
+ return {
175
+ editedRecord: EMPTY_OBJECT,
176
+ hasEdits: false,
177
+ edits: EMPTY_OBJECT,
178
+ };
179
+ }
180
+
181
+ return {
182
+ editedRecord: select( coreStore ).getEditedEntityRecord(
183
+ kind,
184
+ name,
185
+ recordId
186
+ ),
187
+ hasEdits: select( coreStore ).hasEditsForEntityRecord(
188
+ kind,
189
+ name,
190
+ recordId
191
+ ),
192
+ edits: select( coreStore ).getEntityRecordNonTransientEdits(
193
+ kind,
194
+ name,
195
+ recordId
196
+ ),
197
+ };
198
+ },
199
+ [ kind, name, recordId, options.enabled ]
188
200
  );
189
201
 
190
202
  const { data: record, ...querySelectRest } = useQuerySelect(
@@ -57,7 +57,7 @@ export function getMergedItemIds( itemIds, nextItemIds, page, perPage ) {
57
57
  for ( let i = 0; i < size; i++ ) {
58
58
  // Preserve existing item ID except for subset of range of next items.
59
59
  // We need to check against the possible maximum upper boundary because
60
- // a page could recieve less items than what was previously stored.
60
+ // a page could receive fewer than what was previously stored.
61
61
  const isInNextItemsRange =
62
62
  i >= nextItemIdsStartIndex && i < nextItemIdsStartIndex + perPage;
63
63
  mergedItemIds[ i ] = isInNextItemsRange
package/src/reducer.js CHANGED
@@ -238,14 +238,13 @@ function entity( entityConfig ) {
238
238
  // Inject the entity config into the action.
239
239
  replaceAction( ( action ) => {
240
240
  return {
241
- ...action,
242
241
  key: entityConfig.key || DEFAULT_ENTITY_KEY,
242
+ ...action,
243
243
  };
244
244
  } ),
245
245
  ] )(
246
246
  combineReducers( {
247
247
  queriedData: queriedDataReducer,
248
-
249
248
  edits: ( state = {}, action ) => {
250
249
  switch ( action.type ) {
251
250
  case 'RECEIVE_ITEMS':
@@ -355,6 +354,38 @@ function entity( entityConfig ) {
355
354
 
356
355
  return state;
357
356
  },
357
+
358
+ revisions: ( state = {}, action ) => {
359
+ // Use the same queriedDataReducer shape for revisions.
360
+ if ( action.type === 'RECEIVE_ITEM_REVISIONS' ) {
361
+ const recordKey = action.recordKey;
362
+ delete action.recordKey;
363
+ const newState = queriedDataReducer( state[ recordKey ], {
364
+ ...action,
365
+ type: 'RECEIVE_ITEMS',
366
+ } );
367
+ return {
368
+ ...state,
369
+ [ recordKey ]: newState,
370
+ };
371
+ }
372
+
373
+ if ( action.type === 'REMOVE_ITEMS' ) {
374
+ return Object.fromEntries(
375
+ Object.entries( state ).filter(
376
+ ( [ id ] ) =>
377
+ ! action.itemIds.some( ( itemId ) => {
378
+ if ( Number.isInteger( itemId ) ) {
379
+ return itemId === +id;
380
+ }
381
+ return itemId === id;
382
+ } )
383
+ )
384
+ );
385
+ }
386
+
387
+ return state;
388
+ },
358
389
  } )
359
390
  );
360
391
  }
package/src/resolvers.js CHANGED
@@ -244,7 +244,7 @@ export const getEntityRecords =
244
244
 
245
245
  // If we request fields but the result doesn't contain the fields,
246
246
  // explicitly set these fields as "undefined"
247
- // that way we consider the query "fullfilled".
247
+ // that way we consider the query "fulfilled".
248
248
  if ( query._fields ) {
249
249
  records = records.map( ( record ) => {
250
250
  query._fields.split( ',' ).forEach( ( field ) => {
@@ -322,7 +322,7 @@ export const getCurrentTheme =
322
322
  export const getThemeSupports = forwardResolver( 'getCurrentTheme' );
323
323
 
324
324
  /**
325
- * Requests a preview from the from the Embed API.
325
+ * Requests a preview from the Embed API.
326
326
  *
327
327
  * @param {string} url URL to get the preview for.
328
328
  */
@@ -718,3 +718,183 @@ export const getDefaultTemplateId =
718
718
  dispatch.receiveDefaultTemplateId( query, template.id );
719
719
  }
720
720
  };
721
+
722
+ /**
723
+ * Requests an entity's revisions from the REST API.
724
+ *
725
+ * @param {string} kind Entity kind.
726
+ * @param {string} name Entity name.
727
+ * @param {number|string} recordKey The key of the entity record whose revisions you want to fetch.
728
+ * @param {Object|undefined} query Optional object of query parameters to
729
+ * include with request. If requesting specific
730
+ * fields, fields must always include the ID.
731
+ */
732
+ export const getRevisions =
733
+ ( kind, name, recordKey, query = {} ) =>
734
+ async ( { dispatch } ) => {
735
+ const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
736
+ const entityConfig = configs.find(
737
+ ( config ) => config.name === name && config.kind === kind
738
+ );
739
+
740
+ if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
741
+ return;
742
+ }
743
+
744
+ if ( query._fields ) {
745
+ // If requesting specific fields, items and query association to said
746
+ // records are stored by ID reference. Thus, fields must always include
747
+ // the ID.
748
+ query = {
749
+ ...query,
750
+ _fields: [
751
+ ...new Set( [
752
+ ...( getNormalizedCommaSeparable( query._fields ) ||
753
+ [] ),
754
+ entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
755
+ ] ),
756
+ ].join(),
757
+ };
758
+ }
759
+
760
+ const path = addQueryArgs(
761
+ entityConfig.getRevisionsUrl( recordKey ),
762
+ query
763
+ );
764
+
765
+ let records, response;
766
+ const meta = {};
767
+ const isPaginated =
768
+ entityConfig.supportsPagination && query.per_page !== -1;
769
+ try {
770
+ response = await apiFetch( { path, parse: ! isPaginated } );
771
+ } catch ( error ) {
772
+ // Do nothing if our request comes back with an API error.
773
+ return;
774
+ }
775
+
776
+ if ( response ) {
777
+ if ( isPaginated ) {
778
+ records = Object.values( await response.json() );
779
+ meta.totalItems = parseInt(
780
+ response.headers.get( 'X-WP-Total' )
781
+ );
782
+ } else {
783
+ records = Object.values( response );
784
+ }
785
+
786
+ // If we request fields but the result doesn't contain the fields,
787
+ // explicitly set these fields as "undefined"
788
+ // that way we consider the query "fulfilled".
789
+ if ( query._fields ) {
790
+ records = records.map( ( record ) => {
791
+ query._fields.split( ',' ).forEach( ( field ) => {
792
+ if ( ! record.hasOwnProperty( field ) ) {
793
+ record[ field ] = undefined;
794
+ }
795
+ } );
796
+
797
+ return record;
798
+ } );
799
+ }
800
+
801
+ dispatch.receiveRevisions(
802
+ kind,
803
+ name,
804
+ recordKey,
805
+ records,
806
+ query,
807
+ false,
808
+ meta
809
+ );
810
+
811
+ // When requesting all fields, the list of results can be used to
812
+ // resolve the `getRevision` selector in addition to `getRevisions`.
813
+ if ( ! query?._fields && ! query.context ) {
814
+ const key = entityConfig.key || DEFAULT_ENTITY_KEY;
815
+ const resolutionsArgs = records
816
+ .filter( ( record ) => record[ key ] )
817
+ .map( ( record ) => [
818
+ kind,
819
+ name,
820
+ recordKey,
821
+ record[ key ],
822
+ ] );
823
+
824
+ dispatch( {
825
+ type: 'START_RESOLUTIONS',
826
+ selectorName: 'getRevision',
827
+ args: resolutionsArgs,
828
+ } );
829
+ dispatch( {
830
+ type: 'FINISH_RESOLUTIONS',
831
+ selectorName: 'getRevision',
832
+ args: resolutionsArgs,
833
+ } );
834
+ }
835
+ }
836
+ };
837
+
838
+ // Invalidate cache when a new revision is created.
839
+ getRevisions.shouldInvalidate = ( action, kind, name, recordKey ) =>
840
+ action.type === 'SAVE_ENTITY_RECORD_FINISH' &&
841
+ name === action.name &&
842
+ kind === action.kind &&
843
+ ! action.error &&
844
+ recordKey === action.recordId;
845
+
846
+ /**
847
+ * Requests a specific Entity revision from the REST API.
848
+ *
849
+ * @param {string} kind Entity kind.
850
+ * @param {string} name Entity name.
851
+ * @param {number|string} recordKey The key of the entity record whose revisions you want to fetch.
852
+ * @param {number|string} revisionKey The revision's key.
853
+ * @param {Object|undefined} query Optional object of query parameters to
854
+ * include with request. If requesting specific
855
+ * fields, fields must always include the ID.
856
+ */
857
+ export const getRevision =
858
+ ( kind, name, recordKey, revisionKey, query ) =>
859
+ async ( { dispatch } ) => {
860
+ const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
861
+ const entityConfig = configs.find(
862
+ ( config ) => config.name === name && config.kind === kind
863
+ );
864
+
865
+ if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
866
+ return;
867
+ }
868
+
869
+ if ( query !== undefined && query._fields ) {
870
+ // If requesting specific fields, items and query association to said
871
+ // records are stored by ID reference. Thus, fields must always include
872
+ // the ID.
873
+ query = {
874
+ ...query,
875
+ _fields: [
876
+ ...new Set( [
877
+ ...( getNormalizedCommaSeparable( query._fields ) ||
878
+ [] ),
879
+ entityConfig.revisionKey || DEFAULT_ENTITY_KEY,
880
+ ] ),
881
+ ].join(),
882
+ };
883
+ }
884
+ const path = addQueryArgs(
885
+ entityConfig.getRevisionsUrl( recordKey, revisionKey ),
886
+ query
887
+ );
888
+
889
+ let record;
890
+ try {
891
+ record = await apiFetch( { path } );
892
+ } catch ( error ) {
893
+ // Do nothing if our request comes back with an API error.
894
+ return;
895
+ }
896
+
897
+ if ( record ) {
898
+ dispatch.receiveRevisions( kind, name, recordKey, record, query );
899
+ }
900
+ };
package/src/selectors.ts CHANGED
@@ -62,6 +62,16 @@ interface QueriedData {
62
62
  queries: Record< ET.Context, Record< string, Array< number > > >;
63
63
  }
64
64
 
65
+ type RevisionRecord =
66
+ | Record< ET.Context, Record< number, ET.PostRevision > >
67
+ | Record< ET.Context, Record< number, ET.GlobalStylesRevision > >;
68
+
69
+ interface RevisionsQueriedData {
70
+ items: RevisionRecord;
71
+ itemIsComplete: Record< ET.Context, Record< number, boolean > >;
72
+ queries: Record< ET.Context, Record< string, Array< number > > >;
73
+ }
74
+
65
75
  interface EntityState< EntityRecord extends ET.EntityRecord > {
66
76
  edits: Record< string, Partial< EntityRecord > >;
67
77
  saving: Record<
@@ -70,6 +80,7 @@ interface EntityState< EntityRecord extends ET.EntityRecord > {
70
80
  >;
71
81
  deleting: Record< string, Partial< { pending: boolean; error: Error } > >;
72
82
  queriedData: QueriedData;
83
+ revisions?: RevisionsQueriedData;
73
84
  }
74
85
 
75
86
  interface EntityConfig {
@@ -1342,13 +1353,20 @@ export function getUserPatternCategories(
1342
1353
  /**
1343
1354
  * Returns the revisions of the current global styles theme.
1344
1355
  *
1345
- * @param state Data state.
1356
+ * @deprecated since WordPress 6.5.0. Callers should use `select( 'core' ).getRevisions( 'root', 'globalStyles', ${ recordKey } )` instead, where `recordKey` is the id of the global styles parent post.
1357
+ *
1358
+ * @param state Data state.
1346
1359
  *
1347
1360
  * @return The current global styles.
1348
1361
  */
1349
1362
  export function getCurrentThemeGlobalStylesRevisions(
1350
1363
  state: State
1351
1364
  ): Array< object > | null {
1365
+ deprecated( "select( 'core' ).getCurrentThemeGlobalStylesRevisions()", {
1366
+ since: '6.5.0',
1367
+ alternative:
1368
+ "select( 'core' ).getRevisions( 'root', 'globalStyles', ${ recordKey } )",
1369
+ } );
1352
1370
  const currentGlobalStylesId =
1353
1371
  __experimentalGetCurrentGlobalStylesId( state );
1354
1372
 
@@ -1373,3 +1391,103 @@ export function getDefaultTemplateId(
1373
1391
  ): string {
1374
1392
  return state.defaultTemplates[ JSON.stringify( query ) ];
1375
1393
  }
1394
+
1395
+ /**
1396
+ * Returns an entity's revisions.
1397
+ *
1398
+ * @param state State tree
1399
+ * @param kind Entity kind.
1400
+ * @param name Entity name.
1401
+ * @param recordKey The key of the entity record whose revisions you want to fetch.
1402
+ * @param query Optional query. If requesting specific
1403
+ * 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]".
1404
+ *
1405
+ * @return Record.
1406
+ */
1407
+ export const getRevisions = (
1408
+ state: State,
1409
+ kind: string,
1410
+ name: string,
1411
+ recordKey: EntityRecordKey,
1412
+ query?: GetRecordsHttpQuery
1413
+ ): RevisionRecord[] | null => {
1414
+ const queriedStateRevisions =
1415
+ state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ];
1416
+ if ( ! queriedStateRevisions ) {
1417
+ return null;
1418
+ }
1419
+
1420
+ return getQueriedItems( queriedStateRevisions, query );
1421
+ };
1422
+
1423
+ /**
1424
+ * Returns a single, specific revision of a parent entity.
1425
+ *
1426
+ * @param state State tree
1427
+ * @param kind Entity kind.
1428
+ * @param name Entity name.
1429
+ * @param recordKey The key of the entity record whose revisions you want to fetch.
1430
+ * @param revisionKey The revision's key.
1431
+ * @param query Optional query. If requesting specific
1432
+ * 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]".
1433
+ *
1434
+ * @return Record.
1435
+ */
1436
+ export const getRevision = createSelector(
1437
+ (
1438
+ state: State,
1439
+ kind: string,
1440
+ name: string,
1441
+ recordKey: EntityRecordKey,
1442
+ revisionKey: EntityRecordKey,
1443
+ query?: GetRecordsHttpQuery
1444
+ ): RevisionRecord | Record< PropertyKey, never > | undefined => {
1445
+ const queriedState =
1446
+ state.entities.records?.[ kind ]?.[ name ]?.revisions?.[
1447
+ recordKey
1448
+ ];
1449
+
1450
+ if ( ! queriedState ) {
1451
+ return undefined;
1452
+ }
1453
+
1454
+ const context = query?.context ?? 'default';
1455
+
1456
+ if ( query === undefined ) {
1457
+ // If expecting a complete item, validate that completeness.
1458
+ if ( ! queriedState.itemIsComplete[ context ]?.[ revisionKey ] ) {
1459
+ return undefined;
1460
+ }
1461
+
1462
+ return queriedState.items[ context ][ revisionKey ];
1463
+ }
1464
+
1465
+ const item = queriedState.items[ context ]?.[ revisionKey ];
1466
+ if ( item && query._fields ) {
1467
+ const filteredItem = {};
1468
+ const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
1469
+
1470
+ for ( let f = 0; f < fields.length; f++ ) {
1471
+ const field = fields[ f ].split( '.' );
1472
+ let value = item;
1473
+ field.forEach( ( fieldName ) => {
1474
+ value = value?.[ fieldName ];
1475
+ } );
1476
+ setNestedValue( filteredItem, field, value );
1477
+ }
1478
+
1479
+ return filteredItem;
1480
+ }
1481
+
1482
+ return item;
1483
+ },
1484
+ ( state: State, kind, name, recordKey, revisionKey, query ) => {
1485
+ const context = query?.context ?? 'default';
1486
+ return [
1487
+ state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ]
1488
+ ?.items?.[ context ]?.[ revisionKey ],
1489
+ state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ]
1490
+ ?.itemIsComplete?.[ context ]?.[ revisionKey ],
1491
+ ];
1492
+ }
1493
+ );
@@ -80,6 +80,9 @@ describe( 'getKindEntities', () => {
80
80
  labels: {
81
81
  singular_name: 'post',
82
82
  },
83
+ supports: {
84
+ revisions: true,
85
+ },
83
86
  },
84
87
  ];
85
88
  const dispatch = jest.fn();
@@ -95,6 +98,12 @@ describe( 'getKindEntities', () => {
95
98
  expect( dispatch.mock.calls[ 0 ][ 0 ].entities[ 0 ].baseURL ).toBe(
96
99
  '/wp/v2/posts'
97
100
  );
101
+ expect(
102
+ dispatch.mock.calls[ 0 ][ 0 ].entities[ 0 ].getRevisionsUrl( 1 )
103
+ ).toBe( '/wp/v2/posts/1/revisions' );
104
+ expect(
105
+ dispatch.mock.calls[ 0 ][ 0 ].entities[ 0 ].getRevisionsUrl( 1, 2 )
106
+ ).toBe( '/wp/v2/posts/1/revisions/2' );
98
107
  } );
99
108
  } );
100
109