@wordpress/core-data 4.13.0 → 4.14.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 (36) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +75 -4
  3. package/build/entities.js +7 -5
  4. package/build/entities.js.map +1 -1
  5. package/build/hooks/index.js +14 -0
  6. package/build/hooks/index.js.map +1 -1
  7. package/build/hooks/use-entity-record.js +8 -6
  8. package/build/hooks/use-entity-record.js.map +1 -1
  9. package/build/hooks/use-resource-permissions.js +72 -11
  10. package/build/hooks/use-resource-permissions.js.map +1 -1
  11. package/build/index.js +1 -28
  12. package/build/index.js.map +1 -1
  13. package/build/resolvers.js +17 -21
  14. package/build/resolvers.js.map +1 -1
  15. package/build-module/entities.js +7 -6
  16. package/build-module/entities.js.map +1 -1
  17. package/build-module/hooks/index.js +1 -0
  18. package/build-module/hooks/index.js.map +1 -1
  19. package/build-module/hooks/use-entity-record.js +8 -6
  20. package/build-module/hooks/use-entity-record.js.map +1 -1
  21. package/build-module/hooks/use-resource-permissions.js +68 -10
  22. package/build-module/hooks/use-resource-permissions.js.map +1 -1
  23. package/build-module/index.js +0 -3
  24. package/build-module/index.js.map +1 -1
  25. package/build-module/resolvers.js +17 -21
  26. package/build-module/resolvers.js.map +1 -1
  27. package/package.json +12 -11
  28. package/src/entities.ts +8 -6
  29. package/src/entity-types/theme.ts +4 -0
  30. package/src/hooks/index.ts +4 -0
  31. package/src/hooks/test/use-entity-record.js +41 -1
  32. package/src/hooks/test/use-resource-permissions.js +32 -36
  33. package/src/hooks/use-entity-record.ts +16 -6
  34. package/src/hooks/use-resource-permissions.ts +82 -20
  35. package/src/index.js +0 -3
  36. package/src/resolvers.js +36 -24
@@ -71,7 +71,38 @@ describe( 'useEntityRecord', () => {
71
71
 
72
72
  expect( data ).toEqual( {
73
73
  edit: expect.any( Function ),
74
- editedRecord: {},
74
+ editedRecord: { hello: 'world', id: 1 },
75
+ hasEdits: false,
76
+ record: { hello: 'world', id: 1 },
77
+ save: expect.any( Function ),
78
+ hasResolved: true,
79
+ isResolving: false,
80
+ status: 'SUCCESS',
81
+ } );
82
+ } );
83
+
84
+ it( 'applies edits to the entity record', async () => {
85
+ // Provide response
86
+ triggerFetch.mockImplementation( () => TEST_RECORD );
87
+
88
+ let widget;
89
+ const TestComponent = () => {
90
+ widget = useEntityRecord( 'root', 'widget', 1 );
91
+ return <div />;
92
+ };
93
+ render(
94
+ <RegistryProvider value={ registry }>
95
+ <TestComponent />
96
+ </RegistryProvider>
97
+ );
98
+
99
+ await act( async () => {
100
+ jest.advanceTimersByTime( 1 );
101
+ } );
102
+
103
+ expect( widget ).toEqual( {
104
+ edit: expect.any( Function ),
105
+ editedRecord: { hello: 'world', id: 1 },
75
106
  hasEdits: false,
76
107
  record: { hello: 'world', id: 1 },
77
108
  save: expect.any( Function ),
@@ -79,5 +110,14 @@ describe( 'useEntityRecord', () => {
79
110
  isResolving: false,
80
111
  status: 'SUCCESS',
81
112
  } );
113
+
114
+ await act( async () => {
115
+ widget.edit( { hello: 'foo' } );
116
+ jest.advanceTimersByTime( 1 );
117
+ } );
118
+
119
+ expect( widget.hasEdits ).toEqual( true );
120
+ expect( widget.record ).toEqual( { hello: 'world', id: 1 } );
121
+ expect( widget.editedRecord ).toEqual( { hello: 'foo', id: 1 } );
82
122
  } );
83
123
  } );
@@ -50,28 +50,26 @@ describe( 'useResourcePermissions', () => {
50
50
  <TestComponent />
51
51
  </RegistryProvider>
52
52
  );
53
- expect( data ).toEqual( [
54
- false,
55
- {
56
- status: 'IDLE',
57
- isResolving: false,
58
- canCreate: false,
59
- },
60
- ] );
53
+ expect( data ).toEqual( {
54
+ status: 'IDLE',
55
+ isResolving: false,
56
+ hasResolved: false,
57
+ canCreate: false,
58
+ canRead: false,
59
+ } );
61
60
 
62
61
  // Required to make sure no updates happen outside of act()
63
62
  await act( async () => {
64
63
  jest.advanceTimersByTime( 1 );
65
64
  } );
66
65
 
67
- expect( data ).toEqual( [
68
- true,
69
- {
70
- status: 'SUCCESS',
71
- isResolving: false,
72
- canCreate: true,
73
- },
74
- ] );
66
+ expect( data ).toEqual( {
67
+ status: 'SUCCESS',
68
+ isResolving: false,
69
+ hasResolved: true,
70
+ canCreate: true,
71
+ canRead: false,
72
+ } );
75
73
  } );
76
74
 
77
75
  it( 'retrieves the relevant permissions for a resource with a key', async () => {
@@ -85,31 +83,29 @@ describe( 'useResourcePermissions', () => {
85
83
  <TestComponent />
86
84
  </RegistryProvider>
87
85
  );
88
- expect( data ).toEqual( [
89
- false,
90
- {
91
- status: 'IDLE',
92
- isResolving: false,
93
- canCreate: false,
94
- canUpdate: false,
95
- canDelete: false,
96
- },
97
- ] );
86
+ expect( data ).toEqual( {
87
+ status: 'IDLE',
88
+ isResolving: false,
89
+ hasResolved: false,
90
+ canCreate: false,
91
+ canRead: false,
92
+ canUpdate: false,
93
+ canDelete: false,
94
+ } );
98
95
 
99
96
  // Required to make sure no updates happen outside of act()
100
97
  await act( async () => {
101
98
  jest.advanceTimersByTime( 1 );
102
99
  } );
103
100
 
104
- expect( data ).toEqual( [
105
- true,
106
- {
107
- status: 'SUCCESS',
108
- isResolving: false,
109
- canCreate: true,
110
- canUpdate: false,
111
- canDelete: false,
112
- },
113
- ] );
101
+ expect( data ).toEqual( {
102
+ status: 'SUCCESS',
103
+ isResolving: false,
104
+ hasResolved: true,
105
+ canCreate: true,
106
+ canRead: false,
107
+ canUpdate: false,
108
+ canDelete: false,
109
+ } );
114
110
  } );
115
111
  } );
@@ -84,8 +84,8 @@ export interface Options {
84
84
  *
85
85
  * @example
86
86
  * ```js
87
- * import { useState } from '@wordpress/data';
88
87
  * import { useDispatch } from '@wordpress/data';
88
+ * import { useCallback } from '@wordpress/element';
89
89
  * import { __ } from '@wordpress/i18n';
90
90
  * import { TextControl } from '@wordpress/components';
91
91
  * import { store as noticeStore } from '@wordpress/notices';
@@ -93,17 +93,19 @@ export interface Options {
93
93
  *
94
94
  * function PageRenameForm( { id } ) {
95
95
  * const page = useEntityRecord( 'postType', 'page', id );
96
- * const [ title, setTitle ] = useState( () => page.record.title.rendered );
97
96
  * const { createSuccessNotice, createErrorNotice } =
98
97
  * useDispatch( noticeStore );
99
98
  *
99
+ * const setTitle = useCallback( ( title ) => {
100
+ * page.edit( { title } );
101
+ * }, [ page.edit ] );
102
+ *
100
103
  * if ( page.isResolving ) {
101
104
  * return 'Loading...';
102
105
  * }
103
106
  *
104
107
  * async function onRename( event ) {
105
108
  * event.preventDefault();
106
- * page.edit( { title } );
107
109
  * try {
108
110
  * await page.save();
109
111
  * createSuccessNotice( __( 'Page renamed.' ), {
@@ -118,7 +120,7 @@ export interface Options {
118
120
  * <form onSubmit={ onRename }>
119
121
  * <TextControl
120
122
  * label={ __( 'Name' ) }
121
- * value={ title }
123
+ * value={ page.editedRecord.title }
122
124
  * onChange={ setTitle }
123
125
  * />
124
126
  * <button type="submit">{ __( 'Save' ) }</button>
@@ -161,8 +163,16 @@ export default function useEntityRecord< RecordType >(
161
163
 
162
164
  const { editedRecord, hasEdits } = useSelect(
163
165
  ( select ) => ( {
164
- editedRecord: select( coreStore ).getEditedEntityRecord(),
165
- hasEdits: select( coreStore ).hasEditsForEntityRecord(),
166
+ editedRecord: select( coreStore ).getEditedEntityRecord(
167
+ kind,
168
+ name,
169
+ recordId
170
+ ),
171
+ hasEdits: select( coreStore ).hasEditsForEntityRecord(
172
+ kind,
173
+ name,
174
+ recordId
175
+ ),
166
176
  } ),
167
177
  [ kind, name, recordId ]
168
178
  );
@@ -1,3 +1,8 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import deprecated from '@wordpress/deprecated';
5
+
1
6
  /**
2
7
  * Internal dependencies
3
8
  */
@@ -65,6 +70,36 @@ type ResourcePermissionsResolution< IdType > = [
65
70
  * // <PagesList />
66
71
  * ```
67
72
  *
73
+ * @example
74
+ * ```js
75
+ * import { useResourcePermissions } from '@wordpress/core-data';
76
+ *
77
+ * function Page({ pageId }) {
78
+ * const {
79
+ * canCreate,
80
+ * canUpdate,
81
+ * canDelete,
82
+ * isResolving
83
+ * } = useResourcePermissions( 'pages', pageId );
84
+ *
85
+ * if ( isResolving ) {
86
+ * return 'Loading ...';
87
+ * }
88
+ *
89
+ * return (
90
+ * <div>
91
+ * {canCreate ? (<button>+ Create a new page</button>) : false}
92
+ * {canUpdate ? (<button>Edit page</button>) : false}
93
+ * {canDelete ? (<button>Delete page</button>) : false}
94
+ * // ...
95
+ * </div>
96
+ * );
97
+ * }
98
+ *
99
+ * // Rendered in the application:
100
+ * // <Page pageId={ 15 } />
101
+ * ```
102
+ *
68
103
  * In the above example, when `PagesList` is rendered into an
69
104
  * application, the appropriate permissions and the resolution details will be retrieved from
70
105
  * the store state using `canUser()`, or resolved if missing.
@@ -72,7 +107,7 @@ type ResourcePermissionsResolution< IdType > = [
72
107
  * @return Entity records data.
73
108
  * @template IdType
74
109
  */
75
- export default function __experimentalUseResourcePermissions< IdType = void >(
110
+ export default function useResourcePermissions< IdType = void >(
76
111
  resource: string,
77
112
  id?: IdType
78
113
  ): ResourcePermissionsResolution< IdType > {
@@ -81,22 +116,39 @@ export default function __experimentalUseResourcePermissions< IdType = void >(
81
116
  const { canUser } = resolve( coreStore );
82
117
  const create = canUser( 'create', resource );
83
118
  if ( ! id ) {
84
- return [
85
- create.hasResolved,
86
- {
87
- status: create.status,
88
- isResolving: create.isResolving,
89
- canCreate: create.hasResolved && create.data,
90
- },
91
- ];
119
+ const read = canUser( 'read', resource );
120
+
121
+ const isResolving = create.isResolving || read.isResolving;
122
+ const hasResolved = create.hasResolved && read.hasResolved;
123
+ let status = Status.Idle;
124
+ if ( isResolving ) {
125
+ status = Status.Resolving;
126
+ } else if ( hasResolved ) {
127
+ status = Status.Success;
128
+ }
129
+
130
+ return {
131
+ status,
132
+ isResolving,
133
+ hasResolved,
134
+ canCreate: create.hasResolved && create.data,
135
+ canRead: read.hasResolved && read.data,
136
+ };
92
137
  }
93
138
 
139
+ const read = canUser( 'read', resource, id );
94
140
  const update = canUser( 'update', resource, id );
95
141
  const _delete = canUser( 'delete', resource, id );
96
142
  const isResolving =
97
- create.isResolving || update.isResolving || _delete.isResolving;
143
+ read.isResolving ||
144
+ create.isResolving ||
145
+ update.isResolving ||
146
+ _delete.isResolving;
98
147
  const hasResolved =
99
- create.hasResolved && update.hasResolved && _delete.hasResolved;
148
+ read.hasResolved &&
149
+ create.hasResolved &&
150
+ update.hasResolved &&
151
+ _delete.hasResolved;
100
152
 
101
153
  let status = Status.Idle;
102
154
  if ( isResolving ) {
@@ -104,17 +156,27 @@ export default function __experimentalUseResourcePermissions< IdType = void >(
104
156
  } else if ( hasResolved ) {
105
157
  status = Status.Success;
106
158
  }
107
- return [
159
+ return {
160
+ status,
161
+ isResolving,
108
162
  hasResolved,
109
- {
110
- status,
111
- isResolving,
112
- canCreate: hasResolved && create.data,
113
- canUpdate: hasResolved && update.data,
114
- canDelete: hasResolved && _delete.data,
115
- },
116
- ];
163
+ canRead: hasResolved && read.data,
164
+ canCreate: hasResolved && create.data,
165
+ canUpdate: hasResolved && update.data,
166
+ canDelete: hasResolved && _delete.data,
167
+ };
117
168
  },
118
169
  [ resource, id ]
119
170
  );
120
171
  }
172
+
173
+ export function __experimentalUseResourcePermissions(
174
+ resource: string,
175
+ id?: unknown
176
+ ) {
177
+ deprecated( `wp.data.__experimentalUseResourcePermissions`, {
178
+ alternative: 'wp.data.useResourcePermissions',
179
+ since: '6.1',
180
+ } );
181
+ return useResourcePermissions( resource, id );
182
+ }
package/src/index.js CHANGED
@@ -68,9 +68,6 @@ export const store = createReduxStore( STORE_NAME, storeConfig() );
68
68
  register( store );
69
69
 
70
70
  export { default as EntityProvider } from './entity-provider';
71
- export { default as useEntityRecord } from './hooks/use-entity-record';
72
- export { default as useEntityRecords } from './hooks/use-entity-records';
73
- export { default as __experimentalUseResourcePermissions } from './hooks/use-resource-permissions';
74
71
  export * from './entity-provider';
75
72
  export * from './entity-types';
76
73
  export * from './fetch';
package/src/resolvers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { camelCase, find, get, includes, map, mapKeys, uniq } from 'lodash';
4
+ import { camelCase } from 'change-case';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -57,7 +57,9 @@ export const getEntityRecord =
57
57
  ( kind, name, key = '', query ) =>
58
58
  async ( { select, dispatch } ) => {
59
59
  const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
60
- const entityConfig = find( configs, { kind, name } );
60
+ const entityConfig = configs.find(
61
+ ( config ) => config.name === name && config.kind === kind
62
+ );
61
63
  if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
62
64
  return;
63
65
  }
@@ -75,11 +77,13 @@ export const getEntityRecord =
75
77
  // the ID.
76
78
  query = {
77
79
  ...query,
78
- _fields: uniq( [
79
- ...( getNormalizedCommaSeparable( query._fields ) ||
80
- [] ),
81
- entityConfig.key || DEFAULT_ENTITY_KEY,
82
- ] ).join(),
80
+ _fields: [
81
+ ...new Set( [
82
+ ...( getNormalizedCommaSeparable( query._fields ) ||
83
+ [] ),
84
+ entityConfig.key || DEFAULT_ENTITY_KEY,
85
+ ] ),
86
+ ].join(),
83
87
  };
84
88
  }
85
89
 
@@ -139,7 +143,9 @@ export const getEntityRecords =
139
143
  ( kind, name, query = {} ) =>
140
144
  async ( { dispatch } ) => {
141
145
  const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
142
- const entityConfig = find( configs, { kind, name } );
146
+ const entityConfig = configs.find(
147
+ ( config ) => config.name === name && config.kind === kind
148
+ );
143
149
  if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
144
150
  return;
145
151
  }
@@ -157,11 +163,13 @@ export const getEntityRecords =
157
163
  // the ID.
158
164
  query = {
159
165
  ...query,
160
- _fields: uniq( [
161
- ...( getNormalizedCommaSeparable( query._fields ) ||
162
- [] ),
163
- entityConfig.key || DEFAULT_ENTITY_KEY,
164
- ] ).join(),
166
+ _fields: [
167
+ ...new Set( [
168
+ ...( getNormalizedCommaSeparable( query._fields ) ||
169
+ [] ),
170
+ entityConfig.key || DEFAULT_ENTITY_KEY,
171
+ ] ),
172
+ ].join(),
165
173
  };
166
174
  }
167
175
 
@@ -307,7 +315,8 @@ export const canUser =
307
315
  // only return the result, without including response properties like the headers.
308
316
  const allowHeader = response.headers?.get( 'allow' );
309
317
  const key = [ action, resource, id ].filter( Boolean ).join( '/' );
310
- const isAllowed = includes( allowHeader, method );
318
+ const isAllowed =
319
+ allowHeader?.includes?.( method ) || allowHeader?.allow === method;
311
320
  dispatch.receiveUserPermission( key, isAllowed );
312
321
  };
313
322
 
@@ -323,7 +332,9 @@ export const canUserEditEntityRecord =
323
332
  ( kind, name, recordId ) =>
324
333
  async ( { dispatch } ) => {
325
334
  const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
326
- const entityConfig = find( configs, { kind, name } );
335
+ const entityConfig = configs.find(
336
+ ( config ) => config.name === name && config.kind === kind
337
+ );
327
338
  if ( ! entityConfig ) {
328
339
  return;
329
340
  }
@@ -427,13 +438,9 @@ export const __experimentalGetCurrentGlobalStylesId =
427
438
  'theme',
428
439
  { status: 'active' }
429
440
  );
430
- const globalStylesURL = get( activeThemes, [
431
- 0,
432
- '_links',
433
- 'wp:user-global-styles',
434
- 0,
435
- 'href',
436
- ] );
441
+ const globalStylesURL =
442
+ activeThemes?.[ 0 ]?._links?.[ 'wp:user-global-styles' ]?.[ 0 ]
443
+ ?.href;
437
444
  if ( globalStylesURL ) {
438
445
  const globalStylesObject = await apiFetch( {
439
446
  url: globalStylesURL,
@@ -476,8 +483,13 @@ export const getBlockPatterns =
476
483
  const restPatterns = await apiFetch( {
477
484
  path: '/wp/v2/block-patterns/patterns',
478
485
  } );
479
- const patterns = map( restPatterns, ( pattern ) =>
480
- mapKeys( pattern, ( value, key ) => camelCase( key ) )
486
+ const patterns = restPatterns?.map( ( pattern ) =>
487
+ Object.fromEntries(
488
+ Object.entries( pattern ).map( ( [ key, value ] ) => [
489
+ camelCase( key ),
490
+ value,
491
+ ] )
492
+ )
481
493
  );
482
494
  dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } );
483
495
  };