@wordpress/core-data 7.30.1-next.836ecdcae.0 → 7.31.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 (73) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +23 -1
  3. package/build/actions.js +32 -2
  4. package/build/actions.js.map +1 -1
  5. package/build/entities.js +1 -1
  6. package/build/entities.js.map +1 -1
  7. package/build/entity-types/user.js.map +1 -1
  8. package/build/hooks/index.js.map +1 -1
  9. package/build/hooks/use-entity-records.js.map +1 -1
  10. package/build/private-actions.js +8 -0
  11. package/build/private-actions.js.map +1 -1
  12. package/build/private-selectors.js +8 -1
  13. package/build/private-selectors.js.map +1 -1
  14. package/build/reducer.js +9 -1
  15. package/build/reducer.js.map +1 -1
  16. package/build/resolvers.js +80 -64
  17. package/build/resolvers.js.map +1 -1
  18. package/build/selectors.js +99 -41
  19. package/build/selectors.js.map +1 -1
  20. package/build-module/actions.js +32 -2
  21. package/build-module/actions.js.map +1 -1
  22. package/build-module/entities.js +1 -1
  23. package/build-module/entities.js.map +1 -1
  24. package/build-module/entity-types/user.js.map +1 -1
  25. package/build-module/hooks/index.js +8 -0
  26. package/build-module/hooks/index.js.map +1 -1
  27. package/build-module/hooks/use-entity-records.js.map +1 -1
  28. package/build-module/private-actions.js +7 -0
  29. package/build-module/private-actions.js.map +1 -1
  30. package/build-module/private-selectors.js +7 -1
  31. package/build-module/private-selectors.js.map +1 -1
  32. package/build-module/reducer.js +8 -1
  33. package/build-module/reducer.js.map +1 -1
  34. package/build-module/resolvers.js +76 -61
  35. package/build-module/resolvers.js.map +1 -1
  36. package/build-module/selectors.js +98 -41
  37. package/build-module/selectors.js.map +1 -1
  38. package/build-types/actions.d.ts.map +1 -1
  39. package/build-types/entity-types/user.d.ts +2 -2
  40. package/build-types/entity-types/user.d.ts.map +1 -1
  41. package/build-types/hooks/index.d.ts +8 -0
  42. package/build-types/hooks/index.d.ts.map +1 -1
  43. package/build-types/hooks/use-entity-records.d.ts +12 -2
  44. package/build-types/hooks/use-entity-records.d.ts.map +1 -1
  45. package/build-types/index.d.ts +2 -1
  46. package/build-types/index.d.ts.map +1 -1
  47. package/build-types/private-actions.d.ts +5 -0
  48. package/build-types/private-actions.d.ts.map +1 -1
  49. package/build-types/private-selectors.d.ts +1 -0
  50. package/build-types/private-selectors.d.ts.map +1 -1
  51. package/build-types/reducer.d.ts +3 -0
  52. package/build-types/reducer.d.ts.map +1 -1
  53. package/build-types/resolvers.d.ts +7 -0
  54. package/build-types/resolvers.d.ts.map +1 -1
  55. package/build-types/selectors.d.ts +18 -2
  56. package/build-types/selectors.d.ts.map +1 -1
  57. package/package.json +18 -18
  58. package/src/actions.js +43 -1
  59. package/src/entities.js +1 -1
  60. package/src/entity-types/user.ts +2 -2
  61. package/src/hooks/index.ts +9 -0
  62. package/src/hooks/use-entity-records.ts +12 -2
  63. package/src/private-actions.js +4 -0
  64. package/src/private-selectors.ts +10 -0
  65. package/src/reducer.js +7 -0
  66. package/src/resolvers.js +127 -81
  67. package/src/selectors.ts +110 -40
  68. package/src/test/actions.js +2 -2
  69. package/src/test/entities.js +32 -0
  70. package/src/test/resolvers.js +103 -12
  71. package/src/test/selectors.js +220 -13
  72. package/src/test/store.js +114 -0
  73. package/tsconfig.tsbuildinfo +1 -1
package/src/selectors.ts CHANGED
@@ -36,7 +36,7 @@ export interface State {
36
36
  blockPatternCategories: Array< unknown >;
37
37
  currentGlobalStylesId: string;
38
38
  currentTheme: string;
39
- currentUser: ET.User< 'edit' >;
39
+ currentUser: ET.User< 'view' >;
40
40
  embedPreviews: Record< string, { html: string } >;
41
41
  entities: EntitiesState;
42
42
  themeBaseGlobalStyles: Record< string, Object >;
@@ -49,6 +49,7 @@ export interface State {
49
49
  userPatternCategories: Array< UserPatternCategory >;
50
50
  defaultTemplates: Record< string, string >;
51
51
  registeredPostMeta: Record< string, Object >;
52
+ templateAutoDraftId: Record< string, number | null >;
52
53
  }
53
54
 
54
55
  type EntityRecordKey = string | number;
@@ -185,7 +186,7 @@ export function getAuthors(
185
186
  *
186
187
  * @return Current user object.
187
188
  */
188
- export function getCurrentUser( state: State ): ET.User< 'edit' > {
189
+ export function getCurrentUser( state: State ): ET.User< 'view' > {
189
190
  return state.currentUser;
190
191
  }
191
192
 
@@ -358,6 +359,18 @@ export const getEntityRecord = createSelector(
358
359
  ): EntityRecord | undefined => {
359
360
  logEntityDeprecation( kind, name, 'getEntityRecord' );
360
361
 
362
+ // For back-compat, we allow querying for static templates through
363
+ // wp_template.
364
+ if (
365
+ kind === 'postType' &&
366
+ name === 'wp_template' &&
367
+ typeof key === 'string' &&
368
+ // __experimentalGetDirtyEntityRecords always calls getEntityRecord
369
+ // with a string key, so we need that it's not a numeric ID.
370
+ ! /^\d+$/.test( key )
371
+ ) {
372
+ name = 'wp_registered_template';
373
+ }
361
374
  const queriedState =
362
375
  state.entities.records?.[ kind ]?.[ name ]?.queriedData;
363
376
  if ( ! queriedState ) {
@@ -365,7 +378,7 @@ export const getEntityRecord = createSelector(
365
378
  }
366
379
  const context = query?.context ?? 'default';
367
380
 
368
- if ( query === undefined ) {
381
+ if ( ! query || ! query._fields ) {
369
382
  // If expecting a complete item, validate that completeness.
370
383
  if ( ! queriedState.itemIsComplete[ context ]?.[ key ] ) {
371
384
  return undefined;
@@ -375,30 +388,29 @@ export const getEntityRecord = createSelector(
375
388
  }
376
389
 
377
390
  const item = queriedState.items[ context ]?.[ key ];
378
- if ( item && query._fields ) {
379
- const filteredItem = {};
380
- const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
381
- for ( let f = 0; f < fields.length; f++ ) {
382
- const field = fields[ f ].split( '.' );
383
- let value = item;
384
- field.forEach( ( fieldName ) => {
385
- value = value?.[ fieldName ];
386
- } );
387
- setNestedValue( filteredItem, field, value );
388
- }
389
- return filteredItem as EntityRecord;
391
+ if ( ! item ) {
392
+ return item;
390
393
  }
391
394
 
392
- return item;
395
+ const filteredItem = {};
396
+ const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
397
+ for ( let f = 0; f < fields.length; f++ ) {
398
+ const field = fields[ f ].split( '.' );
399
+ let value = item;
400
+ field.forEach( ( fieldName ) => {
401
+ value = value?.[ fieldName ];
402
+ } );
403
+ setNestedValue( filteredItem, field, value );
404
+ }
405
+ return filteredItem as EntityRecord;
393
406
  } ) as GetEntityRecord,
394
407
  ( state: State, kind, name, recordId, query ) => {
395
408
  const context = query?.context ?? 'default';
409
+ const queriedState =
410
+ state.entities.records?.[ kind ]?.[ name ]?.queriedData;
396
411
  return [
397
- state.entities.records?.[ kind ]?.[ name ]?.queriedData?.items[
398
- context
399
- ]?.[ recordId ],
400
- state.entities.records?.[ kind ]?.[ name ]?.queriedData
401
- ?.itemIsComplete[ context ]?.[ recordId ],
412
+ queriedState?.items[ context ]?.[ recordId ],
413
+ queriedState?.itemIsComplete[ context ]?.[ recordId ],
402
414
  ];
403
415
  }
404
416
  ) as GetEntityRecord;
@@ -421,6 +433,62 @@ getEntityRecord.__unstableNormalizeArgs = (
421
433
  return newArgs;
422
434
  };
423
435
 
436
+ /**
437
+ * Returns true if a record has been received for the given set of parameters, or false otherwise.
438
+ *
439
+ * Note: This action does not trigger a request for the entity record from the API
440
+ * if it's not available in the local state.
441
+ *
442
+ * @param state State tree
443
+ * @param kind Entity kind.
444
+ * @param name Entity name.
445
+ * @param key Record's key.
446
+ * @param query Optional query.
447
+ *
448
+ * @return Whether an entity record has been received.
449
+ */
450
+ export function hasEntityRecord(
451
+ state: State,
452
+ kind: string,
453
+ name: string,
454
+ key?: EntityRecordKey,
455
+ query?: GetRecordsHttpQuery
456
+ ): boolean {
457
+ const queriedState =
458
+ state.entities.records?.[ kind ]?.[ name ]?.queriedData;
459
+ if ( ! queriedState ) {
460
+ return false;
461
+ }
462
+ const context = query?.context ?? 'default';
463
+
464
+ // If expecting a complete item, validate that completeness.
465
+ if ( ! query || ! query._fields ) {
466
+ return !! queriedState.itemIsComplete[ context ]?.[ key ];
467
+ }
468
+
469
+ const item = queriedState.items[ context ]?.[ key ];
470
+ if ( ! item ) {
471
+ return false;
472
+ }
473
+
474
+ // When `query._fields` is provided, check that each requested field exists,
475
+ // including any nested paths, on the item; return false if any part is missing.
476
+ const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
477
+ for ( let i = 0; i < fields.length; i++ ) {
478
+ const path = fields[ i ].split( '.' );
479
+ let value = item;
480
+ for ( let p = 0; p < path.length; p++ ) {
481
+ const part = path[ p ];
482
+ if ( ! value || ! Object.hasOwn( value, part ) ) {
483
+ return false;
484
+ }
485
+ value = value[ part ];
486
+ }
487
+ }
488
+
489
+ return true;
490
+ }
491
+
424
492
  /**
425
493
  * Returns the Entity's record object by key. Doesn't trigger a resolver nor requests the entity records from the API if the entity record isn't available in the local state.
426
494
  *
@@ -1488,7 +1556,7 @@ export const getRevision = createSelector(
1488
1556
 
1489
1557
  const context = query?.context ?? 'default';
1490
1558
 
1491
- if ( query === undefined ) {
1559
+ if ( ! query || ! query._fields ) {
1492
1560
  // If expecting a complete item, validate that completeness.
1493
1561
  if ( ! queriedState.itemIsComplete[ context ]?.[ revisionKey ] ) {
1494
1562
  return undefined;
@@ -1498,31 +1566,33 @@ export const getRevision = createSelector(
1498
1566
  }
1499
1567
 
1500
1568
  const item = queriedState.items[ context ]?.[ revisionKey ];
1501
- if ( item && query._fields ) {
1502
- const filteredItem = {};
1503
- const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
1504
-
1505
- for ( let f = 0; f < fields.length; f++ ) {
1506
- const field = fields[ f ].split( '.' );
1507
- let value = item;
1508
- field.forEach( ( fieldName ) => {
1509
- value = value?.[ fieldName ];
1510
- } );
1511
- setNestedValue( filteredItem, field, value );
1512
- }
1569
+ if ( ! item ) {
1570
+ return item;
1571
+ }
1513
1572
 
1514
- return filteredItem;
1573
+ const filteredItem = {};
1574
+ const fields = getNormalizedCommaSeparable( query._fields ) ?? [];
1575
+
1576
+ for ( let f = 0; f < fields.length; f++ ) {
1577
+ const field = fields[ f ].split( '.' );
1578
+ let value = item;
1579
+ field.forEach( ( fieldName ) => {
1580
+ value = value?.[ fieldName ];
1581
+ } );
1582
+ setNestedValue( filteredItem, field, value );
1515
1583
  }
1516
1584
 
1517
- return item;
1585
+ return filteredItem;
1518
1586
  },
1519
1587
  ( state: State, kind, name, recordKey, revisionKey, query ) => {
1520
1588
  const context = query?.context ?? 'default';
1589
+ const queriedState =
1590
+ state.entities.records?.[ kind ]?.[ name ]?.revisions?.[
1591
+ recordKey
1592
+ ];
1521
1593
  return [
1522
- state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ]
1523
- ?.items?.[ context ]?.[ revisionKey ],
1524
- state.entities.records?.[ kind ]?.[ name ]?.revisions?.[ recordKey ]
1525
- ?.itemIsComplete?.[ context ]?.[ revisionKey ],
1594
+ queriedState?.items?.[ context ]?.[ revisionKey ],
1595
+ queriedState?.itemIsComplete?.[ context ]?.[ revisionKey ],
1526
1596
  ];
1527
1597
  }
1528
1598
  );
@@ -38,14 +38,14 @@ describe( 'editEntityRecord', () => {
38
38
  const select = {
39
39
  getEntityConfig: jest.fn(),
40
40
  };
41
- const fulfillment = () =>
41
+ const fulfillment = async () =>
42
42
  editEntityRecord(
43
43
  entityConfig.kind,
44
44
  entityConfig.name,
45
45
  entityConfig.id,
46
46
  {}
47
47
  )( { select } );
48
- expect( fulfillment ).toThrow(
48
+ await expect( fulfillment ).rejects.toThrow(
49
49
  `The entity being edited (${ entityConfig.kind }, ${ entityConfig.name }) does not have a loaded config.`
50
50
  );
51
51
  expect( select.getEntityConfig ).toHaveBeenCalledTimes( 1 );
@@ -1,3 +1,10 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import apiFetch from '@wordpress/api-fetch';
5
+
6
+ jest.mock( '@wordpress/api-fetch' );
7
+
1
8
  /**
2
9
  * Internal dependencies
3
10
  */
@@ -5,6 +12,7 @@ import {
5
12
  getMethodName,
6
13
  rootEntitiesConfig,
7
14
  prePersistPostType,
15
+ additionalEntityConfigLoaders,
8
16
  } from '../entities';
9
17
 
10
18
  describe( 'getMethodName', () => {
@@ -68,3 +76,27 @@ describe( 'prePersistPostType', () => {
68
76
  expect( prePersistPostType( record, edits ) ).toEqual( {} );
69
77
  } );
70
78
  } );
79
+
80
+ describe( 'loadTaxonomyEntities', () => {
81
+ beforeEach( () => {
82
+ apiFetch.mockReset();
83
+ } );
84
+
85
+ it( 'should add supportsPagination: true to taxonomy entities', async () => {
86
+ const mockTaxonomies = {
87
+ category: {
88
+ name: 'Categories',
89
+ rest_base: 'categories',
90
+ },
91
+ };
92
+
93
+ apiFetch.mockResolvedValueOnce( mockTaxonomies );
94
+
95
+ const taxonomyLoader = additionalEntityConfigLoaders.find(
96
+ ( loader ) => loader.kind === 'taxonomy'
97
+ );
98
+ const entities = await taxonomyLoader.loadEntities();
99
+
100
+ expect( entities[ 0 ].supportsPagination ).toBe( true );
101
+ } );
102
+ } );
@@ -79,10 +79,6 @@ describe( 'getEntityRecord', () => {
79
79
  it( 'accepts a query that overrides default api path', async () => {
80
80
  const query = { context: 'view', _envelope: '1' };
81
81
 
82
- const select = {
83
- hasEntityRecords: jest.fn( () => {} ),
84
- };
85
-
86
82
  // Provide response
87
83
  triggerFetch.mockImplementation( () => POST_TYPE_RESPONSE );
88
84
 
@@ -91,7 +87,7 @@ describe( 'getEntityRecord', () => {
91
87
  'postType',
92
88
  'post',
93
89
  query
94
- )( { dispatch, select, registry, resolveSelect } );
90
+ )( { dispatch, registry, resolveSelect } );
95
91
 
96
92
  // Trigger apiFetch, test that the query is present in the url.
97
93
  expect( triggerFetch ).toHaveBeenCalledWith( {
@@ -241,7 +237,7 @@ describe( 'getEntityRecords', () => {
241
237
  ] );
242
238
  } );
243
239
 
244
- it( 'caches permissions but does not mark entity records as resolved when using _fields', async () => {
240
+ it( 'caches permissions and marks entity records as resolved when using _fields', async () => {
245
241
  const finishResolutions = jest.fn();
246
242
  const dispatch = Object.assign( jest.fn(), {
247
243
  receiveEntityRecords: jest.fn(),
@@ -285,9 +281,7 @@ describe( 'getEntityRecords', () => {
285
281
  'canUser',
286
282
  expect.any( Array )
287
283
  );
288
-
289
- // But individual entity records should NOT be marked as resolved
290
- expect( finishResolutions ).not.toHaveBeenCalledWith(
284
+ expect( finishResolutions ).toHaveBeenCalledWith(
291
285
  'getEntityRecord',
292
286
  expect.any( Array )
293
287
  );
@@ -328,15 +322,112 @@ describe( 'getEntityRecords', () => {
328
322
  'canUser',
329
323
  expect.any( Array )
330
324
  );
331
-
332
- // Individual entity records should NOT be marked as resolved
333
- expect( finishResolutions ).not.toHaveBeenCalledWith(
325
+ expect( finishResolutions ).toHaveBeenCalledWith(
334
326
  'getEntityRecord',
335
327
  expect.any( Array )
336
328
  );
337
329
  } );
338
330
  } );
339
331
 
332
+ describe( 'taxonomy pagination', () => {
333
+ const registry = { batch: ( callback ) => callback() };
334
+ let dispatch, loadedTaxonomyEntities;
335
+
336
+ beforeEach( async () => {
337
+ dispatch = Object.assign( jest.fn(), {
338
+ receiveEntityRecords: jest.fn(),
339
+ __unstableAcquireStoreLock: jest.fn().mockResolvedValue( 'lock' ),
340
+ __unstableReleaseStoreLock: jest.fn(),
341
+ } );
342
+ triggerFetch.mockReset();
343
+
344
+ const mockTaxonomyConfig = {
345
+ category: {
346
+ name: 'Categories',
347
+ rest_base: 'categories',
348
+ },
349
+ };
350
+
351
+ triggerFetch.mockResolvedValueOnce( mockTaxonomyConfig );
352
+
353
+ const { additionalEntityConfigLoaders } = await import( '../entities' );
354
+ const taxonomyLoader = additionalEntityConfigLoaders.find(
355
+ ( loader ) => loader.kind === 'taxonomy'
356
+ );
357
+ loadedTaxonomyEntities = await taxonomyLoader.loadEntities();
358
+ } );
359
+
360
+ it( 'should make paginated API calls with parse: false', async () => {
361
+ const resolveSelect = {
362
+ getEntitiesConfig: jest
363
+ .fn()
364
+ .mockResolvedValue( loadedTaxonomyEntities ),
365
+ };
366
+
367
+ triggerFetch.mockResolvedValueOnce( [
368
+ { id: 1, name: 'Category 1' },
369
+ { id: 2, name: 'Category 2' },
370
+ ] );
371
+
372
+ await getEntityRecords( 'taxonomy', 'category', {
373
+ per_page: 2,
374
+ page: 1,
375
+ } )( { dispatch, registry, resolveSelect } );
376
+
377
+ expect( triggerFetch ).toHaveBeenLastCalledWith( {
378
+ path: '/wp/v2/categories?context=edit&per_page=2&page=1',
379
+ parse: false,
380
+ } );
381
+ } );
382
+
383
+ it( 'should extract pagination metadata from headers', async () => {
384
+ const resolveSelect = {
385
+ getEntitiesConfig: jest
386
+ .fn()
387
+ .mockResolvedValue( loadedTaxonomyEntities ),
388
+ };
389
+
390
+ const mockResponse = {
391
+ json: () =>
392
+ Promise.resolve( [
393
+ { id: 1, name: 'Category 1' },
394
+ { id: 2, name: 'Category 2' },
395
+ ] ),
396
+ headers: {
397
+ get: jest.fn( ( header ) => {
398
+ if ( header === 'X-WP-Total' ) {
399
+ return '10';
400
+ }
401
+ if ( header === 'X-WP-TotalPages' ) {
402
+ return '5';
403
+ }
404
+ return null;
405
+ } ),
406
+ },
407
+ };
408
+
409
+ triggerFetch.mockResolvedValueOnce( mockResponse );
410
+
411
+ await getEntityRecords( 'taxonomy', 'category', {
412
+ per_page: 2,
413
+ page: 1,
414
+ } )( { dispatch, registry, resolveSelect } );
415
+
416
+ expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
417
+ 'taxonomy',
418
+ 'category',
419
+ [
420
+ { id: 1, name: 'Category 1' },
421
+ { id: 2, name: 'Category 2' },
422
+ ],
423
+ { per_page: 2, page: 1 },
424
+ false,
425
+ undefined,
426
+ { totalItems: 10, totalPages: 5 }
427
+ );
428
+ } );
429
+ } );
430
+
340
431
  describe( 'getEmbedPreview', () => {
341
432
  const SUCCESSFUL_EMBED_RESPONSE = { data: '<p>some html</p>' };
342
433
  const UNEMBEDDABLE_RESPONSE = false;