diff --git a/packages/core-data/src/hooks/test/use-resource-permissions.js b/packages/core-data/src/hooks/test/use-resource-permissions.js new file mode 100644 index 00000000000000..d1d41db3ddf360 --- /dev/null +++ b/packages/core-data/src/hooks/test/use-resource-permissions.js @@ -0,0 +1,117 @@ +/** + * WordPress dependencies + */ +import triggerFetch from '@wordpress/api-fetch'; +import { createRegistry, RegistryProvider } from '@wordpress/data'; + +jest.mock( '@wordpress/api-fetch' ); + +/** + * External dependencies + */ +import { act, render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { store as coreDataStore } from '../../index'; +import useResourcePermissions from '../use-resource-permissions'; + +describe( 'useResourcePermissions', () => { + let registry; + beforeEach( () => { + jest.useFakeTimers(); + + registry = createRegistry(); + registry.register( coreDataStore ); + } ); + + afterEach( () => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + } ); + + it( 'retrieves the relevant permissions for a key-less resource', async () => { + triggerFetch.mockImplementation( () => ( { + headers: { + Allow: 'POST', + }, + } ) ); + let data; + const TestComponent = () => { + data = useResourcePermissions( 'widgets' ); + return
; + }; + render( + + + + ); + expect( data ).toEqual( [ + false, + { + status: 'IDLE', + isResolving: false, + canCreate: false, + }, + ] ); + + // Required to make sure no updates happen outside of act() + await act( async () => { + jest.advanceTimersByTime( 1 ); + } ); + + expect( data ).toEqual( [ + true, + { + status: 'SUCCESS', + isResolving: false, + canCreate: true, + }, + ] ); + } ); + + it( 'retrieves the relevant permissions for a resource with a key', async () => { + triggerFetch.mockImplementation( () => ( { + headers: { + Allow: 'POST', + }, + } ) ); + let data; + const TestComponent = () => { + data = useResourcePermissions( 'widgets', 1 ); + return
; + }; + render( + + + + ); + expect( data ).toEqual( [ + false, + { + status: 'IDLE', + isResolving: false, + canCreate: false, + canUpdate: false, + canDelete: false, + }, + ] ); + + // Required to make sure no updates happen outside of act() + await act( async () => { + jest.advanceTimersByTime( 1 ); + } ); + + expect( data ).toEqual( [ + true, + { + status: 'SUCCESS', + isResolving: false, + canCreate: true, + canUpdate: false, + canDelete: false, + }, + ] ); + } ); +} ); diff --git a/packages/core-data/src/hooks/use-resource-permissions.ts b/packages/core-data/src/hooks/use-resource-permissions.ts new file mode 100644 index 00000000000000..122eadc5759805 --- /dev/null +++ b/packages/core-data/src/hooks/use-resource-permissions.ts @@ -0,0 +1,120 @@ +/** + * Internal dependencies + */ +import { store as coreStore } from '../'; +import { Status } from './constants'; +import useQuerySelect from './use-query-select'; + +interface GlobalResourcePermissionsResolution { + /** Can the current user create new resources of this type? */ + canCreate: boolean; +} +interface SpecificResourcePermissionsResolution { + /** Can the current user update resources of this type? */ + canUpdate: boolean; + /** Can the current user delete resources of this type? */ + canDelete: boolean; +} +interface ResolutionDetails { + /** Resolution status */ + status: Status; + /** + * Is the data still being resolved? + */ + isResolving: boolean; +} + +/** + * Is the data resolved by now? + */ +type HasResolved = boolean; + +type ResourcePermissionsResolution< IdType > = [ + HasResolved, + ResolutionDetails & + GlobalResourcePermissionsResolution & + ( IdType extends void ? SpecificResourcePermissionsResolution : {} ) +]; + +/** + * Resolves resource permissions. + * + * @param resource The resource in question, e.g. media. + * @param id ID of a specific resource entry, if needed, e.g. 10. + * + * @example + * ```js + * import { useResourcePermissions } from '@wordpress/core-data'; + * + * function PagesList() { + * const { canCreate, isResolving } = useResourcePermissions( 'pages' ); + * + * if ( isResolving ) { + * return 'Loading ...'; + * } + * + * return ( + *
+ * {canCreate ? () : false} + * // ... + *
+ * ); + * } + * + * // Rendered in the application: + * // + * ``` + * + * In the above example, when `PagesList` is rendered into an + * application, the appropriate permissions and the resolution details will be retrieved from + * the store state using `canUser()`, or resolved if missing. + * + * @return Entity records data. + * @template IdType + */ +export default function __experimentalUseResourcePermissions< IdType = void >( + resource: string, + id: IdType +): ResourcePermissionsResolution< IdType > { + return useQuerySelect( + ( resolve ) => { + const { canUser } = resolve( coreStore ); + const create = canUser( 'create', resource ); + if ( ! id ) { + return [ + create.hasResolved, + { + status: create.status, + isResolving: create.isResolving, + canCreate: create.hasResolved && create.data, + }, + ]; + } + + const update = canUser( 'update', resource, id ); + const _delete = canUser( 'delete', resource, id ); + const isResolving = + create.isResolving || update.isResolving || _delete.isResolving; + const hasResolved = + create.hasResolved && update.hasResolved && _delete.hasResolved; + + let status = Status.Idle; + if ( isResolving ) { + status = Status.Resolving; + } else if ( hasResolved ) { + status = Status.Success; + } + return [ + hasResolved, + { + status, + isResolving, + canCreate: hasResolved && create.data, + canUpdate: hasResolved && update.data, + canDelete: hasResolved && _delete.data, + }, + ]; + }, + [ resource, id ] + ); +} diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index 05cace07c992b4..6914f376d72e43 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -68,6 +68,9 @@ export const store = createReduxStore( STORE_NAME, storeConfig() ); register( store ); export { default as EntityProvider } from './entity-provider'; +export { default as useEntityRecord } from './hooks/use-entity-record'; +export { default as useEntityRecords } from './hooks/use-entity-records'; +export { default as useResourcePermissions } from './hooks/use-resource-permissions'; export * from './entity-provider'; export * from './entity-types'; export * from './fetch';