@wordpress/core-data 7.27.0 → 7.28.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 (69) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +2 -2
  3. package/build/actions.js +7 -0
  4. package/build/actions.js.map +1 -1
  5. package/build/entities.js +15 -2
  6. package/build/entities.js.map +1 -1
  7. package/build/hooks/use-resource-permissions.js +1 -1
  8. package/build/hooks/use-resource-permissions.js.map +1 -1
  9. package/build/index.js +50 -8
  10. package/build/index.js.map +1 -1
  11. package/build/private-actions.js +99 -0
  12. package/build/private-actions.js.map +1 -1
  13. package/build/private-selectors.js +3 -0
  14. package/build/private-selectors.js.map +1 -1
  15. package/build/resolvers.js +1 -1
  16. package/build/resolvers.js.map +1 -1
  17. package/build/selectors.js +26 -1
  18. package/build/selectors.js.map +1 -1
  19. package/build/utils/log-entity-deprecation.js +63 -0
  20. package/build/utils/log-entity-deprecation.js.map +1 -0
  21. package/build-module/actions.js +7 -0
  22. package/build-module/actions.js.map +1 -1
  23. package/build-module/entities.js +14 -1
  24. package/build-module/entities.js.map +1 -1
  25. package/build-module/hooks/use-resource-permissions.js +1 -1
  26. package/build-module/hooks/use-resource-permissions.js.map +1 -1
  27. package/build-module/index.js +50 -8
  28. package/build-module/index.js.map +1 -1
  29. package/build-module/private-actions.js +96 -0
  30. package/build-module/private-actions.js.map +1 -1
  31. package/build-module/private-selectors.js +2 -0
  32. package/build-module/private-selectors.js.map +1 -1
  33. package/build-module/resolvers.js +1 -1
  34. package/build-module/resolvers.js.map +1 -1
  35. package/build-module/selectors.js +26 -1
  36. package/build-module/selectors.js.map +1 -1
  37. package/build-module/utils/log-entity-deprecation.js +55 -0
  38. package/build-module/utils/log-entity-deprecation.js.map +1 -0
  39. package/build-types/actions.d.ts.map +1 -1
  40. package/build-types/entities.d.ts +29 -18
  41. package/build-types/entities.d.ts.map +1 -1
  42. package/build-types/index.d.ts.map +1 -1
  43. package/build-types/private-actions.d.ts +24 -0
  44. package/build-types/private-actions.d.ts.map +1 -1
  45. package/build-types/private-selectors.d.ts +1 -1
  46. package/build-types/private-selectors.d.ts.map +1 -1
  47. package/build-types/reducer.d.ts +3 -3
  48. package/build-types/reducer.d.ts.map +1 -1
  49. package/build-types/selectors.d.ts +1 -1
  50. package/build-types/selectors.d.ts.map +1 -1
  51. package/build-types/utils/log-entity-deprecation.d.ts +15 -0
  52. package/build-types/utils/log-entity-deprecation.d.ts.map +1 -0
  53. package/package.json +18 -18
  54. package/src/actions.js +11 -0
  55. package/src/entities.js +14 -0
  56. package/src/hooks/test/use-resource-permissions.js +3 -3
  57. package/src/hooks/use-resource-permissions.ts +1 -1
  58. package/src/index.js +54 -14
  59. package/src/private-actions.js +118 -0
  60. package/src/private-selectors.ts +2 -0
  61. package/src/resolvers.js +1 -1
  62. package/src/selectors.ts +29 -1
  63. package/src/test/deprecated-entity-logging.js +291 -0
  64. package/src/test/private-actions.js +213 -0
  65. package/src/test/resolvers.js +7 -7
  66. package/src/test/selectors.js +18 -7
  67. package/src/utils/log-entity-deprecation.ts +67 -0
  68. package/src/utils/test/log-entity-deprecation.js +130 -0
  69. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,213 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import apiFetch from '@wordpress/api-fetch';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { editMediaEntity } from '../private-actions';
10
+
11
+ jest.mock( '@wordpress/api-fetch' );
12
+
13
+ describe( 'editMediaEntity', () => {
14
+ let dispatch;
15
+ let resolveSelect;
16
+
17
+ beforeEach( () => {
18
+ apiFetch.mockReset();
19
+ dispatch = Object.assign( jest.fn(), {
20
+ receiveEntityRecords: jest.fn(),
21
+ __unstableAcquireStoreLock: jest.fn( () => 'test-lock' ),
22
+ __unstableReleaseStoreLock: jest.fn(),
23
+ } );
24
+ resolveSelect = {
25
+ getEntitiesConfig: jest.fn( () => [
26
+ {
27
+ kind: 'postType',
28
+ name: 'attachment',
29
+ baseURL: '/wp/v2/media',
30
+ },
31
+ ] ),
32
+ };
33
+ } );
34
+
35
+ it( 'should return early when recordId is not provided', async () => {
36
+ const result = await editMediaEntity( null )( {
37
+ dispatch,
38
+ resolveSelect,
39
+ } );
40
+
41
+ expect( result ).toBeUndefined();
42
+ expect( dispatch.__unstableAcquireStoreLock ).not.toHaveBeenCalled();
43
+ expect( apiFetch ).not.toHaveBeenCalled();
44
+ } );
45
+
46
+ it( 'should return early when entity config is not found', async () => {
47
+ resolveSelect.getEntitiesConfig.mockReturnValue( [] );
48
+
49
+ const result = await editMediaEntity( 123 )( {
50
+ dispatch,
51
+ resolveSelect,
52
+ } );
53
+
54
+ expect( result ).toBeUndefined();
55
+ expect( dispatch.__unstableAcquireStoreLock ).not.toHaveBeenCalled();
56
+ expect( apiFetch ).not.toHaveBeenCalled();
57
+ } );
58
+
59
+ it( 'should successfully edit a media entity', async () => {
60
+ const recordId = 123;
61
+ const edits = {
62
+ src: 'https://example.com/image.jpg',
63
+ modifiers: [
64
+ { type: 'resize', args: { width: 300, height: 200 } },
65
+ ],
66
+ };
67
+ const updatedRecord = {
68
+ id: recordId,
69
+ src: 'https://example.com/image.jpg',
70
+ modified: true,
71
+ };
72
+
73
+ apiFetch.mockResolvedValue( updatedRecord );
74
+
75
+ const result = await editMediaEntity(
76
+ recordId,
77
+ edits
78
+ )( {
79
+ dispatch,
80
+ resolveSelect,
81
+ } );
82
+
83
+ expect( dispatch.__unstableAcquireStoreLock ).toHaveBeenCalledWith(
84
+ 'core',
85
+ [ 'entities', 'records', 'postType', 'attachment', recordId ],
86
+ { exclusive: true }
87
+ );
88
+
89
+ expect( dispatch ).toHaveBeenCalledWith( {
90
+ type: 'SAVE_ENTITY_RECORD_START',
91
+ kind: 'postType',
92
+ name: 'attachment',
93
+ recordId,
94
+ } );
95
+
96
+ expect( apiFetch ).toHaveBeenCalledWith( {
97
+ path: '/wp/v2/media/123/edit',
98
+ method: 'POST',
99
+ data: edits,
100
+ } );
101
+
102
+ expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith(
103
+ 'postType',
104
+ 'attachment',
105
+ [ updatedRecord ],
106
+ undefined,
107
+ true,
108
+ undefined,
109
+ undefined
110
+ );
111
+
112
+ expect( dispatch ).toHaveBeenCalledWith( {
113
+ type: 'SAVE_ENTITY_RECORD_FINISH',
114
+ kind: 'postType',
115
+ name: 'attachment',
116
+ recordId,
117
+ error: undefined,
118
+ } );
119
+
120
+ expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledWith(
121
+ 'test-lock'
122
+ );
123
+
124
+ expect( result ).toBe( updatedRecord );
125
+ } );
126
+
127
+ it( 'should handle API errors when throwOnError is false', async () => {
128
+ const recordId = 123;
129
+ const edits = { src: 'https://example.com/image.jpg' };
130
+ const apiError = new Error( 'API error' );
131
+
132
+ apiFetch.mockRejectedValue( apiError );
133
+
134
+ const result = await editMediaEntity( recordId, edits, {
135
+ throwOnError: false,
136
+ } )( { dispatch, resolveSelect } );
137
+
138
+ expect( dispatch ).toHaveBeenCalledWith( {
139
+ type: 'SAVE_ENTITY_RECORD_START',
140
+ kind: 'postType',
141
+ name: 'attachment',
142
+ recordId,
143
+ } );
144
+
145
+ expect( dispatch ).toHaveBeenCalledWith( {
146
+ type: 'SAVE_ENTITY_RECORD_FINISH',
147
+ kind: 'postType',
148
+ name: 'attachment',
149
+ recordId,
150
+ error: apiError,
151
+ } );
152
+
153
+ expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledWith(
154
+ 'test-lock'
155
+ );
156
+
157
+ expect( result ).toBeUndefined();
158
+ } );
159
+
160
+ it( 'should throw errors when throwOnError is true', async () => {
161
+ const recordId = 123;
162
+ const edits = { src: 'https://example.com/image.jpg' };
163
+ const apiError = new Error( 'API error' );
164
+
165
+ apiFetch.mockRejectedValue( apiError );
166
+
167
+ await expect(
168
+ editMediaEntity( recordId, edits, { throwOnError: true } )( {
169
+ dispatch,
170
+ resolveSelect,
171
+ } )
172
+ ).rejects.toEqual( apiError );
173
+
174
+ expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledWith(
175
+ 'test-lock'
176
+ );
177
+ } );
178
+
179
+ it( 'should use custom fetch function when provided', async () => {
180
+ const recordId = 123;
181
+ const edits = { src: 'https://example.com/image.jpg' };
182
+ const customFetch = jest.fn().mockResolvedValue( { id: recordId } );
183
+
184
+ await editMediaEntity( recordId, edits, {
185
+ __unstableFetch: customFetch,
186
+ } )( { dispatch, resolveSelect } );
187
+
188
+ expect( customFetch ).toHaveBeenCalledWith( {
189
+ path: '/wp/v2/media/123/edit',
190
+ method: 'POST',
191
+ data: edits,
192
+ } );
193
+ expect( apiFetch ).not.toHaveBeenCalled();
194
+ } );
195
+
196
+ it( 'should handle null response from API', async () => {
197
+ const recordId = 123;
198
+ const edits = { src: 'https://example.com/image.jpg' };
199
+
200
+ apiFetch.mockResolvedValue( null );
201
+
202
+ const result = await editMediaEntity(
203
+ recordId,
204
+ edits
205
+ )( {
206
+ dispatch,
207
+ resolveSelect,
208
+ } );
209
+
210
+ expect( dispatch.receiveEntityRecords ).not.toHaveBeenCalled();
211
+ expect( result ).toBeUndefined();
212
+ } );
213
+ } );
@@ -379,8 +379,8 @@ describe( 'getEmbedPreview', () => {
379
379
  describe( 'canUser', () => {
380
380
  const ENTITIES = [
381
381
  {
382
- name: 'media',
383
- kind: 'root',
382
+ name: 'attachment',
383
+ kind: 'postType',
384
384
  baseURL: '/wp/v2/media',
385
385
  baseURLParams: { context: 'edit' },
386
386
  },
@@ -417,7 +417,7 @@ describe( 'canUser', () => {
417
417
  'create',
418
418
  'media'
419
419
  )( { dispatch, registry, resolveSelect } );
420
- await canUser( 'create', { kind: 'root', name: 'media' } )( {
420
+ await canUser( 'create', { kind: 'postType', name: 'attachment' } )( {
421
421
  dispatch,
422
422
  registry,
423
423
  resolveSelect,
@@ -469,7 +469,7 @@ describe( 'canUser', () => {
469
469
  headers: new Map( [ [ 'allow', 'GET' ] ] ),
470
470
  } ) );
471
471
 
472
- await canUser( 'create', { kind: 'root', name: 'media' } )( {
472
+ await canUser( 'create', { kind: 'postType', name: 'attachment' } )( {
473
473
  dispatch,
474
474
  registry,
475
475
  resolveSelect,
@@ -482,7 +482,7 @@ describe( 'canUser', () => {
482
482
  } );
483
483
 
484
484
  expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
485
- 'create/root/media',
485
+ 'create/postType/attachment',
486
486
  false
487
487
  );
488
488
  } );
@@ -514,7 +514,7 @@ describe( 'canUser', () => {
514
514
  headers: new Map( [ [ 'allow', 'POST, GET, PUT, DELETE' ] ] ),
515
515
  } ) );
516
516
 
517
- await canUser( 'create', { kind: 'root', name: 'media' } )( {
517
+ await canUser( 'create', { kind: 'postType', name: 'attachment' } )( {
518
518
  dispatch,
519
519
  registry,
520
520
  resolveSelect,
@@ -527,7 +527,7 @@ describe( 'canUser', () => {
527
527
  } );
528
528
 
529
529
  expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith(
530
- 'create/root/media',
530
+ 'create/postType/attachment',
531
531
  true
532
532
  );
533
533
  } );
@@ -24,6 +24,7 @@ import {
24
24
  getRevisions,
25
25
  getRevision,
26
26
  } from '../selectors';
27
+
27
28
  // getEntityRecord and __experimentalGetEntityRecordNoResolver selectors share the same tests.
28
29
  describe.each( [
29
30
  [ getEntityRecord ],
@@ -52,6 +53,7 @@ describe.each( [
52
53
  ] );
53
54
  } );
54
55
  } );
56
+
55
57
  it( 'should return undefined for unknown entity kind, name', () => {
56
58
  const state = deepFreeze( {
57
59
  entities: {
@@ -290,6 +292,7 @@ describe( 'getRawEntityRecord', () => {
290
292
  },
291
293
  },
292
294
  };
295
+
293
296
  it( 'should preserve the structure of `raw` field by default', () => {
294
297
  const state = deepFreeze( {
295
298
  entities: {
@@ -732,7 +735,7 @@ describe( 'canUser', () => {
732
735
  } );
733
736
  expect( canUser( state, 'create', 'media' ) ).toBe( undefined );
734
737
  expect(
735
- canUser( state, 'create', { kind: 'root', name: 'media' } )
738
+ canUser( state, 'create', { kind: 'postType', name: 'attachment' } )
736
739
  ).toBe( undefined );
737
740
  } );
738
741
 
@@ -740,20 +743,24 @@ describe( 'canUser', () => {
740
743
  const state = deepFreeze( {
741
744
  userPermissions: {},
742
745
  } );
743
- expect( canUser( state, 'create', { name: 'media' } ) ).toBe( false );
744
- expect( canUser( state, 'create', { kind: 'root' } ) ).toBe( false );
746
+ expect( canUser( state, 'create', { name: 'attachment' } ) ).toBe(
747
+ false
748
+ );
749
+ expect( canUser( state, 'create', { kind: 'postType' } ) ).toBe(
750
+ false
751
+ );
745
752
  } );
746
753
 
747
754
  it( 'returns whether an action can be performed', () => {
748
755
  const state = deepFreeze( {
749
756
  userPermissions: {
750
757
  'create/media': false,
751
- 'create/root/media': false,
758
+ 'create/postType/attachment': false,
752
759
  },
753
760
  } );
754
761
  expect( canUser( state, 'create', 'media' ) ).toBe( false );
755
762
  expect(
756
- canUser( state, 'create', { kind: 'root', name: 'media' } )
763
+ canUser( state, 'create', { kind: 'postType', name: 'attachment' } )
757
764
  ).toBe( false );
758
765
  } );
759
766
 
@@ -761,12 +768,16 @@ describe( 'canUser', () => {
761
768
  const state = deepFreeze( {
762
769
  userPermissions: {
763
770
  'create/media/123': false,
764
- 'create/root/media/123': false,
771
+ 'create/postType/attachment/123': false,
765
772
  },
766
773
  } );
767
774
  expect( canUser( state, 'create', 'media', 123 ) ).toBe( false );
768
775
  expect(
769
- canUser( state, 'create', { kind: 'root', name: 'media', id: 123 } )
776
+ canUser( state, 'create', {
777
+ kind: 'postType',
778
+ name: 'attachment',
779
+ id: 123,
780
+ } )
770
781
  ).toBe( false );
771
782
  } );
772
783
  } );
@@ -0,0 +1,67 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import deprecated from '@wordpress/deprecated';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { deprecatedEntities } from '../entities';
10
+
11
+ let loggedAlready = false;
12
+
13
+ /**
14
+ * Logs a deprecation warning for an entity, if it's deprecated.
15
+ *
16
+ * @param kind The kind of the entity.
17
+ * @param name The name of the entity.
18
+ * @param functionName The name of the function that was called with a deprecated entity.
19
+ * @param options The options for the deprecation warning.
20
+ * @param options.alternativeFunctionName The name of the alternative function that should be used instead.
21
+ * @param options.isShorthandSelector Whether the function is a shorthand selector.
22
+ */
23
+ export default function logEntityDeprecation(
24
+ kind: string,
25
+ name: string,
26
+ functionName: string,
27
+ {
28
+ alternativeFunctionName,
29
+ isShorthandSelector = false,
30
+ }: {
31
+ alternativeFunctionName?: string;
32
+ isShorthandSelector?: boolean;
33
+ } = {}
34
+ ) {
35
+ const deprecation = deprecatedEntities[ kind ]?.[ name ];
36
+ if ( ! deprecation ) {
37
+ return;
38
+ }
39
+
40
+ if ( ! loggedAlready ) {
41
+ const { alternative } = deprecation;
42
+
43
+ const message = isShorthandSelector
44
+ ? `'${ functionName }'`
45
+ : `The '${ kind }', '${ name }' entity (used via '${ functionName }')`;
46
+
47
+ let alternativeMessage = `the '${ alternative.kind }', '${ alternative.name }' entity`;
48
+ if ( alternativeFunctionName ) {
49
+ alternativeMessage += ` via the '${ alternativeFunctionName }' function`;
50
+ }
51
+
52
+ deprecated( message, {
53
+ ...deprecation,
54
+ alternative: alternativeMessage,
55
+ } );
56
+ }
57
+
58
+ // Only log an entity deprecation once per call stack,
59
+ // else there's spurious logging when selections or actions call through to other selectors or actions.
60
+ // Note: this won't prevent the deprecation warning being logged if a selector or action makes an async call
61
+ // to another selector or action, but this is probably the best we can do.
62
+ loggedAlready = true;
63
+ // At the end of the call stack, reset the flag.
64
+ setTimeout( () => {
65
+ loggedAlready = false;
66
+ }, 0 );
67
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import deprecated from '@wordpress/deprecated';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import logEntityDeprecation from '../log-entity-deprecation';
10
+
11
+ jest.useFakeTimers();
12
+
13
+ // Mock the deprecatedEntities import
14
+ jest.mock( '../../entities', () => ( {
15
+ deprecatedEntities: {
16
+ root: {
17
+ media: {
18
+ since: '6.9',
19
+ alternative: {
20
+ kind: 'postType',
21
+ name: 'attachment',
22
+ },
23
+ },
24
+ },
25
+ },
26
+ } ) );
27
+
28
+ // Mock the deprecated function
29
+ jest.mock( '@wordpress/deprecated' );
30
+
31
+ describe( 'logEntityDeprecation', () => {
32
+ beforeEach( () => {
33
+ jest.clearAllMocks();
34
+
35
+ // Ensure the timeout that prevents spurious logging is cleared.
36
+ jest.runAllTimers();
37
+ } );
38
+
39
+ it( 'should call deprecated when entity is deprecated', () => {
40
+ logEntityDeprecation( 'root', 'media', 'getEntityRecord' );
41
+
42
+ expect( deprecated ).toHaveBeenCalledWith(
43
+ "The 'root', 'media' entity (used via 'getEntityRecord')",
44
+ {
45
+ since: '6.9',
46
+ alternative: "the 'postType', 'attachment' entity",
47
+ }
48
+ );
49
+ } );
50
+
51
+ it( 'should not call deprecated when entity is not deprecated', () => {
52
+ logEntityDeprecation( 'root', 'nonExistentEntity', 'getEntityRecord' );
53
+
54
+ expect( deprecated ).not.toHaveBeenCalled();
55
+ } );
56
+
57
+ it( 'should not call deprecated when kind is not deprecated', () => {
58
+ logEntityDeprecation( 'nonExistentKind', 'media', 'getEntityRecord' );
59
+
60
+ expect( deprecated ).not.toHaveBeenCalled();
61
+ } );
62
+
63
+ it( 'should handle different function names', () => {
64
+ logEntityDeprecation( 'root', 'media', 'saveEntityRecord' );
65
+
66
+ expect( deprecated ).toHaveBeenCalledWith(
67
+ "The 'root', 'media' entity (used via 'saveEntityRecord')",
68
+ {
69
+ since: '6.9',
70
+ alternative: "the 'postType', 'attachment' entity",
71
+ }
72
+ );
73
+ } );
74
+
75
+ it( 'should include alternative function name when provided', () => {
76
+ logEntityDeprecation( 'root', 'media', 'getEntityRecord', {
77
+ alternativeFunctionName: 'getPostTypeEntity',
78
+ } );
79
+
80
+ expect( deprecated ).toHaveBeenCalledWith(
81
+ "The 'root', 'media' entity (used via 'getEntityRecord')",
82
+ {
83
+ since: '6.9',
84
+ alternative:
85
+ "the 'postType', 'attachment' entity via the 'getPostTypeEntity' function",
86
+ }
87
+ );
88
+ } );
89
+
90
+ it( 'should handle isShorthandSelector', () => {
91
+ logEntityDeprecation( 'root', 'media', 'getMedia', {
92
+ isShorthandSelector: true,
93
+ } );
94
+
95
+ expect( deprecated ).toHaveBeenCalledWith( "'getMedia'", {
96
+ since: '6.9',
97
+ alternative: "the 'postType', 'attachment' entity",
98
+ } );
99
+ } );
100
+
101
+ it( 'should handle empty string parameters', () => {
102
+ logEntityDeprecation( '', '', '' );
103
+
104
+ expect( deprecated ).not.toHaveBeenCalled();
105
+ } );
106
+
107
+ it( 'should handle null parameters', () => {
108
+ logEntityDeprecation( null, null, null );
109
+
110
+ expect( deprecated ).not.toHaveBeenCalled();
111
+ } );
112
+
113
+ it( 'should handle undefined parameters', () => {
114
+ logEntityDeprecation( undefined, undefined, undefined );
115
+
116
+ expect( deprecated ).not.toHaveBeenCalled();
117
+ } );
118
+
119
+ it( 'should handle undefined options', () => {
120
+ logEntityDeprecation( 'root', 'media', 'getEntityRecord', undefined );
121
+
122
+ expect( deprecated ).toHaveBeenCalledWith(
123
+ "The 'root', 'media' entity (used via 'getEntityRecord')",
124
+ {
125
+ since: '6.9',
126
+ alternative: "the 'postType', 'attachment' entity",
127
+ }
128
+ );
129
+ } );
130
+ } );