Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit c86f561
Author: Adam Zieliński <[email protected]>
Date:   Wed Feb 16 12:17:33 2022 +0100

    Move the return type to TS

commit eefec13
Author: Adam Zieliński <[email protected]>
Date:   Wed Feb 16 12:16:36 2022 +0100

    Adjust TS types

commit 12c7433
Author: Adam Zieliński <[email protected]>
Date:   Wed Feb 16 12:13:22 2022 +0100

    Make useResourcePermissions return a tuple and make hasResolved the first item of the tuple forces the users to think about that variable and it's not easy to accidentally miss it.

commit caa48a7
Author: Adam Zieliński <[email protected]>
Date:   Tue Feb 15 17:03:31 2022 +0100

    Fix typo in the tests

commit 33cdb4e
Author: Adam Zieliński <[email protected]>
Date:   Mon Feb 14 15:05:09 2022 +0100

    Expose __experimentalUseResourcePermissions as a public API

commit cf30c5a
Author: Adam Zieliński <[email protected]>
Date:   Mon Feb 14 14:58:42 2022 +0100

    Distinguish between global and local resoluion

commit 0db7bd2
Author: Adam Zieliński <[email protected]>
Date:   Mon Feb 14 14:42:36 2022 +0100

    Propose useResourcePermissions hook

commit 2d5e270
Author: Adam Zieliński <[email protected]>
Date:   Mon Feb 14 14:48:09 2022 +0100

    Move the status computation inside the enriched selectors

commit e7ac34e
Author: Adam Zieliński <[email protected]>
Date:   Mon Feb 14 14:01:11 2022 +0100

    Propose useEntityRecords
  • Loading branch information
adamziel committed May 18, 2022
1 parent b495580 commit 31de2ce
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 0 deletions.
117 changes: 117 additions & 0 deletions packages/core-data/src/hooks/test/use-resource-permissions.js
Original file line number Diff line number Diff line change
@@ -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 <div />;
};
render(
<RegistryProvider value={ registry }>
<TestComponent />
</RegistryProvider>
);
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 <div />;
};
render(
<RegistryProvider value={ registry }>
<TestComponent />
</RegistryProvider>
);
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,
},
] );
} );
} );
120 changes: 120 additions & 0 deletions packages/core-data/src/hooks/use-resource-permissions.ts
Original file line number Diff line number Diff line change
@@ -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 (
* <div>
* {canCreate ? (<button>+ Create a new page</button>) : false}
* // ...
* </div>
* );
* }
*
* // Rendered in the application:
* // <PagesList />
* ```
*
* 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 ]
);
}
3 changes: 3 additions & 0 deletions packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 31de2ce

Please sign in to comment.