@wordpress/core-data 4.11.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 (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 +72 -11
  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 +68 -10
  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 +49 -1
  46. package/src/hooks/test/use-resource-permissions.js +32 -36
  47. package/src/hooks/use-entity-record.ts +107 -2
  48. package/src/hooks/use-resource-permissions.ts +82 -20
  49. package/src/index.js +0 -3
  50. package/src/resolvers.js +41 -36
  51. package/src/selectors.ts +6 -4
@@ -1,8 +1,3 @@
1
- /**
2
- * External dependencies
3
- */
4
- import { zip } from 'lodash';
5
-
6
1
  /**
7
2
  * Internal dependencies
8
3
  */
@@ -138,12 +133,8 @@ export default function createBatch( processor = defaultProcessor ) {
138
133
 
139
134
  let isSuccess = true;
140
135
 
141
- for ( const pair of zip( results, queue ) ) {
142
- /** @type {{error?: unknown, output?: unknown}} */
143
- const result = pair[ 0 ];
144
-
145
- /** @type {{resolve: (value: any) => void; reject: (error: any) => void} | undefined} */
146
- const queueItem = pair[ 1 ];
136
+ results.forEach( ( result, key ) => {
137
+ const queueItem = queue[ key ];
147
138
 
148
139
  if ( result?.error ) {
149
140
  queueItem?.reject( result.error );
@@ -151,7 +142,7 @@ export default function createBatch( processor = defaultProcessor ) {
151
142
  } else {
152
143
  queueItem?.resolve( result?.output ?? result );
153
144
  }
154
- }
145
+ } );
155
146
 
156
147
  queue = [];
157
148
 
package/src/entities.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { upperFirst, camelCase, map, find, get, startCase } from 'lodash';
4
+ import { capitalCase, pascalCase } from 'change-case';
5
+ import { map, find, get } from 'lodash';
5
6
 
6
7
  /**
7
8
  * WordPress dependencies
@@ -457,7 +458,9 @@ async function loadPostTypeEntities() {
457
458
  getTitle: ( record ) =>
458
459
  record?.title?.rendered ||
459
460
  record?.title ||
460
- ( isTemplate ? startCase( record.slug ) : String( record.id ) ),
461
+ ( isTemplate
462
+ ? capitalCase( record.slug ?? '' )
463
+ : String( record.id ) ),
461
464
  __unstablePrePersist: isTemplate ? undefined : prePersistPostType,
462
465
  __unstable_rest_base: postType.rest_base,
463
466
  };
@@ -511,12 +514,11 @@ export const getMethodName = (
511
514
  usePlural = false
512
515
  ) => {
513
516
  const entityConfig = find( rootEntitiesConfig, { kind, name } );
514
- const kindPrefix = kind === 'root' ? '' : upperFirst( camelCase( kind ) );
515
- const nameSuffix =
516
- upperFirst( camelCase( name ) ) + ( usePlural ? 's' : '' );
517
+ const kindPrefix = kind === 'root' ? '' : pascalCase( kind );
518
+ const nameSuffix = pascalCase( name ) + ( usePlural ? 's' : '' );
517
519
  const suffix =
518
520
  usePlural && 'plural' in entityConfig! && entityConfig?.plural
519
- ? upperFirst( camelCase( entityConfig.plural ) )
521
+ ? pascalCase( entityConfig.plural )
520
522
  : nameSuffix;
521
523
  return `${ prefix }${ kindPrefix }${ suffix }`;
522
524
  };
@@ -78,6 +78,10 @@ declare module './base-entity-records' {
78
78
  * Whether theme opts in to wide alignment CSS class.
79
79
  */
80
80
  'align-wide': boolean;
81
+ /**
82
+ * Whether appearanceTools are enabled in Global Styles.
83
+ */
84
+ 'appearance-tools': boolean;
81
85
  /**
82
86
  * Whether posts and comments RSS feed links are added to head.
83
87
  */
@@ -6,3 +6,7 @@ export {
6
6
  default as useEntityRecords,
7
7
  __experimentalUseEntityRecords,
8
8
  } from './use-entity-records';
9
+ export {
10
+ default as useResourcePermissions,
11
+ __experimentalUseResourcePermissions,
12
+ } from './use-resource-permissions';
@@ -50,7 +50,11 @@ describe( 'useEntityRecord', () => {
50
50
  );
51
51
 
52
52
  expect( data ).toEqual( {
53
- records: undefined,
53
+ edit: expect.any( Function ),
54
+ editedRecord: {},
55
+ hasEdits: false,
56
+ record: undefined,
57
+ save: expect.any( Function ),
54
58
  hasResolved: false,
55
59
  isResolving: false,
56
60
  status: 'IDLE',
@@ -66,10 +70,54 @@ describe( 'useEntityRecord', () => {
66
70
  } );
67
71
 
68
72
  expect( data ).toEqual( {
73
+ edit: expect.any( Function ),
74
+ editedRecord: { hello: 'world', id: 1 },
75
+ hasEdits: false,
69
76
  record: { hello: 'world', id: 1 },
77
+ save: expect.any( Function ),
70
78
  hasResolved: true,
71
79
  isResolving: false,
72
80
  status: 'SUCCESS',
73
81
  } );
74
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 },
106
+ hasEdits: false,
107
+ record: { hello: 'world', id: 1 },
108
+ save: expect.any( Function ),
109
+ hasResolved: true,
110
+ isResolving: false,
111
+ status: 'SUCCESS',
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 } );
122
+ } );
75
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
  } );
@@ -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,39 @@ 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
+ kind,
168
+ name,
169
+ recordId
170
+ ),
171
+ hasEdits: select( coreStore ).hasEditsForEntityRecord(
172
+ kind,
173
+ name,
174
+ recordId
175
+ ),
176
+ } ),
177
+ [ kind, name, recordId ]
178
+ );
179
+
180
+ const { data: record, ...querySelectRest } = useQuerySelect(
79
181
  ( query ) => {
80
182
  if ( ! options.enabled ) {
81
183
  return null;
@@ -87,7 +189,10 @@ export default function useEntityRecord< RecordType >(
87
189
 
88
190
  return {
89
191
  record,
90
- ...rest,
192
+ editedRecord,
193
+ hasEdits,
194
+ ...querySelectRest,
195
+ ...mutations,
91
196
  };
92
197
  }
93
198
 
@@ -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';