@wordpress/core-data 4.0.2 → 4.0.6

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 (65) hide show
  1. package/README.md +3 -5
  2. package/build/actions.js +79 -34
  3. package/build/actions.js.map +1 -1
  4. package/build/entities.js +51 -9
  5. package/build/entities.js.map +1 -1
  6. package/build/fetch/__experimental-fetch-url-data.js +1 -1
  7. package/build/fetch/__experimental-fetch-url-data.js.map +1 -1
  8. package/build/index.js +0 -7
  9. package/build/index.js.map +1 -1
  10. package/build/queried-data/get-query-parts.js +2 -0
  11. package/build/queried-data/get-query-parts.js.map +1 -1
  12. package/build/reducer.js +16 -18
  13. package/build/reducer.js.map +1 -1
  14. package/build/resolvers.js +48 -31
  15. package/build/resolvers.js.map +1 -1
  16. package/build/selectors.js +70 -10
  17. package/build/selectors.js.map +1 -1
  18. package/build/utils/forward-resolver.js +23 -0
  19. package/build/utils/forward-resolver.js.map +1 -0
  20. package/build/utils/index.js +3 -3
  21. package/build/utils/index.js.map +1 -1
  22. package/build-module/actions.js +74 -33
  23. package/build-module/actions.js.map +1 -1
  24. package/build-module/entities.js +51 -9
  25. package/build-module/entities.js.map +1 -1
  26. package/build-module/fetch/__experimental-fetch-url-data.js +1 -1
  27. package/build-module/fetch/__experimental-fetch-url-data.js.map +1 -1
  28. package/build-module/index.js +0 -5
  29. package/build-module/index.js.map +1 -1
  30. package/build-module/queried-data/get-query-parts.js +2 -0
  31. package/build-module/queried-data/get-query-parts.js.map +1 -1
  32. package/build-module/reducer.js +14 -16
  33. package/build-module/reducer.js.map +1 -1
  34. package/build-module/resolvers.js +51 -37
  35. package/build-module/resolvers.js.map +1 -1
  36. package/build-module/selectors.js +64 -8
  37. package/build-module/selectors.js.map +1 -1
  38. package/build-module/utils/forward-resolver.js +15 -0
  39. package/build-module/utils/forward-resolver.js.map +1 -0
  40. package/build-module/utils/index.js +1 -1
  41. package/build-module/utils/index.js.map +1 -1
  42. package/package.json +10 -11
  43. package/src/actions.js +83 -52
  44. package/src/entities.js +36 -6
  45. package/src/fetch/__experimental-fetch-url-data.js +1 -1
  46. package/src/index.js +0 -3
  47. package/src/queried-data/get-query-parts.js +2 -0
  48. package/src/reducer.js +14 -17
  49. package/src/resolvers.js +67 -42
  50. package/src/selectors.js +143 -35
  51. package/src/test/actions.js +116 -37
  52. package/src/test/selectors.js +56 -1
  53. package/src/utils/forward-resolver.js +14 -0
  54. package/src/utils/index.js +1 -1
  55. package/build/controls.js +0 -44
  56. package/build/controls.js.map +0 -1
  57. package/build/utils/if-not-resolved.js +0 -31
  58. package/build/utils/if-not-resolved.js.map +0 -1
  59. package/build-module/controls.js +0 -31
  60. package/build-module/controls.js.map +0 -1
  61. package/build-module/utils/if-not-resolved.js +0 -23
  62. package/build-module/utils/if-not-resolved.js.map +0 -1
  63. package/src/controls.js +0 -31
  64. package/src/utils/if-not-resolved.js +0 -22
  65. package/src/utils/test/if-not-resolved.js +0 -76
package/src/resolvers.js CHANGED
@@ -7,18 +7,14 @@ import { find, includes, get, hasIn, compact, uniq } from 'lodash';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { addQueryArgs } from '@wordpress/url';
10
- import triggerFetch from '@wordpress/api-fetch';
10
+ import apiFetch from '@wordpress/api-fetch';
11
11
 
12
12
  /**
13
13
  * Internal dependencies
14
14
  */
15
15
  import { STORE_NAME } from './name';
16
-
17
- /**
18
- * Internal dependencies
19
- */
20
16
  import { getKindEntities, DEFAULT_ENTITY_KEY } from './entities';
21
- import { ifNotResolved, getNormalizedCommaSeparable } from './utils';
17
+ import { forwardResolver, getNormalizedCommaSeparable } from './utils';
22
18
 
23
19
  /**
24
20
  * Requests authors from the REST API.
@@ -31,7 +27,7 @@ export const getAuthors = ( query ) => async ( { dispatch } ) => {
31
27
  '/wp/v2/users/?who=authors&per_page=100',
32
28
  query
33
29
  );
34
- const users = await triggerFetch( { path } );
30
+ const users = await apiFetch( { path } );
35
31
  dispatch.receiveUserQuery( path, users );
36
32
  };
37
33
 
@@ -39,7 +35,7 @@ export const getAuthors = ( query ) => async ( { dispatch } ) => {
39
35
  * Requests the current user from the REST API.
40
36
  */
41
37
  export const getCurrentUser = () => async ( { dispatch } ) => {
42
- const currentUser = await triggerFetch( { path: '/wp/v2/users/me' } );
38
+ const currentUser = await apiFetch( { path: '/wp/v2/users/me' } );
43
39
  dispatch.receiveCurrentUser( currentUser );
44
40
  };
45
41
 
@@ -58,7 +54,7 @@ export const getEntityRecord = ( kind, name, key = '', query ) => async ( {
58
54
  } ) => {
59
55
  const entities = await dispatch( getKindEntities( kind ) );
60
56
  const entity = find( entities, { kind, name } );
61
- if ( ! entity ) {
57
+ if ( ! entity || entity?.__experimentalNoFetch ) {
62
58
  return;
63
59
  }
64
60
 
@@ -89,7 +85,7 @@ export const getEntityRecord = ( kind, name, key = '', query ) => async ( {
89
85
  // for how the request is made to the REST API.
90
86
 
91
87
  // eslint-disable-next-line @wordpress/no-unused-vars-before-return
92
- const path = addQueryArgs( entity.baseURL + '/' + key, {
88
+ const path = addQueryArgs( entity.baseURL + ( key ? '/' + key : '' ), {
93
89
  ...entity.baseURLParams,
94
90
  ...query,
95
91
  } );
@@ -106,11 +102,12 @@ export const getEntityRecord = ( kind, name, key = '', query ) => async ( {
106
102
  }
107
103
  }
108
104
 
109
- const record = await triggerFetch( { path } );
105
+ const record = await apiFetch( { path } );
110
106
  dispatch.receiveEntityRecords( kind, name, record, query );
111
107
  } catch ( error ) {
112
108
  // We need a way to handle and access REST API errors in state
113
109
  // Until then, catching the error ensures the resolver is marked as resolved.
110
+ // See similar implementation in `getEntityRecords()`.
114
111
  } finally {
115
112
  dispatch.__unstableReleaseStoreLock( lock );
116
113
  }
@@ -119,26 +116,12 @@ export const getEntityRecord = ( kind, name, key = '', query ) => async ( {
119
116
  /**
120
117
  * Requests an entity's record from the REST API.
121
118
  */
122
- export const getRawEntityRecord = ifNotResolved(
123
- getEntityRecord,
124
- 'getEntityRecord'
125
- );
119
+ export const getRawEntityRecord = forwardResolver( 'getEntityRecord' );
126
120
 
127
121
  /**
128
122
  * Requests an entity's record from the REST API.
129
123
  */
130
- export const getEditedEntityRecord = ifNotResolved(
131
- getRawEntityRecord,
132
- 'getRawEntityRecord'
133
- );
134
-
135
- /**
136
- * Requests the entity's records from the REST API.
137
- *
138
- * @param {string} kind Entity kind.
139
- * @param {string} name Entity name.
140
- * @param {Object?} query Query Object.
141
- */
124
+ export const getEditedEntityRecord = forwardResolver( 'getEntityRecord' );
142
125
 
143
126
  /**
144
127
  * Requests the entity's records from the REST API.
@@ -152,7 +135,7 @@ export const getEntityRecords = ( kind, name, query = {} ) => async ( {
152
135
  } ) => {
153
136
  const entities = await dispatch( getKindEntities( kind ) );
154
137
  const entity = find( entities, { kind, name } );
155
- if ( ! entity ) {
138
+ if ( ! entity || entity?.__experimentalNoFetch ) {
156
139
  return;
157
140
  }
158
141
 
@@ -181,7 +164,7 @@ export const getEntityRecords = ( kind, name, query = {} ) => async ( {
181
164
  ...query,
182
165
  } );
183
166
 
184
- let records = Object.values( await triggerFetch( { path } ) );
167
+ let records = Object.values( await apiFetch( { path } ) );
185
168
  // If we request fields but the result doesn't contain the fields,
186
169
  // explicitely set these fields as "undefined"
187
170
  // that way we consider the query "fullfilled".
@@ -219,6 +202,10 @@ export const getEntityRecords = ( kind, name, query = {} ) => async ( {
219
202
  args: resolutionsArgs,
220
203
  } );
221
204
  }
205
+ } catch ( error ) {
206
+ // We need a way to handle and access REST API errors in state
207
+ // Until then, catching the error ensures the resolver is marked as resolved.
208
+ // See similar implementation in `getEntityRecord()`.
222
209
  } finally {
223
210
  dispatch.__unstableReleaseStoreLock( lock );
224
211
  }
@@ -236,22 +223,20 @@ getEntityRecords.shouldInvalidate = ( action, kind, name ) => {
236
223
  /**
237
224
  * Requests the current theme.
238
225
  */
239
- export const getCurrentTheme = () => async ( { dispatch } ) => {
240
- const activeThemes = await triggerFetch( {
241
- path: '/wp/v2/themes?status=active',
242
- } );
226
+ export const getCurrentTheme = () => async ( { dispatch, resolveSelect } ) => {
227
+ const activeThemes = await resolveSelect.getEntityRecords(
228
+ 'root',
229
+ 'theme',
230
+ { status: 'active' }
231
+ );
232
+
243
233
  dispatch.receiveCurrentTheme( activeThemes[ 0 ] );
244
234
  };
245
235
 
246
236
  /**
247
237
  * Requests theme supports data from the index.
248
238
  */
249
- export const getThemeSupports = () => async ( { dispatch } ) => {
250
- const activeThemes = await triggerFetch( {
251
- path: '/wp/v2/themes?status=active',
252
- } );
253
- dispatch.receiveThemeSupports( activeThemes[ 0 ].theme_supports );
254
- };
239
+ export const getThemeSupports = forwardResolver( 'getCurrentTheme' );
255
240
 
256
241
  /**
257
242
  * Requests a preview from the from the Embed API.
@@ -260,7 +245,7 @@ export const getThemeSupports = () => async ( { dispatch } ) => {
260
245
  */
261
246
  export const getEmbedPreview = ( url ) => async ( { dispatch } ) => {
262
247
  try {
263
- const embedProxyResponse = await triggerFetch( {
248
+ const embedProxyResponse = await apiFetch( {
264
249
  path: addQueryArgs( '/oembed/1.0/proxy', { url } ),
265
250
  } );
266
251
  dispatch.receiveEmbedPreview( url, embedProxyResponse );
@@ -296,7 +281,7 @@ export const canUser = ( action, resource, id ) => async ( { dispatch } ) => {
296
281
 
297
282
  let response;
298
283
  try {
299
- response = await triggerFetch( {
284
+ response = await apiFetch( {
300
285
  path,
301
286
  // Ideally this would always be an OPTIONS request, but unfortunately there's
302
287
  // a bug in the REST API which causes the Allow header to not be sent on
@@ -359,7 +344,7 @@ export const getAutosaves = ( postType, postId ) => async ( {
359
344
  resolveSelect,
360
345
  } ) => {
361
346
  const { rest_base: restBase } = await resolveSelect.getPostType( postType );
362
- const autosaves = await triggerFetch( {
347
+ const autosaves = await apiFetch( {
363
348
  path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit`,
364
349
  } );
365
350
 
@@ -430,3 +415,43 @@ __experimentalGetTemplateForLink.shouldInvalidate = ( action ) => {
430
415
  action.name === 'wp_template'
431
416
  );
432
417
  };
418
+
419
+ export const __experimentalGetCurrentGlobalStylesId = () => async ( {
420
+ dispatch,
421
+ resolveSelect,
422
+ } ) => {
423
+ const activeThemes = await resolveSelect.getEntityRecords(
424
+ 'root',
425
+ 'theme',
426
+ { status: 'active' }
427
+ );
428
+ const globalStylesURL = get( activeThemes, [
429
+ 0,
430
+ '_links',
431
+ 'wp:user-global-styles',
432
+ 0,
433
+ 'href',
434
+ ] );
435
+ if ( globalStylesURL ) {
436
+ const globalStylesObject = await apiFetch( {
437
+ url: globalStylesURL,
438
+ } );
439
+ dispatch.__experimentalReceiveCurrentGlobalStylesId(
440
+ globalStylesObject.id
441
+ );
442
+ }
443
+ };
444
+
445
+ export const __experimentalGetCurrentThemeBaseGlobalStyles = () => async ( {
446
+ resolveSelect,
447
+ dispatch,
448
+ } ) => {
449
+ const currentTheme = await resolveSelect.getCurrentTheme();
450
+ const themeGlobalStyles = await apiFetch( {
451
+ path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }`,
452
+ } );
453
+ await dispatch.__experimentalReceiveThemeBaseGlobalStyles(
454
+ currentTheme.stylesheet,
455
+ themeGlobalStyles
456
+ );
457
+ };
package/src/selectors.js CHANGED
@@ -19,6 +19,15 @@ import { getQueriedItems } from './queried-data';
19
19
  import { DEFAULT_ENTITY_KEY } from './entities';
20
20
  import { getNormalizedCommaSeparable, isRawAttribute } from './utils';
21
21
 
22
+ /**
23
+ * Shared reference to an empty object for cases where it is important to avoid
24
+ * returning a new object reference on every invocation, as in a connected or
25
+ * other pure component which performs `shouldComponentUpdate` check on props.
26
+ * This should be used as a last resort, since the normalized data should be
27
+ * maintained by the reducer result in state.
28
+ */
29
+ const EMPTY_OBJECT = {};
30
+
22
31
  /**
23
32
  * Shared reference to an empty array for cases where it is important to avoid
24
33
  * returning a new array reference on every invocation, as in a connected or
@@ -134,40 +143,63 @@ export function getEntity( state, kind, name ) {
134
143
  *
135
144
  * @return {Object?} Record.
136
145
  */
137
- export function getEntityRecord( state, kind, name, key, query ) {
138
- const queriedState = get( state.entities.data, [
139
- kind,
140
- name,
141
- 'queriedData',
142
- ] );
143
- if ( ! queriedState ) {
144
- return undefined;
145
- }
146
- const context = query?.context ?? 'default';
147
-
148
- if ( query === undefined ) {
149
- // If expecting a complete item, validate that completeness.
150
- if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) {
146
+ export const getEntityRecord = createSelector(
147
+ ( state, kind, name, key, query ) => {
148
+ const queriedState = get( state.entities.data, [
149
+ kind,
150
+ name,
151
+ 'queriedData',
152
+ ] );
153
+ if ( ! queriedState ) {
151
154
  return undefined;
152
155
  }
156
+ const context = query?.context ?? 'default';
153
157
 
154
- return queriedState.items[ context ][ key ];
155
- }
158
+ if ( query === undefined ) {
159
+ // If expecting a complete item, validate that completeness.
160
+ if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) {
161
+ return undefined;
162
+ }
156
163
 
157
- const item = queriedState.items[ context ]?.[ key ];
158
- if ( item && query._fields ) {
159
- const filteredItem = {};
160
- const fields = getNormalizedCommaSeparable( query._fields );
161
- for ( let f = 0; f < fields.length; f++ ) {
162
- const field = fields[ f ].split( '.' );
163
- const value = get( item, field );
164
- set( filteredItem, field, value );
164
+ return queriedState.items[ context ][ key ];
165
165
  }
166
- return filteredItem;
167
- }
168
166
 
169
- return item;
170
- }
167
+ const item = queriedState.items[ context ]?.[ key ];
168
+ if ( item && query._fields ) {
169
+ const filteredItem = {};
170
+ const fields = getNormalizedCommaSeparable( query._fields );
171
+ for ( let f = 0; f < fields.length; f++ ) {
172
+ const field = fields[ f ].split( '.' );
173
+ const value = get( item, field );
174
+ set( filteredItem, field, value );
175
+ }
176
+ return filteredItem;
177
+ }
178
+
179
+ return item;
180
+ },
181
+ ( state, kind, name, recordId, query ) => {
182
+ const context = query?.context ?? 'default';
183
+ return [
184
+ get( state.entities.data, [
185
+ kind,
186
+ name,
187
+ 'queriedData',
188
+ 'items',
189
+ context,
190
+ recordId,
191
+ ] ),
192
+ get( state.entities.data, [
193
+ kind,
194
+ name,
195
+ 'queriedData',
196
+ 'itemIsComplete',
197
+ context,
198
+ recordId,
199
+ ] ),
200
+ ];
201
+ }
202
+ );
171
203
 
172
204
  /**
173
205
  * Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity from the API if the entity record isn't available in the local state.
@@ -221,7 +253,28 @@ export const getRawEntityRecord = createSelector(
221
253
  }, {} )
222
254
  );
223
255
  },
224
- ( state ) => [ state.entities.data ]
256
+ ( state, kind, name, recordId, query ) => {
257
+ const context = query?.context ?? 'default';
258
+ return [
259
+ state.entities.config,
260
+ get( state.entities.data, [
261
+ kind,
262
+ name,
263
+ 'queriedData',
264
+ 'items',
265
+ context,
266
+ recordId,
267
+ ] ),
268
+ get( state.entities.data, [
269
+ kind,
270
+ name,
271
+ 'queriedData',
272
+ 'itemIsComplete',
273
+ context,
274
+ recordId,
275
+ ] ),
276
+ ];
277
+ }
225
278
  );
226
279
 
227
280
  /**
@@ -282,8 +335,12 @@ export const __experimentalGetDirtyEntityRecords = createSelector(
282
335
  Object.keys( data[ kind ] ).forEach( ( name ) => {
283
336
  const primaryKeys = Object.keys(
284
337
  data[ kind ][ name ].edits
285
- ).filter( ( primaryKey ) =>
286
- hasEditsForEntityRecord( state, kind, name, primaryKey )
338
+ ).filter(
339
+ ( primaryKey ) =>
340
+ // The entity record must exist (not be deleted),
341
+ // and it must have edits.
342
+ getEntityRecord( state, kind, name, primaryKey ) &&
343
+ hasEditsForEntityRecord( state, kind, name, primaryKey )
287
344
  );
288
345
 
289
346
  if ( primaryKeys.length ) {
@@ -408,7 +465,10 @@ export const getEntityRecordNonTransientEdits = createSelector(
408
465
  return acc;
409
466
  }, {} );
410
467
  },
411
- ( state ) => [ state.entities.config, state.entities.data ]
468
+ ( state, kind, name, recordId ) => [
469
+ state.entities.config,
470
+ get( state.entities.data, [ kind, name, 'edits', recordId ] ),
471
+ ]
412
472
  );
413
473
 
414
474
  /**
@@ -446,7 +506,29 @@ export const getEditedEntityRecord = createSelector(
446
506
  ...getRawEntityRecord( state, kind, name, recordId ),
447
507
  ...getEntityRecordEdits( state, kind, name, recordId ),
448
508
  } ),
449
- ( state ) => [ state.entities.data ]
509
+ ( state, kind, name, recordId, query ) => {
510
+ const context = query?.context ?? 'default';
511
+ return [
512
+ state.entities.config,
513
+ get( state.entities.data, [
514
+ kind,
515
+ name,
516
+ 'queriedData',
517
+ 'items',
518
+ context,
519
+ recordId,
520
+ ] ),
521
+ get( state.entities.data, [
522
+ kind,
523
+ name,
524
+ 'queriedData',
525
+ 'itemIsComplete',
526
+ context,
527
+ recordId,
528
+ ] ),
529
+ get( state.entities.data, [ kind, name, 'edits', recordId ] ),
530
+ ];
531
+ }
450
532
  );
451
533
 
452
534
  /**
@@ -615,7 +697,18 @@ export function hasRedo( state ) {
615
697
  * @return {Object} The current theme.
616
698
  */
617
699
  export function getCurrentTheme( state ) {
618
- return state.themes[ state.currentTheme ];
700
+ return getEntityRecord( state, 'root', 'theme', state.currentTheme );
701
+ }
702
+
703
+ /**
704
+ * Return the ID of the current global styles object.
705
+ *
706
+ * @param {Object} state Data state.
707
+ *
708
+ * @return {string} The current global styles ID.
709
+ */
710
+ export function __experimentalGetCurrentGlobalStylesId( state ) {
711
+ return state.currentGlobalStylesId;
619
712
  }
620
713
 
621
714
  /**
@@ -626,7 +719,7 @@ export function getCurrentTheme( state ) {
626
719
  * @return {*} Index data.
627
720
  */
628
721
  export function getThemeSupports( state ) {
629
- return state.themeSupports;
722
+ return getCurrentTheme( state )?.theme_supports ?? EMPTY_OBJECT;
630
723
  }
631
724
 
632
725
  /**
@@ -813,3 +906,18 @@ export function __experimentalGetTemplateForLink( state, link ) {
813
906
  }
814
907
  return template;
815
908
  }
909
+
910
+ /**
911
+ * Retrieve the current theme's base global styles
912
+ *
913
+ * @param {Object} state Editor state.
914
+ *
915
+ * @return {Object?} The Global Styles object.
916
+ */
917
+ export function __experimentalGetCurrentThemeBaseGlobalStyles( state ) {
918
+ const currentTheme = getCurrentTheme( state );
919
+ if ( ! currentTheme ) {
920
+ return null;
921
+ }
922
+ return state.themeBaseGlobalStyles[ currentTheme.stylesheet ];
923
+ }
@@ -11,6 +11,7 @@ jest.mock( '@wordpress/api-fetch' );
11
11
  import {
12
12
  editEntityRecord,
13
13
  saveEntityRecord,
14
+ saveEditedEntityRecord,
14
15
  deleteEntityRecord,
15
16
  receiveUserPermission,
16
17
  receiveAutosaves,
@@ -33,16 +34,17 @@ describe( 'editEntityRecord', () => {
33
34
  const select = {
34
35
  getEntity: jest.fn(),
35
36
  };
36
- const fulfillment = editEntityRecord(
37
- entity.kind,
38
- entity.name,
39
- entity.id,
40
- {}
41
- )( { select } );
42
- expect( select.getEntity ).toHaveBeenCalledTimes( 1 );
43
- await expect( fulfillment ).rejects.toThrow(
37
+ const fulfillment = () =>
38
+ editEntityRecord(
39
+ entity.kind,
40
+ entity.name,
41
+ entity.id,
42
+ {}
43
+ )( { select } );
44
+ expect( fulfillment ).toThrow(
44
45
  `The entity being edited (${ entity.kind }, ${ entity.name }) does not have a loaded config.`
45
46
  );
47
+ expect( select.getEntity ).toHaveBeenCalledTimes( 1 );
46
48
  } );
47
49
  } );
48
50
 
@@ -106,6 +108,94 @@ describe( 'deleteEntityRecord', () => {
106
108
  } );
107
109
  } );
108
110
 
111
+ describe( 'saveEditedEntityRecord', () => {
112
+ beforeEach( async () => {
113
+ apiFetch.mockReset();
114
+ jest.useFakeTimers();
115
+ } );
116
+
117
+ it( 'Uses "id" as a key when no entity key is provided', async () => {
118
+ const area = { id: 1, menu: 0 };
119
+ const entities = [
120
+ {
121
+ kind: 'root',
122
+ name: 'navigationArea',
123
+ baseURL: '/wp/v2/block-navigation-areas',
124
+ },
125
+ ];
126
+ const select = {
127
+ getEntityRecordNonTransientEdits: () => [],
128
+ hasEditsForEntityRecord: () => true,
129
+ };
130
+
131
+ const dispatch = Object.assign( jest.fn(), {
132
+ saveEntityRecord: jest.fn(),
133
+ } );
134
+ // Provide entities
135
+ dispatch.mockReturnValueOnce( entities );
136
+
137
+ // Provide response
138
+ const updatedRecord = { ...area, menu: 10 };
139
+ apiFetch.mockImplementation( () => {
140
+ return updatedRecord;
141
+ } );
142
+
143
+ await saveEditedEntityRecord(
144
+ 'root',
145
+ 'navigationArea',
146
+ 1
147
+ )( { dispatch, select } );
148
+
149
+ expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
150
+ 'root',
151
+ 'navigationArea',
152
+ { id: 1 },
153
+ undefined
154
+ );
155
+ } );
156
+
157
+ it( 'Uses the entity key when provided', async () => {
158
+ const area = { area: 'primary', menu: 0 };
159
+ const entities = [
160
+ {
161
+ kind: 'root',
162
+ name: 'navigationArea',
163
+ baseURL: '/wp/v2/block-navigation-areas',
164
+ key: 'area',
165
+ },
166
+ ];
167
+ const select = {
168
+ getEntityRecordNonTransientEdits: () => [],
169
+ hasEditsForEntityRecord: () => true,
170
+ };
171
+
172
+ const dispatch = Object.assign( jest.fn(), {
173
+ saveEntityRecord: jest.fn(),
174
+ } );
175
+ // Provide entities
176
+ dispatch.mockReturnValueOnce( entities );
177
+
178
+ // Provide response
179
+ const updatedRecord = { ...area, menu: 10 };
180
+ apiFetch.mockImplementation( () => {
181
+ return updatedRecord;
182
+ } );
183
+
184
+ await saveEditedEntityRecord(
185
+ 'root',
186
+ 'navigationArea',
187
+ 'primary'
188
+ )( { dispatch, select } );
189
+
190
+ expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
191
+ 'root',
192
+ 'navigationArea',
193
+ { area: 'primary' },
194
+ undefined
195
+ );
196
+ } );
197
+ } );
198
+
109
199
  describe( 'saveEntityRecord', () => {
110
200
  beforeEach( async () => {
111
201
  apiFetch.mockReset();
@@ -386,21 +476,7 @@ describe( 'receiveCurrentUser', () => {
386
476
 
387
477
  describe( '__experimentalBatch', () => {
388
478
  it( 'batches multiple actions together', async () => {
389
- const generator = __experimentalBatch(
390
- [
391
- ( { saveEntityRecord: _saveEntityRecord } ) =>
392
- _saveEntityRecord( 'root', 'widget', {} ),
393
- ( { saveEditedEntityRecord: _saveEditedEntityRecord } ) =>
394
- _saveEditedEntityRecord( 'root', 'widget', 123 ),
395
- ( { deleteEntityRecord: _deleteEntityRecord } ) =>
396
- _deleteEntityRecord( 'root', 'widget', 123, {} ),
397
- ],
398
- { __unstableProcessor: ( inputs ) => Promise.resolve( inputs ) }
399
- );
400
- // Run generator up to `yield getDispatch()`.
401
- const { value: getDispatchControl } = generator.next();
402
- expect( getDispatchControl ).toEqual( { type: 'GET_DISPATCH' } );
403
- const actions = {
479
+ const dispatch = {
404
480
  saveEntityRecord: jest.fn(
405
481
  ( kind, name, record, { __unstableFetch } ) => {
406
482
  __unstableFetch( {} );
@@ -420,36 +496,39 @@ describe( '__experimentalBatch', () => {
420
496
  }
421
497
  ),
422
498
  };
423
- const dispatch = () => actions;
424
- // Run generator up to `yield __unstableAwaitPromise( ... )`.
425
- const { value: awaitPromiseControl } = generator.next( dispatch );
426
- expect( actions.saveEntityRecord ).toHaveBeenCalledWith(
499
+
500
+ const results = await __experimentalBatch(
501
+ [
502
+ ( { saveEntityRecord: _saveEntityRecord } ) =>
503
+ _saveEntityRecord( 'root', 'widget', {} ),
504
+ ( { saveEditedEntityRecord: _saveEditedEntityRecord } ) =>
505
+ _saveEditedEntityRecord( 'root', 'widget', 123 ),
506
+ ( { deleteEntityRecord: _deleteEntityRecord } ) =>
507
+ _deleteEntityRecord( 'root', 'widget', 123, {} ),
508
+ ],
509
+ { __unstableProcessor: ( inputs ) => Promise.resolve( inputs ) }
510
+ )( { dispatch } );
511
+
512
+ expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
427
513
  'root',
428
514
  'widget',
429
515
  {},
430
516
  { __unstableFetch: expect.any( Function ) }
431
517
  );
432
- expect( actions.saveEditedEntityRecord ).toHaveBeenCalledWith(
518
+ expect( dispatch.saveEditedEntityRecord ).toHaveBeenCalledWith(
433
519
  'root',
434
520
  'widget',
435
521
  123,
436
522
  { __unstableFetch: expect.any( Function ) }
437
523
  );
438
- expect( actions.deleteEntityRecord ).toHaveBeenCalledWith(
524
+ expect( dispatch.deleteEntityRecord ).toHaveBeenCalledWith(
439
525
  'root',
440
526
  'widget',
441
527
  123,
442
528
  {},
443
529
  { __unstableFetch: expect.any( Function ) }
444
530
  );
445
- expect( awaitPromiseControl ).toEqual( {
446
- type: 'AWAIT_PROMISE',
447
- promise: expect.any( Promise ),
448
- } );
449
- // Run generator to the end.
450
- const { value: results } = generator.next(
451
- await awaitPromiseControl.promise
452
- );
531
+
453
532
  expect( results ).toEqual( [
454
533
  { id: 123, created: true },
455
534
  { id: 123, updated: true },