@wordpress/core-data 4.11.0 → 4.14.1-next.d6164808d3.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 (51) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +124 -81
  3. package/build/actions.js +27 -0
  4. package/build/actions.js.map +1 -1
  5. package/build/batch/create-batch.js +3 -15
  6. package/build/batch/create-batch.js.map +1 -1
  7. package/build/entities.js +7 -5
  8. package/build/entities.js.map +1 -1
  9. package/build/hooks/index.js +14 -0
  10. package/build/hooks/index.js.map +1 -1
  11. package/build/hooks/use-entity-record.js +84 -2
  12. package/build/hooks/use-entity-record.js.map +1 -1
  13. package/build/hooks/use-resource-permissions.js +53 -6
  14. package/build/hooks/use-resource-permissions.js.map +1 -1
  15. package/build/index.js +1 -28
  16. package/build/index.js.map +1 -1
  17. package/build/resolvers.js +22 -24
  18. package/build/resolvers.js.map +1 -1
  19. package/build/selectors.js +5 -3
  20. package/build/selectors.js.map +1 -1
  21. package/build-module/actions.js +27 -0
  22. package/build-module/actions.js.map +1 -1
  23. package/build-module/batch/create-batch.js +3 -14
  24. package/build-module/batch/create-batch.js.map +1 -1
  25. package/build-module/entities.js +7 -6
  26. package/build-module/entities.js.map +1 -1
  27. package/build-module/hooks/index.js +1 -0
  28. package/build-module/hooks/index.js.map +1 -1
  29. package/build-module/hooks/use-entity-record.js +82 -2
  30. package/build-module/hooks/use-entity-record.js.map +1 -1
  31. package/build-module/hooks/use-resource-permissions.js +49 -5
  32. package/build-module/hooks/use-resource-permissions.js.map +1 -1
  33. package/build-module/index.js +0 -3
  34. package/build-module/index.js.map +1 -1
  35. package/build-module/resolvers.js +22 -24
  36. package/build-module/resolvers.js.map +1 -1
  37. package/build-module/selectors.js +6 -4
  38. package/build-module/selectors.js.map +1 -1
  39. package/package.json +12 -11
  40. package/src/actions.js +27 -0
  41. package/src/batch/create-batch.js +3 -12
  42. package/src/entities.ts +8 -6
  43. package/src/entity-types/theme.ts +4 -0
  44. package/src/hooks/index.ts +4 -0
  45. package/src/hooks/test/use-entity-record.js +9 -1
  46. package/src/hooks/test/use-resource-permissions.js +28 -36
  47. package/src/hooks/use-entity-record.ts +99 -2
  48. package/src/hooks/use-resource-permissions.ts +60 -18
  49. package/src/index.js +0 -3
  50. package/src/resolvers.js +41 -36
  51. package/src/selectors.ts +6 -4
@@ -50,28 +50,24 @@ 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
+ } );
61
59
 
62
60
  // Required to make sure no updates happen outside of act()
63
61
  await act( async () => {
64
62
  jest.advanceTimersByTime( 1 );
65
63
  } );
66
64
 
67
- expect( data ).toEqual( [
68
- true,
69
- {
70
- status: 'SUCCESS',
71
- isResolving: false,
72
- canCreate: true,
73
- },
74
- ] );
65
+ expect( data ).toEqual( {
66
+ status: 'SUCCESS',
67
+ isResolving: false,
68
+ hasResolved: true,
69
+ canCreate: true,
70
+ } );
75
71
  } );
76
72
 
77
73
  it( 'retrieves the relevant permissions for a resource with a key', async () => {
@@ -85,31 +81,27 @@ describe( 'useResourcePermissions', () => {
85
81
  <TestComponent />
86
82
  </RegistryProvider>
87
83
  );
88
- expect( data ).toEqual( [
89
- false,
90
- {
91
- status: 'IDLE',
92
- isResolving: false,
93
- canCreate: false,
94
- canUpdate: false,
95
- canDelete: false,
96
- },
97
- ] );
84
+ expect( data ).toEqual( {
85
+ status: 'IDLE',
86
+ isResolving: false,
87
+ hasResolved: false,
88
+ canCreate: false,
89
+ canUpdate: false,
90
+ canDelete: false,
91
+ } );
98
92
 
99
93
  // Required to make sure no updates happen outside of act()
100
94
  await act( async () => {
101
95
  jest.advanceTimersByTime( 1 );
102
96
  } );
103
97
 
104
- expect( data ).toEqual( [
105
- true,
106
- {
107
- status: 'SUCCESS',
108
- isResolving: false,
109
- canCreate: true,
110
- canUpdate: false,
111
- canDelete: false,
112
- },
113
- ] );
98
+ expect( data ).toEqual( {
99
+ status: 'SUCCESS',
100
+ isResolving: false,
101
+ hasResolved: true,
102
+ canCreate: true,
103
+ canUpdate: false,
104
+ canDelete: false,
105
+ } );
114
106
  } );
115
107
  } );
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * WordPress dependencies
3
3
  */
4
+ import { useDispatch, useSelect } from '@wordpress/data';
4
5
  import deprecated from '@wordpress/deprecated';
6
+ import { useMemo } from '@wordpress/element';
5
7
 
6
8
  /**
7
9
  * Internal dependencies
@@ -14,11 +16,25 @@ export interface EntityRecordResolution< RecordType > {
14
16
  /** The requested entity record */
15
17
  record: RecordType | null;
16
18
 
19
+ /** The edited entity record */
20
+ editedRecord: Partial< RecordType >;
21
+
22
+ /** Apply local (in-browser) edits to the edited entity record */
23
+ edit: ( diff: Partial< RecordType > ) => void;
24
+
25
+ /** Persist the edits to the server */
26
+ save: () => Promise< void >;
27
+
17
28
  /**
18
29
  * Is the record still being resolved?
19
30
  */
20
31
  isResolving: boolean;
21
32
 
33
+ /**
34
+ * Does the record have any local edits?
35
+ */
36
+ hasEdits: boolean;
37
+
22
38
  /**
23
39
  * Is the record resolved by now?
24
40
  */
@@ -66,6 +82,60 @@ export interface Options {
66
82
  * application, the page and the resolution details will be retrieved from
67
83
  * the store state using `getEntityRecord()`, or resolved if missing.
68
84
  *
85
+ * @example
86
+ * ```js
87
+ * import { useDispatch } from '@wordpress/data';
88
+ * import { useCallback } from '@wordpress/element';
89
+ * import { __ } from '@wordpress/i18n';
90
+ * import { TextControl } from '@wordpress/components';
91
+ * import { store as noticeStore } from '@wordpress/notices';
92
+ * import { useEntityRecord } from '@wordpress/core-data';
93
+ *
94
+ * function PageRenameForm( { id } ) {
95
+ * const page = useEntityRecord( 'postType', 'page', id );
96
+ * const { createSuccessNotice, createErrorNotice } =
97
+ * useDispatch( noticeStore );
98
+ *
99
+ * const setTitle = useCallback( ( title ) => {
100
+ * page.edit( { title } );
101
+ * }, [ page.edit ] );
102
+ *
103
+ * if ( page.isResolving ) {
104
+ * return 'Loading...';
105
+ * }
106
+ *
107
+ * async function onRename( event ) {
108
+ * event.preventDefault();
109
+ * try {
110
+ * await page.save();
111
+ * createSuccessNotice( __( 'Page renamed.' ), {
112
+ * type: 'snackbar',
113
+ * } );
114
+ * } catch ( error ) {
115
+ * createErrorNotice( error.message, { type: 'snackbar' } );
116
+ * }
117
+ * }
118
+ *
119
+ * return (
120
+ * <form onSubmit={ onRename }>
121
+ * <TextControl
122
+ * label={ __( 'Name' ) }
123
+ * value={ page.editedRecord.title }
124
+ * onChange={ setTitle }
125
+ * />
126
+ * <button type="submit">{ __( 'Save' ) }</button>
127
+ * </form>
128
+ * );
129
+ * }
130
+ *
131
+ * // Rendered in the application:
132
+ * // <PageRenameForm id={ 1 } />
133
+ * ```
134
+ *
135
+ * In the above example, updating and saving the page title is handled
136
+ * via the `edit()` and `save()` mutation helpers provided by
137
+ * `useEntityRecord()`;
138
+ *
69
139
  * @return Entity record data.
70
140
  * @template RecordType
71
141
  */
@@ -75,7 +145,31 @@ export default function useEntityRecord< RecordType >(
75
145
  recordId: string | number,
76
146
  options: Options = { enabled: true }
77
147
  ): EntityRecordResolution< RecordType > {
78
- const { data: record, ...rest } = useQuerySelect(
148
+ const { editEntityRecord, saveEditedEntityRecord } =
149
+ useDispatch( coreStore );
150
+
151
+ const mutations = useMemo(
152
+ () => ( {
153
+ edit: ( record ) =>
154
+ editEntityRecord( kind, name, recordId, record ),
155
+ save: ( saveOptions: any = {} ) =>
156
+ saveEditedEntityRecord( kind, name, recordId, {
157
+ throwOnError: true,
158
+ ...saveOptions,
159
+ } ),
160
+ } ),
161
+ [ recordId ]
162
+ );
163
+
164
+ const { editedRecord, hasEdits } = useSelect(
165
+ ( select ) => ( {
166
+ editedRecord: select( coreStore ).getEditedEntityRecord(),
167
+ hasEdits: select( coreStore ).hasEditsForEntityRecord(),
168
+ } ),
169
+ [ kind, name, recordId ]
170
+ );
171
+
172
+ const { data: record, ...querySelectRest } = useQuerySelect(
79
173
  ( query ) => {
80
174
  if ( ! options.enabled ) {
81
175
  return null;
@@ -87,7 +181,10 @@ export default function useEntityRecord< RecordType >(
87
181
 
88
182
  return {
89
183
  record,
90
- ...rest,
184
+ editedRecord,
185
+ hasEdits,
186
+ ...querySelectRest,
187
+ ...mutations,
91
188
  };
92
189
  }
93
190
 
@@ -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,14 +116,12 @@ 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
+ return {
120
+ status: create.status,
121
+ isResolving: create.isResolving,
122
+ hasResolved: create.hasResolved,
123
+ canCreate: create.hasResolved && create.data,
124
+ };
92
125
  }
93
126
 
94
127
  const update = canUser( 'update', resource, id );
@@ -104,17 +137,26 @@ export default function __experimentalUseResourcePermissions< IdType = void >(
104
137
  } else if ( hasResolved ) {
105
138
  status = Status.Success;
106
139
  }
107
- return [
140
+ return {
141
+ status,
142
+ isResolving,
108
143
  hasResolved,
109
- {
110
- status,
111
- isResolving,
112
- canCreate: hasResolved && create.data,
113
- canUpdate: hasResolved && update.data,
114
- canDelete: hasResolved && _delete.data,
115
- },
116
- ];
144
+ canCreate: hasResolved && create.data,
145
+ canUpdate: hasResolved && update.data,
146
+ canDelete: hasResolved && _delete.data,
147
+ };
117
148
  },
118
149
  [ resource, id ]
119
150
  );
120
151
  }
152
+
153
+ export function __experimentalUseResourcePermissions(
154
+ resource: string,
155
+ id?: IdType
156
+ ) {
157
+ deprecated( `wp.data.__experimentalUseResourcePermissions`, {
158
+ alternative: 'wp.data.useResourcePermissions',
159
+ since: '6.1',
160
+ } );
161
+ return useResourcePermissions( resource, id );
162
+ }
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,16 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import {
5
- camelCase,
6
- compact,
7
- find,
8
- get,
9
- includes,
10
- map,
11
- mapKeys,
12
- uniq,
13
- } from 'lodash';
4
+ import { camelCase } from 'change-case';
14
5
 
15
6
  /**
16
7
  * WordPress dependencies
@@ -59,13 +50,16 @@ export const getCurrentUser =
59
50
  * @param {string} name Entity name.
60
51
  * @param {number|string} key Record's key
61
52
  * @param {Object|undefined} query Optional object of query parameters to
62
- * include with request.
53
+ * include with request. If requesting specific
54
+ * fields, fields must always include the ID.
63
55
  */
64
56
  export const getEntityRecord =
65
57
  ( kind, name, key = '', query ) =>
66
58
  async ( { select, dispatch } ) => {
67
59
  const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
68
- const entityConfig = find( configs, { kind, name } );
60
+ const entityConfig = configs.find(
61
+ ( config ) => config.name === name && config.kind === kind
62
+ );
69
63
  if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
70
64
  return;
71
65
  }
@@ -83,11 +77,13 @@ export const getEntityRecord =
83
77
  // the ID.
84
78
  query = {
85
79
  ...query,
86
- _fields: uniq( [
87
- ...( getNormalizedCommaSeparable( query._fields ) ||
88
- [] ),
89
- entityConfig.key || DEFAULT_ENTITY_KEY,
90
- ] ).join(),
80
+ _fields: [
81
+ ...new Set( [
82
+ ...( getNormalizedCommaSeparable( query._fields ) ||
83
+ [] ),
84
+ entityConfig.key || DEFAULT_ENTITY_KEY,
85
+ ] ),
86
+ ].join(),
91
87
  };
92
88
  }
93
89
 
@@ -140,13 +136,16 @@ export const getEditedEntityRecord = forwardResolver( 'getEntityRecord' );
140
136
  *
141
137
  * @param {string} kind Entity kind.
142
138
  * @param {string} name Entity name.
143
- * @param {Object?} query Query Object.
139
+ * @param {Object?} query Query Object. If requesting specific fields, fields
140
+ * must always include the ID.
144
141
  */
145
142
  export const getEntityRecords =
146
143
  ( kind, name, query = {} ) =>
147
144
  async ( { dispatch } ) => {
148
145
  const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
149
- const entityConfig = find( configs, { kind, name } );
146
+ const entityConfig = configs.find(
147
+ ( config ) => config.name === name && config.kind === kind
148
+ );
150
149
  if ( ! entityConfig || entityConfig?.__experimentalNoFetch ) {
151
150
  return;
152
151
  }
@@ -164,11 +163,13 @@ export const getEntityRecords =
164
163
  // the ID.
165
164
  query = {
166
165
  ...query,
167
- _fields: uniq( [
168
- ...( getNormalizedCommaSeparable( query._fields ) ||
169
- [] ),
170
- entityConfig.key || DEFAULT_ENTITY_KEY,
171
- ] ).join(),
166
+ _fields: [
167
+ ...new Set( [
168
+ ...( getNormalizedCommaSeparable( query._fields ) ||
169
+ [] ),
170
+ entityConfig.key || DEFAULT_ENTITY_KEY,
171
+ ] ),
172
+ ].join(),
172
173
  };
173
174
  }
174
175
 
@@ -313,8 +314,9 @@ export const canUser =
313
314
  // return the expected result in the native version. Instead, API requests
314
315
  // only return the result, without including response properties like the headers.
315
316
  const allowHeader = response.headers?.get( 'allow' );
316
- const key = compact( [ action, resource, id ] ).join( '/' );
317
- const isAllowed = includes( allowHeader, method );
317
+ const key = [ action, resource, id ].filter( Boolean ).join( '/' );
318
+ const isAllowed =
319
+ allowHeader?.includes?.( method ) || allowHeader?.allow === method;
318
320
  dispatch.receiveUserPermission( key, isAllowed );
319
321
  };
320
322
 
@@ -330,7 +332,9 @@ export const canUserEditEntityRecord =
330
332
  ( kind, name, recordId ) =>
331
333
  async ( { dispatch } ) => {
332
334
  const configs = await dispatch( getOrLoadEntitiesConfig( kind ) );
333
- const entityConfig = find( configs, { kind, name } );
335
+ const entityConfig = configs.find(
336
+ ( config ) => config.name === name && config.kind === kind
337
+ );
334
338
  if ( ! entityConfig ) {
335
339
  return;
336
340
  }
@@ -434,13 +438,9 @@ export const __experimentalGetCurrentGlobalStylesId =
434
438
  'theme',
435
439
  { status: 'active' }
436
440
  );
437
- const globalStylesURL = get( activeThemes, [
438
- 0,
439
- '_links',
440
- 'wp:user-global-styles',
441
- 0,
442
- 'href',
443
- ] );
441
+ const globalStylesURL =
442
+ activeThemes?.[ 0 ]?._links?.[ 'wp:user-global-styles' ]?.[ 0 ]
443
+ ?.href;
444
444
  if ( globalStylesURL ) {
445
445
  const globalStylesObject = await apiFetch( {
446
446
  url: globalStylesURL,
@@ -483,8 +483,13 @@ export const getBlockPatterns =
483
483
  const restPatterns = await apiFetch( {
484
484
  path: '/wp/v2/block-patterns/patterns',
485
485
  } );
486
- const patterns = map( restPatterns, ( pattern ) =>
487
- 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
+ )
488
493
  );
489
494
  dispatch( { type: 'RECEIVE_BLOCK_PATTERNS', patterns } );
490
495
  };
package/src/selectors.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * External dependencies
3
3
  */
4
4
  import createSelector from 'rememo';
5
- import { set, map, find, get, filter, compact } from 'lodash';
5
+ import { set, map, find, get, filter } from 'lodash';
6
6
 
7
7
  /**
8
8
  * WordPress dependencies
@@ -296,7 +296,8 @@ interface GetEntityRecord {
296
296
  * @param kind Entity kind.
297
297
  * @param name Entity name.
298
298
  * @param key Record's key
299
- * @param query Optional query.
299
+ * @param query Optional query. If requesting specific
300
+ * fields, fields must always include the ID.
300
301
  *
301
302
  * @return Record.
302
303
  */
@@ -526,7 +527,8 @@ interface GetEntityRecords {
526
527
  * @param state State tree
527
528
  * @param kind Entity kind.
528
529
  * @param name Entity name.
529
- * @param query Optional terms query.
530
+ * @param query Optional terms query. If requesting specific
531
+ * fields, fields must always include the ID.
530
532
  *
531
533
  * @return Records.
532
534
  */
@@ -1115,7 +1117,7 @@ export function canUser(
1115
1117
  resource: string,
1116
1118
  id?: GenericRecordKey
1117
1119
  ): boolean | undefined {
1118
- const key = compact( [ action, resource, id ] ).join( '/' );
1120
+ const key = [ action, resource, id ].filter( Boolean ).join( '/' );
1119
1121
  return get( state, [ 'userPermissions', key ] );
1120
1122
  }
1121
1123