@wordpress/core-data 4.8.0 → 4.11.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 (94) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +56 -56
  3. package/build/actions.js.map +1 -1
  4. package/build/batch/create-batch.js +1 -1
  5. package/build/batch/create-batch.js.map +1 -1
  6. package/build/batch/default-processor.js +13 -7
  7. package/build/batch/default-processor.js.map +1 -1
  8. package/build/entities.js.map +1 -1
  9. package/build/entity-provider.js.map +1 -1
  10. package/build/hooks/use-entity-record.js.map +1 -1
  11. package/build/hooks/use-query-select.js.map +1 -1
  12. package/build/hooks/use-resource-permissions.js +94 -0
  13. package/build/hooks/use-resource-permissions.js.map +1 -0
  14. package/build/index.js +28 -1
  15. package/build/index.js.map +1 -1
  16. package/build/queried-data/selectors.js.map +1 -1
  17. package/build/reducer.js +4 -1
  18. package/build/reducer.js.map +1 -1
  19. package/build/resolvers.js +4 -14
  20. package/build/resolvers.js.map +1 -1
  21. package/build/selectors.js +45 -8
  22. package/build/selectors.js.map +1 -1
  23. package/build/utils/forward-resolver.js.map +1 -1
  24. package/build/utils/on-sub-key.js.map +1 -1
  25. package/build/utils/with-weak-map-cache.js +1 -7
  26. package/build/utils/with-weak-map-cache.js.map +1 -1
  27. package/build-module/actions.js.map +1 -1
  28. package/build-module/batch/create-batch.js +2 -2
  29. package/build-module/batch/create-batch.js.map +1 -1
  30. package/build-module/batch/default-processor.js +12 -5
  31. package/build-module/batch/default-processor.js.map +1 -1
  32. package/build-module/entities.js.map +1 -1
  33. package/build-module/entity-provider.js.map +1 -1
  34. package/build-module/hooks/use-entity-record.js.map +1 -1
  35. package/build-module/hooks/use-query-select.js.map +1 -1
  36. package/build-module/hooks/use-resource-permissions.js +82 -0
  37. package/build-module/hooks/use-resource-permissions.js.map +1 -0
  38. package/build-module/index.js +3 -0
  39. package/build-module/index.js.map +1 -1
  40. package/build-module/queried-data/selectors.js.map +1 -1
  41. package/build-module/reducer.js +5 -2
  42. package/build-module/reducer.js.map +1 -1
  43. package/build-module/resolvers.js +5 -15
  44. package/build-module/resolvers.js.map +1 -1
  45. package/build-module/selectors.js +40 -4
  46. package/build-module/selectors.js.map +1 -1
  47. package/build-module/utils/forward-resolver.js.map +1 -1
  48. package/build-module/utils/on-sub-key.js.map +1 -1
  49. package/build-module/utils/with-weak-map-cache.js +1 -6
  50. package/build-module/utils/with-weak-map-cache.js.map +1 -1
  51. package/package.json +11 -11
  52. package/src/actions.js +389 -372
  53. package/src/batch/create-batch.js +2 -2
  54. package/src/batch/default-processor.js +10 -5
  55. package/src/entities.ts +16 -17
  56. package/src/entity-provider.js +4 -6
  57. package/src/entity-types/attachment.ts +4 -3
  58. package/src/entity-types/comment.ts +4 -3
  59. package/src/entity-types/entities.ts +5 -2
  60. package/src/entity-types/index.ts +114 -20
  61. package/src/entity-types/menu-location.ts +4 -3
  62. package/src/entity-types/nav-menu-item.ts +4 -3
  63. package/src/entity-types/nav-menu.ts +3 -3
  64. package/src/entity-types/page.ts +3 -3
  65. package/src/entity-types/plugin.ts +3 -3
  66. package/src/entity-types/post.ts +3 -3
  67. package/src/entity-types/settings.ts +3 -3
  68. package/src/entity-types/sidebar.ts +4 -3
  69. package/src/entity-types/taxonomy.ts +4 -3
  70. package/src/entity-types/theme.ts +3 -3
  71. package/src/entity-types/type.ts +3 -3
  72. package/src/entity-types/user.ts +3 -3
  73. package/src/entity-types/widget-type.ts +4 -3
  74. package/src/entity-types/widget.ts +3 -3
  75. package/src/entity-types/wp-template-part.ts +4 -3
  76. package/src/entity-types/wp-template.ts +4 -3
  77. package/src/fetch/test/__experimental-fetch-link-suggestions.js +2 -4
  78. package/src/hooks/test/use-query-select.js +4 -2
  79. package/src/hooks/test/use-resource-permissions.js +115 -0
  80. package/src/hooks/use-entity-record.ts +0 -1
  81. package/src/hooks/use-query-select.ts +26 -24
  82. package/src/hooks/use-resource-permissions.ts +120 -0
  83. package/src/index.js +3 -0
  84. package/src/locks/test/selectors.js +2 -1
  85. package/src/queried-data/selectors.js +2 -8
  86. package/src/reducer.js +9 -2
  87. package/src/resolvers.js +344 -326
  88. package/src/selectors.ts +347 -194
  89. package/src/test/reducer.js +5 -4
  90. package/src/test/resolvers.js +1 -3
  91. package/src/test/selectors.js +1 -2
  92. package/src/utils/forward-resolver.js +6 -5
  93. package/src/utils/on-sub-key.js +20 -20
  94. package/src/utils/with-weak-map-cache.js +1 -6
package/src/resolvers.js CHANGED
@@ -1,7 +1,16 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { find, includes, get, compact, uniq, map, mapKeys } from 'lodash';
4
+ import {
5
+ camelCase,
6
+ compact,
7
+ find,
8
+ get,
9
+ includes,
10
+ map,
11
+ mapKeys,
12
+ uniq,
13
+ } from 'lodash';
5
14
 
6
15
  /**
7
16
  * WordPress dependencies
@@ -22,22 +31,26 @@ import { forwardResolver, getNormalizedCommaSeparable } from './utils';
22
31
  * @param {Object|undefined} query Optional object of query parameters to
23
32
  * include with request.
24
33
  */
25
- export const getAuthors = ( query ) => async ( { dispatch } ) => {
26
- const path = addQueryArgs(
27
- '/wp/v2/users/?who=authors&per_page=100',
28
- query
29
- );
30
- const users = await apiFetch( { path } );
31
- dispatch.receiveUserQuery( path, users );
32
- };
34
+ export const getAuthors =
35
+ ( query ) =>
36
+ async ( { dispatch } ) => {
37
+ const path = addQueryArgs(
38
+ '/wp/v2/users/?who=authors&per_page=100',
39
+ query
40
+ );
41
+ const users = await apiFetch( { path } );
42
+ dispatch.receiveUserQuery( path, users );
43
+ };
33
44
 
34
45
  /**
35
46
  * Requests the current user from the REST API.
36
47
  */
37
- export const getCurrentUser = () => async ( { dispatch } ) => {
38
- const currentUser = await apiFetch( { path: '/wp/v2/users/me' } );
39
- dispatch.receiveCurrentUser( currentUser );
40
- };
48
+ export const getCurrentUser =
49
+ () =>
50
+ async ( { dispatch } ) => {
51
+ const currentUser = await apiFetch( { path: '/wp/v2/users/me' } );
52
+ dispatch.receiveCurrentUser( currentUser );
53
+ };
41
54
 
42
55
  /**
43
56
  * Requests an entity's record from the REST API.
@@ -48,69 +61,69 @@ export const getCurrentUser = () => async ( { dispatch } ) => {
48
61
  * @param {Object|undefined} query Optional object of query parameters to
49
62
  * include with request.
50
63
  */
51
- export const getEntityRecord = ( kind, name, key = '', query ) => async ( {
52
- select,
53
- dispatch,
54
- } ) => {
55
- const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
56
- const entityConfig = find( configs, { kind, name } );
57
- if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
58
- return;
59
- }
60
-
61
- const lock = await dispatch.__unstableAcquireStoreLock(
62
- STORE_NAME,
63
- [ 'entities', 'records', kind, name, key ],
64
- { exclusive: false }
65
- );
66
-
67
- try {
68
- if ( query !== undefined && query._fields ) {
69
- // If requesting specific fields, items and query association to said
70
- // records are stored by ID reference. Thus, fields must always include
71
- // the ID.
72
- query = {
73
- ...query,
74
- _fields: uniq( [
75
- ...( getNormalizedCommaSeparable( query._fields ) || [] ),
76
- entityConfig.key || DEFAULT_ENTITY_KEY,
77
- ] ).join(),
78
- };
64
+ export const getEntityRecord =
65
+ ( kind, name, key = '', query ) =>
66
+ async ( { select, dispatch } ) => {
67
+ const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
68
+ const entityConfig = find( configs, { kind, name } );
69
+ if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
70
+ return;
79
71
  }
80
72
 
81
- // Disable reason: While true that an early return could leave `path`
82
- // unused, it's important that path is derived using the query prior to
83
- // additional query modifications in the condition below, since those
84
- // modifications are relevant to how the data is tracked in state, and not
85
- // for how the request is made to the REST API.
86
-
87
- // eslint-disable-next-line @wordpress/no-unused-vars-before-return
88
- const path = addQueryArgs(
89
- entityConfig.baseURL + ( key ? '/' + key : '' ),
90
- {
91
- ...entityConfig.baseURLParams,
92
- ...query,
93
- }
73
+ const lock = await dispatch.__unstableAcquireStoreLock(
74
+ STORE_NAME,
75
+ [ 'entities', 'records', kind, name, key ],
76
+ { exclusive: false }
94
77
  );
95
78
 
96
- if ( query !== undefined ) {
97
- query = { ...query, include: [ key ] };
79
+ try {
80
+ if ( query !== undefined && query._fields ) {
81
+ // If requesting specific fields, items and query association to said
82
+ // records are stored by ID reference. Thus, fields must always include
83
+ // the ID.
84
+ query = {
85
+ ...query,
86
+ _fields: uniq( [
87
+ ...( getNormalizedCommaSeparable( query._fields ) ||
88
+ [] ),
89
+ entityConfig.key || DEFAULT_ENTITY_KEY,
90
+ ] ).join(),
91
+ };
92
+ }
98
93
 
99
- // The resolution cache won't consider query as reusable based on the
100
- // fields, so it's tested here, prior to initiating the REST request,
101
- // and without causing `getEntityRecords` resolution to occur.
102
- const hasRecords = select.hasEntityRecords( kind, name, query );
103
- if ( hasRecords ) {
104
- return;
94
+ // Disable reason: While true that an early return could leave `path`
95
+ // unused, it's important that path is derived using the query prior to
96
+ // additional query modifications in the condition below, since those
97
+ // modifications are relevant to how the data is tracked in state, and not
98
+ // for how the request is made to the REST API.
99
+
100
+ // eslint-disable-next-line @wordpress/no-unused-vars-before-return
101
+ const path = addQueryArgs(
102
+ entityConfig.baseURL + ( key ? '/' + key : '' ),
103
+ {
104
+ ...entityConfig.baseURLParams,
105
+ ...query,
106
+ }
107
+ );
108
+
109
+ if ( query !== undefined ) {
110
+ query = { ...query, include: [ key ] };
111
+
112
+ // The resolution cache won't consider query as reusable based on the
113
+ // fields, so it's tested here, prior to initiating the REST request,
114
+ // and without causing `getEntityRecords` resolution to occur.
115
+ const hasRecords = select.hasEntityRecords( kind, name, query );
116
+ if ( hasRecords ) {
117
+ return;
118
+ }
105
119
  }
106
- }
107
120
 
108
- const record = await apiFetch( { path } );
109
- dispatch.receiveEntityRecords( kind, name, record, query );
110
- } finally {
111
- dispatch.__unstableReleaseStoreLock( lock );
112
- }
113
- };
121
+ const record = await apiFetch( { path } );
122
+ dispatch.receiveEntityRecords( kind, name, record, query );
123
+ } finally {
124
+ dispatch.__unstableReleaseStoreLock( lock );
125
+ }
126
+ };
114
127
 
115
128
  /**
116
129
  * Requests an entity's record from the REST API.
@@ -129,82 +142,83 @@ export const getEditedEntityRecord = forwardResolver( 'getEntityRecord' );
129
142
  * @param {string} name Entity name.
130
143
  * @param {Object?} query Query Object.
131
144
  */
132
- export const getEntityRecords = ( kind, name, query = {} ) => async ( {
133
- dispatch,
134
- } ) => {
135
- const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
136
- const entityConfig = find( configs, { kind, name } );
137
- if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
138
- return;
139
- }
140
-
141
- const lock = await dispatch.__unstableAcquireStoreLock(
142
- STORE_NAME,
143
- [ 'entities', 'records', kind, name ],
144
- { exclusive: false }
145
- );
146
-
147
- try {
148
- if ( query._fields ) {
149
- // If requesting specific fields, items and query association to said
150
- // records are stored by ID reference. Thus, fields must always include
151
- // the ID.
152
- query = {
153
- ...query,
154
- _fields: uniq( [
155
- ...( getNormalizedCommaSeparable( query._fields ) || [] ),
156
- entityConfig.key || DEFAULT_ENTITY_KEY,
157
- ] ).join(),
158
- };
145
+ export const getEntityRecords =
146
+ ( kind, name, query = {} ) =>
147
+ async ( { dispatch } ) => {
148
+ const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
149
+ const entityConfig = find( configs, { kind, name } );
150
+ if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
151
+ return;
159
152
  }
160
153
 
161
- const path = addQueryArgs( entityConfig.baseURL, {
162
- ...entityConfig.baseURLParams,
163
- ...query,
164
- } );
154
+ const lock = await dispatch.__unstableAcquireStoreLock(
155
+ STORE_NAME,
156
+ [ 'entities', 'records', kind, name ],
157
+ { exclusive: false }
158
+ );
165
159
 
166
- let records = Object.values( await apiFetch( { path } ) );
167
- // If we request fields but the result doesn't contain the fields,
168
- // explicitely set these fields as "undefined"
169
- // that way we consider the query "fullfilled".
170
- if ( query._fields ) {
171
- records = records.map( ( record ) => {
172
- query._fields.split( ',' ).forEach( ( field ) => {
173
- if ( ! record.hasOwnProperty( field ) ) {
174
- record[ field ] = undefined;
175
- }
176
- } );
160
+ try {
161
+ if ( query._fields ) {
162
+ // If requesting specific fields, items and query association to said
163
+ // records are stored by ID reference. Thus, fields must always include
164
+ // the ID.
165
+ query = {
166
+ ...query,
167
+ _fields: uniq( [
168
+ ...( getNormalizedCommaSeparable( query._fields ) ||
169
+ [] ),
170
+ entityConfig.key || DEFAULT_ENTITY_KEY,
171
+ ] ).join(),
172
+ };
173
+ }
177
174
 
178
- return record;
175
+ const path = addQueryArgs( entityConfig.baseURL, {
176
+ ...entityConfig.baseURLParams,
177
+ ...query,
179
178
  } );
180
- }
181
179
 
182
- dispatch.receiveEntityRecords( kind, name, records, query );
183
-
184
- // When requesting all fields, the list of results can be used to
185
- // resolve the `getEntityRecord` selector in addition to `getEntityRecords`.
186
- // See https://github.com/WordPress/gutenberg/pull/26575
187
- if ( ! query?._fields && ! query.context ) {
188
- const key = entityConfig.key || DEFAULT_ENTITY_KEY;
189
- const resolutionsArgs = records
190
- .filter( ( record ) => record[ key ] )
191
- .map( ( record ) => [ kind, name, record[ key ] ] );
192
-
193
- dispatch( {
194
- type: 'START_RESOLUTIONS',
195
- selectorName: 'getEntityRecord',
196
- args: resolutionsArgs,
197
- } );
198
- dispatch( {
199
- type: 'FINISH_RESOLUTIONS',
200
- selectorName: 'getEntityRecord',
201
- args: resolutionsArgs,
202
- } );
180
+ let records = Object.values( await apiFetch( { path } ) );
181
+ // If we request fields but the result doesn't contain the fields,
182
+ // explicitely set these fields as "undefined"
183
+ // that way we consider the query "fullfilled".
184
+ if ( query._fields ) {
185
+ records = records.map( ( record ) => {
186
+ query._fields.split( ',' ).forEach( ( field ) => {
187
+ if ( ! record.hasOwnProperty( field ) ) {
188
+ record[ field ] = undefined;
189
+ }
190
+ } );
191
+
192
+ return record;
193
+ } );
194
+ }
195
+
196
+ dispatch.receiveEntityRecords( kind, name, records, query );
197
+
198
+ // When requesting all fields, the list of results can be used to
199
+ // resolve the `getEntityRecord` selector in addition to `getEntityRecords`.
200
+ // See https://github.com/WordPress/gutenberg/pull/26575
201
+ if ( ! query?._fields && ! query.context ) {
202
+ const key = entityConfig.key || DEFAULT_ENTITY_KEY;
203
+ const resolutionsArgs = records
204
+ .filter( ( record ) => record[ key ] )
205
+ .map( ( record ) => [ kind, name, record[ key ] ] );
206
+
207
+ dispatch( {
208
+ type: 'START_RESOLUTIONS',
209
+ selectorName: 'getEntityRecord',
210
+ args: resolutionsArgs,
211
+ } );
212
+ dispatch( {
213
+ type: 'FINISH_RESOLUTIONS',
214
+ selectorName: 'getEntityRecord',
215
+ args: resolutionsArgs,
216
+ } );
217
+ }
218
+ } finally {
219
+ dispatch.__unstableReleaseStoreLock( lock );
203
220
  }
204
- } finally {
205
- dispatch.__unstableReleaseStoreLock( lock );
206
- }
207
- };
221
+ };
208
222
 
209
223
  getEntityRecords.shouldInvalidate = ( action, kind, name ) => {
210
224
  return (
@@ -218,15 +232,17 @@ getEntityRecords.shouldInvalidate = ( action, kind, name ) => {
218
232
  /**
219
233
  * Requests the current theme.
220
234
  */
221
- export const getCurrentTheme = () => async ( { dispatch, resolveSelect } ) => {
222
- const activeThemes = await resolveSelect.getEntityRecords(
223
- 'root',
224
- 'theme',
225
- { status: 'active' }
226
- );
235
+ export const getCurrentTheme =
236
+ () =>
237
+ async ( { dispatch, resolveSelect } ) => {
238
+ const activeThemes = await resolveSelect.getEntityRecords(
239
+ 'root',
240
+ 'theme',
241
+ { status: 'active' }
242
+ );
227
243
 
228
- dispatch.receiveCurrentTheme( activeThemes[ 0 ] );
229
- };
244
+ dispatch.receiveCurrentTheme( activeThemes[ 0 ] );
245
+ };
230
246
 
231
247
  /**
232
248
  * Requests theme supports data from the index.
@@ -238,17 +254,19 @@ export const getThemeSupports = forwardResolver( 'getCurrentTheme' );
238
254
  *
239
255
  * @param {string} url URL to get the preview for.
240
256
  */
241
- export const getEmbedPreview = ( url ) => async ( { dispatch } ) => {
242
- try {
243
- const embedProxyResponse = await apiFetch( {
244
- path: addQueryArgs( '/oembed/1.0/proxy', { url } ),
245
- } );
246
- dispatch.receiveEmbedPreview( url, embedProxyResponse );
247
- } catch ( error ) {
248
- // Embed API 404s if the URL cannot be embedded, so we have to catch the error from the apiRequest here.
249
- dispatch.receiveEmbedPreview( url, false );
250
- }
251
- };
257
+ export const getEmbedPreview =
258
+ ( url ) =>
259
+ async ( { dispatch } ) => {
260
+ try {
261
+ const embedProxyResponse = await apiFetch( {
262
+ path: addQueryArgs( '/oembed/1.0/proxy', { url } ),
263
+ } );
264
+ dispatch.receiveEmbedPreview( url, embedProxyResponse );
265
+ } catch ( error ) {
266
+ // Embed API 404s if the URL cannot be embedded, so we have to catch the error from the apiRequest here.
267
+ dispatch.receiveEmbedPreview( url, false );
268
+ }
269
+ };
252
270
 
253
271
  /**
254
272
  * Checks whether the current user can perform the given action on the given
@@ -259,42 +277,46 @@ export const getEmbedPreview = ( url ) => async ( { dispatch } ) => {
259
277
  * @param {string} resource REST resource to check, e.g. 'media' or 'posts'.
260
278
  * @param {?string} id ID of the rest resource to check.
261
279
  */
262
- export const canUser = ( action, resource, id ) => async ( { dispatch } ) => {
263
- const methods = {
264
- create: 'POST',
265
- read: 'GET',
266
- update: 'PUT',
267
- delete: 'DELETE',
268
- };
280
+ export const canUser =
281
+ ( action, resource, id ) =>
282
+ async ( { dispatch } ) => {
283
+ const methods = {
284
+ create: 'POST',
285
+ read: 'GET',
286
+ update: 'PUT',
287
+ delete: 'DELETE',
288
+ };
289
+
290
+ const method = methods[ action ];
291
+ if ( ! method ) {
292
+ throw new Error( `'${ action }' is not a valid action.` );
293
+ }
269
294
 
270
- const method = methods[ action ];
271
- if ( ! method ) {
272
- throw new Error( `'${ action }' is not a valid action.` );
273
- }
295
+ const path = id
296
+ ? `/wp/v2/${ resource }/${ id }`
297
+ : `/wp/v2/${ resource }`;
274
298
 
275
- const path = id ? `/wp/v2/${ resource }/${ id }` : `/wp/v2/${ resource }`;
299
+ let response;
300
+ try {
301
+ response = await apiFetch( {
302
+ path,
303
+ method: 'OPTIONS',
304
+ parse: false,
305
+ } );
306
+ } catch ( error ) {
307
+ // Do nothing if our OPTIONS request comes back with an API error (4xx or
308
+ // 5xx). The previously determined isAllowed value will remain in the store.
309
+ return;
310
+ }
276
311
 
277
- let response;
278
- try {
279
- response = await apiFetch( {
280
- path,
281
- method: 'OPTIONS',
282
- parse: false,
283
- } );
284
- } catch ( error ) {
285
- // Do nothing if our OPTIONS request comes back with an API error (4xx or
286
- // 5xx). The previously determined isAllowed value will remain in the store.
287
- return;
288
- }
289
-
290
- // Optional chaining operator is used here because the API requests don't
291
- // return the expected result in the native version. Instead, API requests
292
- // only return the result, without including response properties like the headers.
293
- const allowHeader = response.headers?.get( 'allow' );
294
- const key = compact( [ action, resource, id ] ).join( '/' );
295
- const isAllowed = includes( allowHeader, method );
296
- dispatch.receiveUserPermission( key, isAllowed );
297
- };
312
+ // Optional chaining operator is used here because the API requests don't
313
+ // return the expected result in the native version. Instead, API requests
314
+ // only return the result, without including response properties like the headers.
315
+ const allowHeader = response.headers?.get( 'allow' );
316
+ const key = compact( [ action, resource, id ] ).join( '/' );
317
+ const isAllowed = includes( allowHeader, method );
318
+ dispatch.receiveUserPermission( key, isAllowed );
319
+ };
298
320
 
299
321
  /**
300
322
  * Checks whether the current user can perform the given action on the given
@@ -304,18 +326,18 @@ export const canUser = ( action, resource, id ) => async ( { dispatch } ) => {
304
326
  * @param {string} name Entity name.
305
327
  * @param {string} recordId Record's id.
306
328
  */
307
- export const canUserEditEntityRecord = ( kind, name, recordId ) => async ( {
308
- dispatch,
309
- } ) => {
310
- const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
311
- const entityConfig = find( configs, { kind, name } );
312
- if ( ! entityConfig ) {
313
- return;
314
- }
315
-
316
- const resource = entityConfig.__unstable_rest_base;
317
- await dispatch( canUser( 'update', resource, recordId ) );
318
- };
329
+ export const canUserEditEntityRecord =
330
+ ( kind, name, recordId ) =>
331
+ async ( { dispatch } ) => {
332
+ const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
333
+ const entityConfig = find( configs, { kind, name } );
334
+ if ( ! entityConfig ) {
335
+ return;
336
+ }
337
+
338
+ const resource = entityConfig.__unstable_rest_base;
339
+ await dispatch( canUser( 'update', resource, recordId ) );
340
+ };
319
341
 
320
342
  /**
321
343
  * Request autosave data from the REST API.
@@ -323,19 +345,19 @@ export const canUserEditEntityRecord = ( kind, name, recordId ) => async ( {
323
345
  * @param {string} postType The type of the parent post.
324
346
  * @param {number} postId The id of the parent post.
325
347
  */
326
- export const getAutosaves = ( postType, postId ) => async ( {
327
- dispatch,
328
- resolveSelect,
329
- } ) => {
330
- const { rest_base: restBase } = await resolveSelect.getPostType( postType );
331
- const autosaves = await apiFetch( {
332
- path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit`,
333
- } );
334
-
335
- if ( autosaves && autosaves.length ) {
336
- dispatch.receiveAutosaves( postId, autosaves );
337
- }
338
- };
348
+ export const getAutosaves =
349
+ ( postType, postId ) =>
350
+ async ( { dispatch, resolveSelect } ) => {
351
+ const { rest_base: restBase, rest_namespace: restNamespace = 'wp/v2' } =
352
+ await resolveSelect.getPostType( postType );
353
+ const autosaves = await apiFetch( {
354
+ path: `/${ restNamespace }/${ restBase }/${ postId }/autosaves?context=edit`,
355
+ } );
356
+
357
+ if ( autosaves && autosaves.length ) {
358
+ dispatch.receiveAutosaves( postId, autosaves );
359
+ }
360
+ };
339
361
 
340
362
  /**
341
363
  * Request autosave data from the REST API.
@@ -346,50 +368,54 @@ export const getAutosaves = ( postType, postId ) => async ( {
346
368
  * @param {string} postType The type of the parent post.
347
369
  * @param {number} postId The id of the parent post.
348
370
  */
349
- export const getAutosave = ( postType, postId ) => async ( {
350
- resolveSelect,
351
- } ) => {
352
- await resolveSelect.getAutosaves( postType, postId );
353
- };
371
+ export const getAutosave =
372
+ ( postType, postId ) =>
373
+ async ( { resolveSelect } ) => {
374
+ await resolveSelect.getAutosaves( postType, postId );
375
+ };
354
376
 
355
377
  /**
356
378
  * Retrieve the frontend template used for a given link.
357
379
  *
358
380
  * @param {string} link Link.
359
381
  */
360
- export const __experimentalGetTemplateForLink = ( link ) => async ( {
361
- dispatch,
362
- resolveSelect,
363
- } ) => {
364
- // Ideally this should be using an apiFetch call
365
- // We could potentially do so by adding a "filter" to the `wp_template` end point.
366
- // Also it seems the returned object is not a regular REST API post type.
367
- let template;
368
- try {
369
- template = await window
370
- .fetch( addQueryArgs( link, { '_wp-find-template': true } ) )
371
- .then( ( res ) => res.json() )
372
- .then( ( { data } ) => data );
373
- } catch ( e ) {
374
- // For non-FSE themes, it is possible that this request returns an error.
375
- }
376
-
377
- if ( ! template ) {
378
- return;
379
- }
380
-
381
- const record = await resolveSelect.getEntityRecord(
382
- 'postType',
383
- 'wp_template',
384
- template.id
385
- );
382
+ export const __experimentalGetTemplateForLink =
383
+ ( link ) =>
384
+ async ( { dispatch, resolveSelect } ) => {
385
+ // Ideally this should be using an apiFetch call
386
+ // We could potentially do so by adding a "filter" to the `wp_template` end point.
387
+ // Also it seems the returned object is not a regular REST API post type.
388
+ let template;
389
+ try {
390
+ template = await window
391
+ .fetch( addQueryArgs( link, { '_wp-find-template': true } ) )
392
+ .then( ( res ) => res.json() )
393
+ .then( ( { data } ) => data );
394
+ } catch ( e ) {
395
+ // For non-FSE themes, it is possible that this request returns an error.
396
+ }
386
397
 
387
- if ( record ) {
388
- dispatch.receiveEntityRecords( 'postType', 'wp_template', [ record ], {
389
- 'find-template': link,
390
- } );
391
- }
392
- };
398
+ if ( ! template ) {
399
+ return;
400
+ }
401
+
402
+ const record = await resolveSelect.getEntityRecord(
403
+ 'postType',
404
+ 'wp_template',
405
+ template.id
406
+ );
407
+
408
+ if ( record ) {
409
+ dispatch.receiveEntityRecords(
410
+ 'postType',
411
+ 'wp_template',
412
+ [ record ],
413
+ {
414
+ 'find-template': link,
415
+ }
416
+ );
417
+ }
418
+ };
393
419
 
394
420
  __experimentalGetTemplateForLink.shouldInvalidate = ( action ) => {
395
421
  return (
@@ -400,82 +426,74 @@ __experimentalGetTemplateForLink.shouldInvalidate = ( action ) => {
400
426
  );
401
427
  };
402
428
 
403
- export const __experimentalGetCurrentGlobalStylesId = () => async ( {
404
- dispatch,
405
- resolveSelect,
406
- } ) => {
407
- const activeThemes = await resolveSelect.getEntityRecords(
408
- 'root',
409
- 'theme',
410
- { status: 'active' }
411
- );
412
- const globalStylesURL = get( activeThemes, [
413
- 0,
414
- '_links',
415
- 'wp:user-global-styles',
416
- 0,
417
- 'href',
418
- ] );
419
- if ( globalStylesURL ) {
420
- const globalStylesObject = await apiFetch( {
421
- url: globalStylesURL,
422
- } );
423
- dispatch.__experimentalReceiveCurrentGlobalStylesId(
424
- globalStylesObject.id
429
+ export const __experimentalGetCurrentGlobalStylesId =
430
+ () =>
431
+ async ( { dispatch, resolveSelect } ) => {
432
+ const activeThemes = await resolveSelect.getEntityRecords(
433
+ 'root',
434
+ 'theme',
435
+ { status: 'active' }
425
436
  );
426
- }
427
- };
437
+ const globalStylesURL = get( activeThemes, [
438
+ 0,
439
+ '_links',
440
+ 'wp:user-global-styles',
441
+ 0,
442
+ 'href',
443
+ ] );
444
+ if ( globalStylesURL ) {
445
+ const globalStylesObject = await apiFetch( {
446
+ url: globalStylesURL,
447
+ } );
448
+ dispatch.__experimentalReceiveCurrentGlobalStylesId(
449
+ globalStylesObject.id
450
+ );
451
+ }
452
+ };
428
453
 
429
- export const __experimentalGetCurrentThemeBaseGlobalStyles = () => async ( {
430
- resolveSelect,
431
- dispatch,
432
- } ) => {
433
- const currentTheme = await resolveSelect.getCurrentTheme();
434
- const themeGlobalStyles = await apiFetch( {
435
- path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }`,
436
- } );
437
- dispatch.__experimentalReceiveThemeBaseGlobalStyles(
438
- currentTheme.stylesheet,
439
- themeGlobalStyles
440
- );
441
- };
454
+ export const __experimentalGetCurrentThemeBaseGlobalStyles =
455
+ () =>
456
+ async ( { resolveSelect, dispatch } ) => {
457
+ const currentTheme = await resolveSelect.getCurrentTheme();
458
+ const themeGlobalStyles = await apiFetch( {
459
+ path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }`,
460
+ } );
461
+ dispatch.__experimentalReceiveThemeBaseGlobalStyles(
462
+ currentTheme.stylesheet,
463
+ themeGlobalStyles
464
+ );
465
+ };
442
466
 
443
- export const __experimentalGetCurrentThemeGlobalStylesVariations = () => async ( {
444
- resolveSelect,
445
- dispatch,
446
- } ) => {
447
- const currentTheme = await resolveSelect.getCurrentTheme();
448
- const variations = await apiFetch( {
449
- path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }/variations`,
450
- } );
451
- dispatch.__experimentalReceiveThemeGlobalStyleVariations(
452
- currentTheme.stylesheet,
453
- variations
454
- );
455
- };
467
+ export const __experimentalGetCurrentThemeGlobalStylesVariations =
468
+ () =>
469
+ async ( { resolveSelect, dispatch } ) => {
470
+ const currentTheme = await resolveSelect.getCurrentTheme();
471
+ const variations = await apiFetch( {
472
+ path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }/variations`,
473
+ } );
474
+ dispatch.__experimentalReceiveThemeGlobalStyleVariations(
475
+ currentTheme.stylesheet,
476
+ variations
477
+ );
478
+ };
456
479
 
457
- export const getBlockPatterns = () => async ( { dispatch } ) => {
458
- const restPatterns = await apiFetch( {
459
- path: '/wp/v2/block-patterns/patterns',
460
- } );
461
- const patterns = map( restPatterns, ( pattern ) =>
462
- mapKeys( pattern, ( value, key ) => {
463
- switch ( key ) {
464
- case 'block_types':
465
- return 'blockTypes';
466
- case 'viewport_width':
467
- return 'viewportWidth';
468
- default:
469
- return key;
470
- }
471
- } )
472
- );
473
- dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } );
474
- };
480
+ export const getBlockPatterns =
481
+ () =>
482
+ async ( { dispatch } ) => {
483
+ const restPatterns = await apiFetch( {
484
+ path: '/wp/v2/block-patterns/patterns',
485
+ } );
486
+ const patterns = map( restPatterns, ( pattern ) =>
487
+ mapKeys( pattern, ( value, key ) => camelCase( key ) )
488
+ );
489
+ dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } );
490
+ };
475
491
 
476
- export const getBlockPatternCategories = () => async ( { dispatch } ) => {
477
- const categories = await apiFetch( {
478
- path: '/wp/v2/block-patterns/categories',
479
- } );
480
- dispatch( { type: 'RECEIVE_BLOCK_PATTERN_CATEGORIES', categories } );
481
- };
492
+ export const getBlockPatternCategories =
493
+ () =>
494
+ async ( { dispatch } ) => {
495
+ const categories = await apiFetch( {
496
+ path: '/wp/v2/block-patterns/categories',
497
+ } );
498
+ dispatch( { type: 'RECEIVE_BLOCK_PATTERN_CATEGORIES', categories } );
499
+ };