@wordpress/core-data 7.3.0 → 7.5.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 (153) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +54 -6
  3. package/build/actions.js +1 -1
  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/footnotes/get-rich-text-values-cached.js +2 -2
  10. package/build/footnotes/get-rich-text-values-cached.js.map +1 -1
  11. package/build/hooks/index.js +22 -0
  12. package/build/hooks/index.js.map +1 -1
  13. package/build/hooks/use-entity-block-editor.js +140 -0
  14. package/build/hooks/use-entity-block-editor.js.map +1 -0
  15. package/build/hooks/use-entity-id.js +28 -0
  16. package/build/hooks/use-entity-id.js.map +1 -0
  17. package/build/hooks/use-entity-prop.js +65 -0
  18. package/build/hooks/use-entity-prop.js.map +1 -0
  19. package/build/hooks/use-entity-records.js +39 -0
  20. package/build/hooks/use-entity-records.js.map +1 -1
  21. package/build/hooks/use-resource-permissions.js +25 -8
  22. package/build/hooks/use-resource-permissions.js.map +1 -1
  23. package/build/index.js +14 -2
  24. package/build/index.js.map +1 -1
  25. package/build/lock-unlock.js +18 -0
  26. package/build/lock-unlock.js.map +1 -0
  27. package/build/private-apis.js +8 -9
  28. package/build/private-apis.js.map +1 -1
  29. package/build/private-selectors.js +35 -0
  30. package/build/private-selectors.js.map +1 -1
  31. package/build/resolvers.js +64 -60
  32. package/build/resolvers.js.map +1 -1
  33. package/build/selectors.js +16 -8
  34. package/build/selectors.js.map +1 -1
  35. package/build/utils/index.js +19 -0
  36. package/build/utils/index.js.map +1 -1
  37. package/build/utils/user-permissions.js +32 -0
  38. package/build/utils/user-permissions.js.map +1 -0
  39. package/build-module/actions.js +1 -1
  40. package/build-module/actions.js.map +1 -1
  41. package/build-module/entity-context.js +6 -0
  42. package/build-module/entity-context.js.map +1 -0
  43. package/build-module/entity-provider.js +3 -183
  44. package/build-module/entity-provider.js.map +1 -1
  45. package/build-module/footnotes/get-rich-text-values-cached.js +1 -1
  46. package/build-module/footnotes/get-rich-text-values-cached.js.map +1 -1
  47. package/build-module/hooks/index.js +3 -0
  48. package/build-module/hooks/index.js.map +1 -1
  49. package/build-module/hooks/use-entity-block-editor.js +132 -0
  50. package/build-module/hooks/use-entity-block-editor.js.map +1 -0
  51. package/build-module/hooks/use-entity-id.js +22 -0
  52. package/build-module/hooks/use-entity-id.js.map +1 -0
  53. package/build-module/hooks/use-entity-prop.js +58 -0
  54. package/build-module/hooks/use-entity-prop.js.map +1 -0
  55. package/build-module/hooks/use-entity-records.js +38 -0
  56. package/build-module/hooks/use-entity-records.js.map +1 -1
  57. package/build-module/hooks/use-resource-permissions.js +25 -8
  58. package/build-module/hooks/use-resource-permissions.js.map +1 -1
  59. package/build-module/index.js +2 -1
  60. package/build-module/index.js.map +1 -1
  61. package/build-module/lock-unlock.js +9 -0
  62. package/build-module/lock-unlock.js.map +1 -0
  63. package/build-module/private-apis.js +7 -6
  64. package/build-module/private-apis.js.map +1 -1
  65. package/build-module/private-selectors.js +33 -0
  66. package/build-module/private-selectors.js.map +1 -1
  67. package/build-module/resolvers.js +65 -61
  68. package/build-module/resolvers.js.map +1 -1
  69. package/build-module/selectors.js +17 -9
  70. package/build-module/selectors.js.map +1 -1
  71. package/build-module/utils/index.js +1 -0
  72. package/build-module/utils/index.js.map +1 -1
  73. package/build-module/utils/user-permissions.js +24 -0
  74. package/build-module/utils/user-permissions.js.map +1 -0
  75. package/build-types/actions.d.ts +3 -3
  76. package/build-types/actions.d.ts.map +1 -1
  77. package/build-types/batch/create-batch.d.ts.map +1 -1
  78. package/build-types/entities.d.ts.map +1 -1
  79. package/build-types/entity-context.d.ts +2 -0
  80. package/build-types/entity-context.d.ts.map +1 -0
  81. package/build-types/entity-provider.d.ts +0 -47
  82. package/build-types/entity-provider.d.ts.map +1 -1
  83. package/build-types/fetch/__experimental-fetch-url-data.d.ts.map +1 -1
  84. package/build-types/hooks/index.d.ts +3 -0
  85. package/build-types/hooks/index.d.ts.map +1 -1
  86. package/build-types/hooks/use-entity-block-editor.d.ts +22 -0
  87. package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -0
  88. package/build-types/hooks/use-entity-id.d.ts +9 -0
  89. package/build-types/hooks/use-entity-id.d.ts.map +1 -0
  90. package/build-types/hooks/use-entity-prop.d.ts +19 -0
  91. package/build-types/hooks/use-entity-prop.d.ts.map +1 -0
  92. package/build-types/hooks/use-entity-records.d.ts +1 -0
  93. package/build-types/hooks/use-entity-records.d.ts.map +1 -1
  94. package/build-types/hooks/use-resource-permissions.d.ts +8 -70
  95. package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
  96. package/build-types/index.d.ts +36 -32
  97. package/build-types/index.d.ts.map +1 -1
  98. package/build-types/lock-unlock.d.ts +3 -0
  99. package/build-types/lock-unlock.d.ts.map +1 -0
  100. package/build-types/locks/reducer.d.ts +1 -1
  101. package/build-types/locks/reducer.d.ts.map +1 -1
  102. package/build-types/private-apis.d.ts +1 -2
  103. package/build-types/private-apis.d.ts.map +1 -1
  104. package/build-types/private-selectors.d.ts +15 -0
  105. package/build-types/private-selectors.d.ts.map +1 -1
  106. package/build-types/queried-data/actions.d.ts +1 -1
  107. package/build-types/queried-data/actions.d.ts.map +1 -1
  108. package/build-types/queried-data/get-query-parts.d.ts.map +1 -1
  109. package/build-types/queried-data/reducer.d.ts +1 -1
  110. package/build-types/queried-data/reducer.d.ts.map +1 -1
  111. package/build-types/queried-data/selectors.d.ts +0 -1
  112. package/build-types/queried-data/selectors.d.ts.map +1 -1
  113. package/build-types/reducer.d.ts +13 -13
  114. package/build-types/reducer.d.ts.map +1 -1
  115. package/build-types/resolvers.d.ts +3 -2
  116. package/build-types/resolvers.d.ts.map +1 -1
  117. package/build-types/selectors.d.ts +11 -6
  118. package/build-types/selectors.d.ts.map +1 -1
  119. package/build-types/utils/get-nested-value.d.ts.map +1 -1
  120. package/build-types/utils/get-normalized-comma-separable.d.ts.map +1 -1
  121. package/build-types/utils/if-matching-action.d.ts +1 -1
  122. package/build-types/utils/index.d.ts +1 -0
  123. package/build-types/utils/on-sub-key.d.ts +1 -1
  124. package/build-types/utils/replace-action.d.ts +1 -1
  125. package/build-types/utils/set-nested-value.d.ts.map +1 -1
  126. package/build-types/utils/user-permissions.d.ts +4 -0
  127. package/build-types/utils/user-permissions.d.ts.map +1 -0
  128. package/package.json +18 -17
  129. package/src/actions.js +1 -1
  130. package/src/entity-context.js +6 -0
  131. package/src/entity-provider.js +2 -209
  132. package/src/footnotes/get-rich-text-values-cached.js +1 -1
  133. package/src/hooks/index.ts +3 -0
  134. package/src/hooks/test/use-entity-record.js +5 -3
  135. package/src/hooks/test/use-resource-permissions.js +96 -5
  136. package/src/hooks/use-entity-block-editor.js +148 -0
  137. package/src/hooks/use-entity-id.js +21 -0
  138. package/src/hooks/use-entity-prop.js +60 -0
  139. package/src/hooks/use-entity-records.ts +50 -0
  140. package/src/hooks/use-resource-permissions.ts +46 -9
  141. package/src/index.js +2 -1
  142. package/src/lock-unlock.js +10 -0
  143. package/src/private-apis.js +7 -7
  144. package/src/private-selectors.ts +43 -0
  145. package/src/resolvers.js +85 -67
  146. package/src/selectors.ts +18 -9
  147. package/src/test/entity-provider.js +6 -2
  148. package/src/test/resolvers.js +217 -50
  149. package/src/test/selectors.js +18 -55
  150. package/src/utils/index.js +5 -0
  151. package/src/utils/user-permissions.js +39 -0
  152. package/tsconfig.json +2 -1
  153. package/tsconfig.tsbuildinfo +1 -1
@@ -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,7 +283,22 @@ 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( () => ( {
@@ -291,19 +306,24 @@ describe( 'canUser', () => {
291
306
  } ) ),
292
307
  batch: ( callback ) => callback(),
293
308
  };
309
+ dispatch = Object.assign( jest.fn(), {
310
+ receiveUserPermission: jest.fn(),
311
+ finishResolution: jest.fn(),
312
+ } );
313
+ dispatch.mockReturnValue( ENTITIES );
294
314
  triggerFetch.mockReset();
295
315
  } );
296
316
 
297
317
  it( 'does nothing when there is an API error', async () => {
298
- const dispatch = Object.assign( jest.fn(), {
299
- receiveUserPermission: jest.fn(),
300
- } );
301
-
302
318
  triggerFetch.mockImplementation( () =>
303
319
  Promise.reject( { status: 404 } )
304
320
  );
305
321
 
306
322
  await canUser( 'create', 'media' )( { dispatch, registry } );
323
+ await canUser( 'create', { kind: 'root', name: 'media' } )( {
324
+ dispatch,
325
+ registry,
326
+ } );
307
327
 
308
328
  expect( triggerFetch ).toHaveBeenCalledWith( {
309
329
  path: '/wp/v2/media',
@@ -314,11 +334,16 @@ describe( 'canUser', () => {
314
334
  expect( dispatch.receiveUserPermission ).not.toHaveBeenCalled();
315
335
  } );
316
336
 
317
- it( 'receives false when the user is not allowed to perform an action', async () => {
318
- const dispatch = Object.assign( jest.fn(), {
319
- receiveUserPermission: jest.fn(),
320
- } );
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
+ } );
321
345
 
346
+ it( 'receives false when the user is not allowed to perform an action', async () => {
322
347
  triggerFetch.mockImplementation( () => ( {
323
348
  headers: new Map( [ [ 'allow', 'GET' ] ] ),
324
349
  } ) );
@@ -337,11 +362,29 @@ describe( 'canUser', () => {
337
362
  );
338
363
  } );
339
364
 
340
- it( 'receives true when the user is allowed to perform an action', async () => {
341
- const dispatch = Object.assign( jest.fn(), {
342
- 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,
343
379
  } );
344
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 () => {
345
388
  triggerFetch.mockImplementation( () => ( {
346
389
  headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
347
390
  } ) );
@@ -360,11 +403,29 @@ describe( 'canUser', () => {
360
403
  );
361
404
  } );
362
405
 
363
- it( 'receives true when the user is allowed to perform an action on a specific resource', async () => {
364
- const dispatch = Object.assign( jest.fn(), {
365
- 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,
366
420
  } );
367
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 () => {
368
429
  triggerFetch.mockImplementation( () => ( {
369
430
  headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
370
431
  } ) );
@@ -383,11 +444,33 @@ describe( 'canUser', () => {
383
444
  );
384
445
  } );
385
446
 
386
- it( 'runs apiFetch only once per resource', async () => {
387
- const dispatch = Object.assign( jest.fn(), {
388
- 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,
389
459
  } );
390
460
 
461
+ expect( triggerFetch ).toHaveBeenCalledWith( {
462
+ path: '/wp/v2/blocks/123',
463
+ method: 'OPTIONS',
464
+ parse: false,
465
+ } );
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 () => {
391
474
  registry = {
392
475
  ...registry,
393
476
  select: () => ( {
@@ -414,11 +497,46 @@ describe( 'canUser', () => {
414
497
  );
415
498
  } );
416
499
 
417
- it( 'retrieves all permissions even when ID is not given', async () => {
418
- const dispatch = Object.assign( jest.fn(), {
419
- 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,
420
525
  } );
421
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 () => {
422
540
  registry = {
423
541
  ...registry,
424
542
  select: () => ( {
@@ -454,10 +572,6 @@ describe( 'canUser', () => {
454
572
  } );
455
573
 
456
574
  it( 'runs apiFetch only once per resource ID', async () => {
457
- const dispatch = Object.assign( jest.fn(), {
458
- receiveUserPermission: jest.fn(),
459
- } );
460
-
461
575
  registry = {
462
576
  ...registry,
463
577
  select: () => ( {
@@ -493,6 +607,59 @@ describe( 'canUser', () => {
493
607
  true
494
608
  );
495
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
+ } );
496
663
  } );
497
664
 
498
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';
@@ -0,0 +1,39 @@
1
+ export const ALLOWED_RESOURCE_ACTIONS = [
2
+ 'create',
3
+ 'read',
4
+ 'update',
5
+ 'delete',
6
+ ];
7
+
8
+ export function getUserPermissionsFromResponse( response ) {
9
+ const permissions = {};
10
+
11
+ // Optional chaining operator is used here because the API requests don't
12
+ // return the expected result in the React native version. Instead, API requests
13
+ // only return the result, without including response properties like the headers.
14
+ const allowedMethods = response.headers?.get( 'allow' ) || '';
15
+
16
+ const methods = {
17
+ create: 'POST',
18
+ read: 'GET',
19
+ update: 'PUT',
20
+ delete: 'DELETE',
21
+ };
22
+ for ( const [ actionName, methodName ] of Object.entries( methods ) ) {
23
+ permissions[ actionName ] = allowedMethods.includes( methodName );
24
+ }
25
+
26
+ return permissions;
27
+ }
28
+
29
+ export function getUserPermissionCacheKey( action, resource, id ) {
30
+ const key = (
31
+ typeof resource === 'object'
32
+ ? [ action, resource.kind, resource.name, resource.id ]
33
+ : [ action, resource, id ]
34
+ )
35
+ .filter( Boolean )
36
+ .join( '/' );
37
+
38
+ return key;
39
+ }
package/tsconfig.json CHANGED
@@ -21,7 +21,8 @@
21
21
  { "path": "../rich-text" },
22
22
  { "path": "../sync" },
23
23
  { "path": "../undo-manager" },
24
- { "path": "../url" }
24
+ { "path": "../url" },
25
+ { "path": "../warning" }
25
26
  ],
26
27
  "include": [ "src/**/*" ]
27
28
  }