@wordpress/core-data 7.2.0 → 7.4.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 (131) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +54 -6
  3. package/build/actions.js +6 -6
  4. package/build/actions.js.map +1 -1
  5. package/build/entity-context.js +13 -0
  6. package/build/entity-context.js.map +1 -0
  7. package/build/entity-provider.js +4 -189
  8. package/build/entity-provider.js.map +1 -1
  9. package/build/entity-types/menu-location.js.map +1 -1
  10. package/build/entity-types/settings.js.map +1 -1
  11. package/build/entity-types/theme.js.map +1 -1
  12. package/build/entity-types/widget-type.js.map +1 -1
  13. package/build/hooks/index.js +22 -0
  14. package/build/hooks/index.js.map +1 -1
  15. package/build/hooks/use-entity-block-editor.js +140 -0
  16. package/build/hooks/use-entity-block-editor.js.map +1 -0
  17. package/build/hooks/use-entity-id.js +28 -0
  18. package/build/hooks/use-entity-id.js.map +1 -0
  19. package/build/hooks/use-entity-prop.js +65 -0
  20. package/build/hooks/use-entity-prop.js.map +1 -0
  21. package/build/hooks/use-resource-permissions.js +25 -8
  22. package/build/hooks/use-resource-permissions.js.map +1 -1
  23. package/build/resolvers.js +81 -70
  24. package/build/resolvers.js.map +1 -1
  25. package/build/selectors.js +23 -9
  26. package/build/selectors.js.map +1 -1
  27. package/build/utils/index.js +19 -0
  28. package/build/utils/index.js.map +1 -1
  29. package/build/utils/user-permissions.js +32 -0
  30. package/build/utils/user-permissions.js.map +1 -0
  31. package/build-module/actions.js +6 -6
  32. package/build-module/actions.js.map +1 -1
  33. package/build-module/entity-context.js +6 -0
  34. package/build-module/entity-context.js.map +1 -0
  35. package/build-module/entity-provider.js +3 -185
  36. package/build-module/entity-provider.js.map +1 -1
  37. package/build-module/entity-types/menu-location.js.map +1 -1
  38. package/build-module/entity-types/settings.js.map +1 -1
  39. package/build-module/entity-types/theme.js.map +1 -1
  40. package/build-module/entity-types/widget-type.js.map +1 -1
  41. package/build-module/hooks/index.js +3 -0
  42. package/build-module/hooks/index.js.map +1 -1
  43. package/build-module/hooks/use-entity-block-editor.js +132 -0
  44. package/build-module/hooks/use-entity-block-editor.js.map +1 -0
  45. package/build-module/hooks/use-entity-id.js +22 -0
  46. package/build-module/hooks/use-entity-id.js.map +1 -0
  47. package/build-module/hooks/use-entity-prop.js +58 -0
  48. package/build-module/hooks/use-entity-prop.js.map +1 -0
  49. package/build-module/hooks/use-resource-permissions.js +25 -8
  50. package/build-module/hooks/use-resource-permissions.js.map +1 -1
  51. package/build-module/resolvers.js +82 -71
  52. package/build-module/resolvers.js.map +1 -1
  53. package/build-module/selectors.js +24 -10
  54. package/build-module/selectors.js.map +1 -1
  55. package/build-module/utils/index.js +1 -0
  56. package/build-module/utils/index.js.map +1 -1
  57. package/build-module/utils/user-permissions.js +24 -0
  58. package/build-module/utils/user-permissions.js.map +1 -0
  59. package/build-types/actions.d.ts +3 -3
  60. package/build-types/actions.d.ts.map +1 -1
  61. package/build-types/batch/create-batch.d.ts.map +1 -1
  62. package/build-types/entities.d.ts.map +1 -1
  63. package/build-types/entity-context.d.ts +2 -0
  64. package/build-types/entity-context.d.ts.map +1 -0
  65. package/build-types/entity-provider.d.ts +0 -48
  66. package/build-types/entity-provider.d.ts.map +1 -1
  67. package/build-types/entity-types/menu-location.d.ts.map +1 -1
  68. package/build-types/entity-types/settings.d.ts.map +1 -1
  69. package/build-types/entity-types/theme.d.ts.map +1 -1
  70. package/build-types/entity-types/widget-type.d.ts.map +1 -1
  71. package/build-types/fetch/__experimental-fetch-url-data.d.ts.map +1 -1
  72. package/build-types/hooks/index.d.ts +3 -0
  73. package/build-types/hooks/index.d.ts.map +1 -1
  74. package/build-types/hooks/use-entity-block-editor.d.ts +22 -0
  75. package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -0
  76. package/build-types/hooks/use-entity-id.d.ts +9 -0
  77. package/build-types/hooks/use-entity-id.d.ts.map +1 -0
  78. package/build-types/hooks/use-entity-prop.d.ts +19 -0
  79. package/build-types/hooks/use-entity-prop.d.ts.map +1 -0
  80. package/build-types/hooks/use-resource-permissions.d.ts +8 -70
  81. package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
  82. package/build-types/index.d.ts +35 -32
  83. package/build-types/index.d.ts.map +1 -1
  84. package/build-types/locks/reducer.d.ts +1 -1
  85. package/build-types/locks/reducer.d.ts.map +1 -1
  86. package/build-types/queried-data/actions.d.ts +1 -1
  87. package/build-types/queried-data/actions.d.ts.map +1 -1
  88. package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
  89. package/build-types/queried-data/reducer.d.ts +1 -1
  90. package/build-types/queried-data/reducer.d.ts.map +1 -1
  91. package/build-types/queried-data/selectors.d.ts +0 -1
  92. package/build-types/queried-data/selectors.d.ts.map +1 -1
  93. package/build-types/reducer.d.ts +13 -13
  94. package/build-types/reducer.d.ts.map +1 -1
  95. package/build-types/resolvers.d.ts +3 -2
  96. package/build-types/resolvers.d.ts.map +1 -1
  97. package/build-types/selectors.d.ts +11 -6
  98. package/build-types/selectors.d.ts.map +1 -1
  99. package/build-types/utils/get-nested-value.d.ts.map +1 -1
  100. package/build-types/utils/get-normalized-comma-separable.d.ts.map +1 -1
  101. package/build-types/utils/if-matching-action.d.ts +1 -1
  102. package/build-types/utils/index.d.ts +1 -0
  103. package/build-types/utils/on-sub-key.d.ts +1 -1
  104. package/build-types/utils/replace-action.d.ts +1 -1
  105. package/build-types/utils/set-nested-value.d.ts.map +1 -1
  106. package/build-types/utils/user-permissions.d.ts +4 -0
  107. package/build-types/utils/user-permissions.d.ts.map +1 -0
  108. package/package.json +18 -17
  109. package/src/actions.js +6 -6
  110. package/src/entity-context.js +6 -0
  111. package/src/entity-provider.js +2 -211
  112. package/src/entity-types/menu-location.ts +1 -0
  113. package/src/entity-types/settings.ts +1 -0
  114. package/src/entity-types/theme.ts +1 -0
  115. package/src/entity-types/widget-type.ts +1 -0
  116. package/src/hooks/index.ts +3 -0
  117. package/src/hooks/test/use-entity-record.js +5 -3
  118. package/src/hooks/test/use-resource-permissions.js +96 -5
  119. package/src/hooks/use-entity-block-editor.js +148 -0
  120. package/src/hooks/use-entity-id.js +21 -0
  121. package/src/hooks/use-entity-prop.js +60 -0
  122. package/src/hooks/use-resource-permissions.ts +46 -9
  123. package/src/resolvers.js +102 -78
  124. package/src/selectors.ts +24 -9
  125. package/src/test/entity-provider.js +6 -2
  126. package/src/test/resolvers.js +221 -50
  127. package/src/test/selectors.js +18 -55
  128. package/src/utils/index.js +5 -0
  129. package/src/utils/user-permissions.js +39 -0
  130. package/tsconfig.json +2 -2
  131. package/tsconfig.tsbuildinfo +1 -1
package/src/selectors.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  isRawAttribute,
21
21
  setNestedValue,
22
22
  isNumericID,
23
+ getUserPermissionCacheKey,
23
24
  } from './utils';
24
25
  import type * as ET from './entity-types';
25
26
  import type { UndoManager } from '@wordpress/undo-manager';
@@ -120,6 +121,8 @@ type EntityRecordArgs =
120
121
  | [ string, string, EntityRecordKey ]
121
122
  | [ string, string, EntityRecordKey, GetRecordsHttpQuery ];
122
123
 
124
+ type EntityResource = { kind: string; name: string; id?: EntityRecordKey };
125
+
123
126
  /**
124
127
  * Shared reference to an empty object for cases where it is important to avoid
125
128
  * returning a new object reference on every invocation, as in a connected or
@@ -232,7 +235,9 @@ export function getEntitiesByKind( state: State, kind: string ): Array< any > {
232
235
  export const getEntitiesConfig = createSelector(
233
236
  ( state: State, kind: string ): Array< any > =>
234
237
  state.entities.config.filter( ( entity ) => entity.kind === kind ),
238
+ /* eslint-disable @typescript-eslint/no-unused-vars */
235
239
  ( state: State, kind: string ) => state.entities.config
240
+ /* eslint-enable @typescript-eslint/no-unused-vars */
236
241
  );
237
242
  /**
238
243
  * Returns the entity config given its kind and name.
@@ -992,6 +997,7 @@ export function getLastEntityDeleteError(
992
997
  ?.error;
993
998
  }
994
999
 
1000
+ /* eslint-disable @typescript-eslint/no-unused-vars */
995
1001
  /**
996
1002
  * Returns the previous edit from the current undo offset
997
1003
  * for the entity records edits history, if any.
@@ -1008,7 +1014,9 @@ export function getUndoEdit( state: State ): Optional< any > {
1008
1014
  } );
1009
1015
  return undefined;
1010
1016
  }
1017
+ /* eslint-enable @typescript-eslint/no-unused-vars */
1011
1018
 
1019
+ /* eslint-disable @typescript-eslint/no-unused-vars */
1012
1020
  /**
1013
1021
  * Returns the next edit from the current undo offset
1014
1022
  * for the entity records edits history, if any.
@@ -1025,6 +1033,7 @@ export function getRedoEdit( state: State ): Optional< any > {
1025
1033
  } );
1026
1034
  return undefined;
1027
1035
  }
1036
+ /* eslint-enable @typescript-eslint/no-unused-vars */
1028
1037
 
1029
1038
  /**
1030
1039
  * Returns true if there is a previous edit from the current undo offset
@@ -1130,7 +1139,8 @@ export function isPreviewEmbedFallback( state: State, url: string ): boolean {
1130
1139
  *
1131
1140
  * @param state Data state.
1132
1141
  * @param action Action to check. One of: 'create', 'read', 'update', 'delete'.
1133
- * @param resource REST resource to check, e.g. 'media' or 'posts'.
1142
+ * @param resource Entity resource to check. Accepts entity object `{ kind: 'root', name: 'media', id: 1 }`
1143
+ * or REST base as a string - `media`.
1134
1144
  * @param id Optional ID of the rest resource to check.
1135
1145
  *
1136
1146
  * @return Whether or not the user can perform the action,
@@ -1139,10 +1149,16 @@ export function isPreviewEmbedFallback( state: State, url: string ): boolean {
1139
1149
  export function canUser(
1140
1150
  state: State,
1141
1151
  action: string,
1142
- resource: string,
1152
+ resource: string | EntityResource,
1143
1153
  id?: EntityRecordKey
1144
1154
  ): boolean | undefined {
1145
- const key = [ action, resource, id ].filter( Boolean ).join( '/' );
1155
+ const isEntity = typeof resource === 'object';
1156
+ if ( isEntity && ( ! resource.kind || ! resource.name ) ) {
1157
+ return false;
1158
+ }
1159
+
1160
+ const key = getUserPermissionCacheKey( action, resource, id );
1161
+
1146
1162
  return state.userPermissions[ key ];
1147
1163
  }
1148
1164
 
@@ -1167,13 +1183,12 @@ export function canUserEditEntityRecord(
1167
1183
  name: string,
1168
1184
  recordId: EntityRecordKey
1169
1185
  ): boolean | undefined {
1170
- const entityConfig = getEntityConfig( state, kind, name );
1171
- if ( ! entityConfig ) {
1172
- return false;
1173
- }
1174
- const resource = entityConfig.__unstable_rest_base;
1186
+ deprecated( `wp.data.select( 'core' ).canUserEditEntityRecord()`, {
1187
+ since: '6.7',
1188
+ alternative: `wp.data.select( 'core' ).canUser( 'update', { kind, name, id } )`,
1189
+ } );
1175
1190
 
1176
- return canUser( state, 'update', resource, recordId );
1191
+ return canUser( state, 'update', { kind, name, id: recordId } );
1177
1192
  }
1178
1193
 
1179
1194
  /**
@@ -14,13 +14,14 @@ import {
14
14
  } from '@wordpress/blocks';
15
15
  import { RichText, useBlockProps } from '@wordpress/block-editor';
16
16
  import { createRegistry, RegistryProvider } from '@wordpress/data';
17
- import '@wordpress/block-library';
17
+ import { registerCoreBlocks } from '@wordpress/block-library';
18
+ import { unregisterFormatType } from '@wordpress/rich-text';
18
19
 
19
20
  /**
20
21
  * Internal dependencies
21
22
  */
22
23
  import { store as coreDataStore } from '../index';
23
- import { useEntityBlockEditor } from '../entity-provider';
24
+ import useEntityBlockEditor from '../hooks/use-entity-block-editor';
24
25
 
25
26
  const postTypeConfig = {
26
27
  kind: 'postType',
@@ -137,12 +138,15 @@ describe( 'useEntityBlockEditor', () => {
137
138
  title: 'block title',
138
139
  edit,
139
140
  } );
141
+
142
+ registerCoreBlocks();
140
143
  } );
141
144
 
142
145
  afterEach( () => {
143
146
  getBlockTypes().forEach( ( block ) => {
144
147
  unregisterBlockType( block.name );
145
148
  } );
149
+ unregisterFormatType( 'core/footnote' );
146
150
  } );
147
151
 
148
152
  it( 'does not mutate block attributes that include an array of strings or null values', async () => {
@@ -19,6 +19,7 @@ import {
19
19
 
20
20
  describe( 'getEntityRecord', () => {
21
21
  const POST_TYPE = { slug: 'post' };
22
+ const POST_TYPE_RESPONSE = { json: () => Promise.resolve( POST_TYPE ) };
22
23
  const ENTITIES = [
23
24
  {
24
25
  name: 'postType',
@@ -27,28 +28,37 @@ describe( 'getEntityRecord', () => {
27
28
  baseURLParams: { context: 'edit' },
28
29
  },
29
30
  ];
31
+ const registry = { batch: ( callback ) => callback() };
30
32
 
33
+ let dispatch;
31
34
  beforeEach( async () => {
32
- triggerFetch.mockReset();
33
- } );
34
-
35
- it( 'yields with requested post type', async () => {
36
- const dispatch = Object.assign( jest.fn(), {
35
+ dispatch = Object.assign( jest.fn(), {
37
36
  receiveEntityRecords: jest.fn(),
38
37
  __unstableAcquireStoreLock: jest.fn(),
39
38
  __unstableReleaseStoreLock: jest.fn(),
39
+ receiveUserPermission: jest.fn(),
40
+ finishResolution: jest.fn(),
40
41
  } );
42
+ triggerFetch.mockReset();
43
+ } );
44
+
45
+ it( 'yields with requested post type', async () => {
41
46
  // Provide entities
42
47
  dispatch.mockReturnValueOnce( ENTITIES );
43
48
 
44
49
  // Provide response
45
- triggerFetch.mockImplementation( () => POST_TYPE );
50
+ triggerFetch.mockImplementation( () => POST_TYPE_RESPONSE );
46
51
 
47
- await getEntityRecord( 'root', 'postType', 'post' )( { dispatch } );
52
+ await getEntityRecord(
53
+ 'root',
54
+ 'postType',
55
+ 'post'
56
+ )( { dispatch, registry } );
48
57
 
49
58
  // Fetch request should have been issued.
50
59
  expect( triggerFetch ).toHaveBeenCalledWith( {
51
60
  path: '/wp/v2/types/post?context=edit',
61
+ parse: false,
52
62
  } );
53
63
 
54
64
  // The record should have been received.
@@ -75,24 +85,18 @@ describe( 'getEntityRecord', () => {
75
85
  const select = {
76
86
  hasEntityRecords: jest.fn( () => {} ),
77
87
  };
78
-
79
- const dispatch = Object.assign( jest.fn(), {
80
- receiveEntityRecords: jest.fn(),
81
- __unstableAcquireStoreLock: jest.fn(),
82
- __unstableReleaseStoreLock: jest.fn(),
83
- } );
84
88
  // Provide entities
85
89
  dispatch.mockReturnValueOnce( ENTITIES );
86
90
 
87
91
  // Provide response
88
- triggerFetch.mockImplementation( () => POST_TYPE );
92
+ triggerFetch.mockImplementation( () => POST_TYPE_RESPONSE );
89
93
 
90
94
  await getEntityRecord(
91
95
  'root',
92
96
  'postType',
93
97
  'post',
94
98
  query
95
- )( { dispatch, select } );
99
+ )( { dispatch, select, registry } );
96
100
 
97
101
  // Check resolution cache for an existing entity that fulfills the request with query.
98
102
  expect( select.hasEntityRecords ).toHaveBeenCalledWith(
@@ -104,6 +108,7 @@ describe( 'getEntityRecord', () => {
104
108
  // Trigger apiFetch, test that the query is present in the url.
105
109
  expect( triggerFetch ).toHaveBeenCalledWith( {
106
110
  path: '/wp/v2/types/post?context=view&_envelope=1',
111
+ parse: false,
107
112
  } );
108
113
 
109
114
  // The record should have been received.
@@ -211,10 +216,12 @@ describe( 'getEntityRecords', () => {
211
216
  } );
212
217
 
213
218
  it( 'marks specific entity records as resolved', async () => {
219
+ const finishResolutions = jest.fn();
214
220
  const dispatch = Object.assign( jest.fn(), {
215
221
  receiveEntityRecords: jest.fn(),
216
222
  __unstableAcquireStoreLock: jest.fn(),
217
223
  __unstableReleaseStoreLock: jest.fn(),
224
+ finishResolutions,
218
225
  } );
219
226
  // Provide entities
220
227
  dispatch.mockReturnValueOnce( ENTITIES );
@@ -230,16 +237,9 @@ describe( 'getEntityRecords', () => {
230
237
  } );
231
238
 
232
239
  // The record should have been received.
233
- expect( dispatch ).toHaveBeenCalledWith( {
234
- type: 'START_RESOLUTIONS',
235
- selectorName: 'getEntityRecord',
236
- args: [ [ ENTITIES[ 1 ].kind, ENTITIES[ 1 ].name, 2 ] ],
237
- } );
238
- expect( dispatch ).toHaveBeenCalledWith( {
239
- type: 'FINISH_RESOLUTIONS',
240
- selectorName: 'getEntityRecord',
241
- args: [ [ ENTITIES[ 1 ].kind, ENTITIES[ 1 ].name, 2 ] ],
242
- } );
240
+ expect( finishResolutions ).toHaveBeenCalledWith( 'getEntityRecord', [
241
+ [ ENTITIES[ 1 ].kind, ENTITIES[ 1 ].name, 2 ],
242
+ ] );
243
243
  } );
244
244
  } );
245
245
 
@@ -283,26 +283,47 @@ describe( 'getEmbedPreview', () => {
283
283
  } );
284
284
 
285
285
  describe( 'canUser', () => {
286
- let registry;
286
+ const ENTITIES = [
287
+ {
288
+ name: 'media',
289
+ kind: 'root',
290
+ baseURL: '/wp/v2/media',
291
+ baseURLParams: { context: 'edit' },
292
+ },
293
+ {
294
+ name: 'wp_block',
295
+ kind: 'postType',
296
+ baseURL: '/wp/v2/blocks',
297
+ baseURLParams: { context: 'edit' },
298
+ },
299
+ ];
300
+
301
+ let dispatch, registry;
287
302
  beforeEach( async () => {
288
303
  registry = {
289
304
  select: jest.fn( () => ( {
290
305
  hasStartedResolution: () => false,
291
306
  } ) ),
307
+ batch: ( callback ) => callback(),
292
308
  };
309
+ dispatch = Object.assign( jest.fn(), {
310
+ receiveUserPermission: jest.fn(),
311
+ finishResolution: jest.fn(),
312
+ } );
313
+ dispatch.mockReturnValue( ENTITIES );
293
314
  triggerFetch.mockReset();
294
315
  } );
295
316
 
296
317
  it( 'does nothing when there is an API error', async () => {
297
- const dispatch = Object.assign( jest.fn(), {
298
- receiveUserPermission: jest.fn(),
299
- } );
300
-
301
318
  triggerFetch.mockImplementation( () =>
302
319
  Promise.reject( { status: 404 } )
303
320
  );
304
321
 
305
322
  await canUser( 'create', 'media' )( { dispatch, registry } );
323
+ await canUser( 'create', { kind: 'root', name: 'media' } )( {
324
+ dispatch,
325
+ registry,
326
+ } );
306
327
 
307
328
  expect( triggerFetch ).toHaveBeenCalledWith( {
308
329
  path: '/wp/v2/media',
@@ -313,11 +334,16 @@ describe( 'canUser', () => {
313
334
  expect( dispatch.receiveUserPermission ).not.toHaveBeenCalled();
314
335
  } );
315
336
 
316
- it( 'receives false when the user is not allowed to perform an action', async () => {
317
- const dispatch = Object.assign( jest.fn(), {
318
- receiveUserPermission: jest.fn(),
319
- } );
337
+ it( 'throws an error when an entity resource object is malformed', async () => {
338
+ await expect(
339
+ canUser( 'create', { name: 'wp_block' } )( {
340
+ dispatch,
341
+ registry,
342
+ } )
343
+ ).rejects.toThrow( 'The entity resource object is not valid.' );
344
+ } );
320
345
 
346
+ it( 'receives false when the user is not allowed to perform an action', async () => {
321
347
  triggerFetch.mockImplementation( () => ( {
322
348
  headers: new Map( [ [ 'allow', 'GET' ] ] ),
323
349
  } ) );
@@ -336,11 +362,29 @@ describe( 'canUser', () => {
336
362
  );
337
363
  } );
338
364
 
339
- it( 'receives true when the user is allowed to perform an action', async () => {
340
- const dispatch = Object.assign( jest.fn(), {
341
- receiveUserPermission: jest.fn(),
365
+ it( 'receives false when the user is not allowed to perform an action on entities', async () => {
366
+ triggerFetch.mockImplementation( () => ( {
367
+ headers: new Map( [ [ 'allow', 'GET' ] ] ),
368
+ } ) );
369
+
370
+ await canUser( 'create', { kind: 'root', name: 'media' } )( {
371
+ dispatch,
372
+ registry,
373
+ } );
374
+
375
+ expect( triggerFetch ).toHaveBeenCalledWith( {
376
+ path: '/wp/v2/media',
377
+ method: 'OPTIONS',
378
+ parse: false,
342
379
  } );
343
380
 
381
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
382
+ 'create/root/media',
383
+ false
384
+ );
385
+ } );
386
+
387
+ it( 'receives true when the user is allowed to perform an action', async () => {
344
388
  triggerFetch.mockImplementation( () => ( {
345
389
  headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
346
390
  } ) );
@@ -359,11 +403,29 @@ describe( 'canUser', () => {
359
403
  );
360
404
  } );
361
405
 
362
- it( 'receives true when the user is allowed to perform an action on a specific resource', async () => {
363
- const dispatch = Object.assign( jest.fn(), {
364
- receiveUserPermission: jest.fn(),
406
+ it( 'receives true when the user is allowed to perform an action on entities', async () => {
407
+ triggerFetch.mockImplementation( () => ( {
408
+ headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
409
+ } ) );
410
+
411
+ await canUser( 'create', { kind: 'root', name: 'media' } )( {
412
+ dispatch,
413
+ registry,
414
+ } );
415
+
416
+ expect( triggerFetch ).toHaveBeenCalledWith( {
417
+ path: '/wp/v2/media',
418
+ method: 'OPTIONS',
419
+ parse: false,
365
420
  } );
366
421
 
422
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
423
+ 'create/root/media',
424
+ true
425
+ );
426
+ } );
427
+
428
+ it( 'receives true when the user is allowed to perform an action on a specific resource', async () => {
367
429
  triggerFetch.mockImplementation( () => ( {
368
430
  headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
369
431
  } ) );
@@ -382,12 +444,35 @@ describe( 'canUser', () => {
382
444
  );
383
445
  } );
384
446
 
385
- it( 'runs apiFetch only once per resource', async () => {
386
- const dispatch = Object.assign( jest.fn(), {
387
- receiveUserPermission: jest.fn(),
447
+ it( 'receives true when the user is allowed to perform an action on a specific entity', async () => {
448
+ triggerFetch.mockImplementation( () => ( {
449
+ headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
450
+ } ) );
451
+
452
+ await canUser( 'create', {
453
+ kind: 'postType',
454
+ name: 'wp_block',
455
+ id: 123,
456
+ } )( {
457
+ dispatch,
458
+ registry,
459
+ } );
460
+
461
+ expect( triggerFetch ).toHaveBeenCalledWith( {
462
+ path: '/wp/v2/blocks/123',
463
+ method: 'OPTIONS',
464
+ parse: false,
388
465
  } );
389
466
 
467
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
468
+ 'create/postType/wp_block/123',
469
+ true
470
+ );
471
+ } );
472
+
473
+ it( 'runs apiFetch only once per resource', async () => {
390
474
  registry = {
475
+ ...registry,
391
476
  select: () => ( {
392
477
  hasStartedResolution: ( _, [ action ] ) => action === 'read',
393
478
  } ),
@@ -412,12 +497,48 @@ describe( 'canUser', () => {
412
497
  );
413
498
  } );
414
499
 
415
- it( 'retrieves all permissions even when ID is not given', async () => {
416
- const dispatch = Object.assign( jest.fn(), {
417
- receiveUserPermission: jest.fn(),
500
+ it( 'runs apiFetch only once per entity', async () => {
501
+ registry = {
502
+ ...registry,
503
+ select: () => ( {
504
+ hasStartedResolution: ( _, [ action ] ) => action === 'read',
505
+ } ),
506
+ };
507
+
508
+ triggerFetch.mockImplementation( () => ( {
509
+ headers: new Map( [ [ 'allow', 'POST, GET' ] ] ),
510
+ } ) );
511
+
512
+ await canUser( 'create', {
513
+ kind: 'postType',
514
+ name: 'wp_block',
515
+ } )( {
516
+ dispatch,
517
+ registry,
518
+ } );
519
+ await canUser( 'read', {
520
+ kind: 'postType',
521
+ name: 'wp_block',
522
+ } )( {
523
+ dispatch,
524
+ registry,
418
525
  } );
419
526
 
527
+ expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
528
+
529
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
530
+ 'create/postType/wp_block',
531
+ true
532
+ );
533
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
534
+ 'read/postType/wp_block',
535
+ true
536
+ );
537
+ } );
538
+
539
+ it( 'retrieves all permissions even when ID is not given', async () => {
420
540
  registry = {
541
+ ...registry,
421
542
  select: () => ( {
422
543
  hasStartedResolution: ( _, [ action ] ) => action === 'read',
423
544
  } ),
@@ -451,11 +572,8 @@ describe( 'canUser', () => {
451
572
  } );
452
573
 
453
574
  it( 'runs apiFetch only once per resource ID', async () => {
454
- const dispatch = Object.assign( jest.fn(), {
455
- receiveUserPermission: jest.fn(),
456
- } );
457
-
458
575
  registry = {
576
+ ...registry,
459
577
  select: () => ( {
460
578
  hasStartedResolution: ( _, [ action ] ) => action === 'create',
461
579
  } ),
@@ -489,6 +607,59 @@ describe( 'canUser', () => {
489
607
  true
490
608
  );
491
609
  } );
610
+
611
+ it( 'runs apiFetch only once per entity ID', async () => {
612
+ registry = {
613
+ ...registry,
614
+ select: () => ( {
615
+ hasStartedResolution: ( _, [ action ] ) => action === 'create',
616
+ } ),
617
+ };
618
+
619
+ triggerFetch.mockImplementation( () => ( {
620
+ headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
621
+ } ) );
622
+
623
+ await canUser( 'create', {
624
+ kind: 'postType',
625
+ name: 'wp_block',
626
+ id: 123,
627
+ } )( { dispatch, registry } );
628
+ await canUser( 'read', {
629
+ kind: 'postType',
630
+ name: 'wp_block',
631
+ id: 123,
632
+ } )( { dispatch, registry } );
633
+ await canUser( 'update', {
634
+ kind: 'postType',
635
+ name: 'wp_block',
636
+ id: 123,
637
+ } )( { dispatch, registry } );
638
+ await canUser( 'delete', {
639
+ kind: 'postType',
640
+ name: 'wp_block',
641
+ id: 123,
642
+ } )( { dispatch, registry } );
643
+
644
+ expect( triggerFetch ).toHaveBeenCalledTimes( 1 );
645
+
646
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
647
+ 'create/postType/wp_block/123',
648
+ true
649
+ );
650
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
651
+ 'read/postType/wp_block/123',
652
+ true
653
+ );
654
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
655
+ 'update/postType/wp_block/123',
656
+ true
657
+ );
658
+ expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
659
+ 'delete/postType/wp_block/123',
660
+ true
661
+ );
662
+ } );
492
663
  } );
493
664
 
494
665
  describe( 'getAutosaves', () => {
@@ -18,7 +18,6 @@ import {
18
18
  getEmbedPreview,
19
19
  isPreviewEmbedFallback,
20
20
  canUser,
21
- canUserEditEntityRecord,
22
21
  getAutosave,
23
22
  getAutosaves,
24
23
  getCurrentUser,
@@ -690,79 +689,43 @@ describe( 'canUser', () => {
690
689
  userPermissions: {},
691
690
  } );
692
691
  expect( canUser( state, 'create', 'media' ) ).toBe( undefined );
692
+ expect(
693
+ canUser( state, 'create', { kind: 'root', name: 'media' } )
694
+ ).toBe( undefined );
695
+ } );
696
+
697
+ it( 'returns null when entity kind or name is missing', () => {
698
+ const state = deepFreeze( {
699
+ userPermissions: {},
700
+ } );
701
+ expect( canUser( state, 'create', { name: 'media' } ) ).toBe( false );
702
+ expect( canUser( state, 'create', { kind: 'root' } ) ).toBe( false );
693
703
  } );
694
704
 
695
705
  it( 'returns whether an action can be performed', () => {
696
706
  const state = deepFreeze( {
697
707
  userPermissions: {
698
708
  'create/media': false,
709
+ 'create/root/media': false,
699
710
  },
700
711
  } );
701
712
  expect( canUser( state, 'create', 'media' ) ).toBe( false );
713
+ expect(
714
+ canUser( state, 'create', { kind: 'root', name: 'media' } )
715
+ ).toBe( false );
702
716
  } );
703
717
 
704
718
  it( 'returns whether an action can be performed for a given resource', () => {
705
719
  const state = deepFreeze( {
706
720
  userPermissions: {
707
721
  'create/media/123': false,
722
+ 'create/root/media/123': false,
708
723
  },
709
724
  } );
710
725
  expect( canUser( state, 'create', 'media', 123 ) ).toBe( false );
711
- } );
712
- } );
713
-
714
- describe( 'canUserEditEntityRecord', () => {
715
- it( 'returns false by default', () => {
716
- const state = deepFreeze( {
717
- userPermissions: {},
718
- entities: { records: {} },
719
- } );
720
- expect( canUserEditEntityRecord( state, 'postType', 'post' ) ).toBe(
721
- false
722
- );
723
- } );
724
-
725
- it( 'returns whether the user can edit', () => {
726
- const state = deepFreeze( {
727
- userPermissions: {
728
- 'create/posts': false,
729
- 'update/posts/1': true,
730
- },
731
- entities: {
732
- config: [
733
- {
734
- kind: 'postType',
735
- name: 'post',
736
- __unstable_rest_base: 'posts',
737
- },
738
- ],
739
- records: {
740
- root: {
741
- postType: {
742
- queriedData: {
743
- items: {
744
- default: {
745
- post: {
746
- slug: 'post',
747
- __unstable: 'posts',
748
- },
749
- },
750
- },
751
- itemIsComplete: {
752
- default: {
753
- post: true,
754
- },
755
- },
756
- queries: {},
757
- },
758
- },
759
- },
760
- },
761
- },
762
- } );
763
726
  expect(
764
- canUserEditEntityRecord( state, 'postType', 'post', '1' )
765
- ).toBe( true );
727
+ canUser( state, 'create', { kind: 'root', name: 'media', id: 123 } )
728
+ ).toBe( false );
766
729
  } );
767
730
  } );
768
731
 
@@ -9,3 +9,8 @@ export { default as isRawAttribute } from './is-raw-attribute';
9
9
  export { default as setNestedValue } from './set-nested-value';
10
10
  export { default as getNestedValue } from './get-nested-value';
11
11
  export { default as isNumericID } from './is-numeric-id';
12
+ export {
13
+ getUserPermissionCacheKey,
14
+ getUserPermissionsFromResponse,
15
+ ALLOWED_RESOURCE_ACTIONS,
16
+ } from './user-permissions';