Skip to content

Commit

Permalink
Core Data: Resolve user capabilities when fetching an entity (WordPre…
Browse files Browse the repository at this point in the history
…ss#63430)

* Core Data: Resolve user capabilities when fetching an entity
* Mark canUser selector as resolved
* Fix unit tests
* Dedupe logic
* Showcase: Update Pattern block to benefit from new user permission resolutions
* Clarify comment

Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>
  • Loading branch information
5 people authored and carstingaxion committed Jul 18, 2024
1 parent bdfb6f6 commit 49e6720
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 93 deletions.
89 changes: 58 additions & 31 deletions packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,51 @@ export default function ReusableBlockEditRecursionWrapper( props ) {
);
}

function ReusableBlockControl( {
recordId,
canOverrideBlocks,
hasContent,
handleEditOriginal,
resetContent,
} ) {
const canUserEdit = useSelect(
( select ) =>
!! select( coreStore ).canUser( 'update', {
kind: 'postType',
name: 'wp_block',
id: recordId,
} ),
[ recordId ]
);

return (
<>
{ canUserEdit && !! handleEditOriginal && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton onClick={ handleEditOriginal }>
{ __( 'Edit original' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
) }

{ canOverrideBlocks && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ resetContent }
disabled={ ! hasContent }
>
{ __( 'Reset' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
) }
</>
);
}

function ReusableBlockEdit( {
name,
attributes: { ref, content },
Expand All @@ -143,29 +188,20 @@ function ReusableBlockEdit( {

const {
innerBlocks,
userCanEdit,
onNavigateToEntityRecord,
editingMode,
hasPatternOverridesSource,
} = useSelect(
( select ) => {
const { canUser } = select( coreStore );
const {
getBlocks,
getSettings,
getBlockEditingMode: _getBlockEditingMode,
} = select( blockEditorStore );
const { getBlockBindingsSource } = unlock( select( blocksStore ) );
const canEdit = canUser( 'update', {
kind: 'postType',
name: 'wp_block',
id: ref,
} );

// For editing link to the site editor if the theme and user permissions support it.
return {
innerBlocks: getBlocks( patternClientId ),
userCanEdit: canEdit,
getBlockEditingMode: _getBlockEditingMode,
onNavigateToEntityRecord:
getSettings().onNavigateToEntityRecord,
Expand All @@ -175,7 +211,7 @@ function ReusableBlockEdit( {
),
};
},
[ patternClientId, ref ]
[ patternClientId ]
);

// Sync the editing mode of the pattern block with the inner blocks.
Expand Down Expand Up @@ -256,27 +292,18 @@ function ReusableBlockEdit( {

return (
<>
{ userCanEdit && onNavigateToEntityRecord && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton onClick={ handleEditOriginal }>
{ __( 'Edit original' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
) }

{ canOverrideBlocks && (
<BlockControls>
<ToolbarGroup>
<ToolbarButton
onClick={ resetContent }
disabled={ ! content }
>
{ __( 'Reset' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
{ hasResolved && (
<ReusableBlockControl
recordId={ ref }
canOverrideBlocks={ canOverrideBlocks }
hasContent={ !! content }
handleEditOriginal={
onNavigateToEntityRecord
? handleEditOriginal
: undefined
}
resetContent={ resetContent }
/>
) }

{ children === null ? (
Expand Down
8 changes: 6 additions & 2 deletions packages/block-library/src/block/test/edit.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ describe( 'Synced patterns', () => {
if ( path.startsWith( endpoint ) ) {
response = getMockedReusableBlock( id );
}
return Promise.resolve( response );
return Promise.resolve( {
json: () => Promise.resolve( response ),
} );
} );

const screen = await initializeEditor( {
Expand Down Expand Up @@ -229,7 +231,9 @@ describe( 'Synced patterns', () => {
response.content.raw = `<!-- wp:image {"id":1,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://cldup.com/cXyG__fTLN.jpg" alt="" class="wp-image-1"/></figure>
<!-- /wp:image -->`;
return Promise.resolve( response );
return Promise.resolve( {
json: () => Promise.resolve( response ),
} );
} );

const screen = await initializeEditor( {
Expand Down
1 change: 1 addition & 0 deletions packages/block-library/src/image/test/edit.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function mockGetMedia( media ) {
const FETCH_MEDIA = {
request: {
path: `/wp/v2/media/1?context=edit`,
parse: false,
},
response: {
source_url: 'https://cldup.com/cXyG__fTLN.jpg',
Expand Down
8 changes: 5 additions & 3 deletions packages/core-data/src/hooks/test/use-entity-record.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ describe( 'useEntityRecord', () => {
} );

const TEST_RECORD = { id: 1, hello: 'world' };
const TEST_RECORD_RESPONSE = { json: () => Promise.resolve( TEST_RECORD ) };

it( 'resolves the entity record when missing from the state', async () => {
// Provide response
triggerFetch.mockImplementation( () => TEST_RECORD );
triggerFetch.mockImplementation( () => TEST_RECORD_RESPONSE );

let data;
const TestComponent = () => {
Expand Down Expand Up @@ -60,6 +61,7 @@ describe( 'useEntityRecord', () => {
await waitFor( () =>
expect( triggerFetch ).toHaveBeenCalledWith( {
path: '/wp/v2/widgets/1?context=edit',
parse: false,
} )
);

Expand All @@ -79,7 +81,7 @@ describe( 'useEntityRecord', () => {

it( 'applies edits to the entity record', async () => {
// Provide response
triggerFetch.mockImplementation( () => TEST_RECORD );
triggerFetch.mockImplementation( () => TEST_RECORD_RESPONSE );

let widget;
const TestComponent = () => {
Expand Down Expand Up @@ -119,7 +121,7 @@ describe( 'useEntityRecord', () => {
} );

it( 'does not resolve entity record when disabled via options', async () => {
triggerFetch.mockImplementation( () => TEST_RECORD );
triggerFetch.mockImplementation( () => TEST_RECORD_RESPONSE );

let data;
const TestComponent = ( { enabled } ) => {
Expand Down
68 changes: 36 additions & 32 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import apiFetch from '@wordpress/api-fetch';
*/
import { STORE_NAME } from './name';
import { getOrLoadEntitiesConfig, DEFAULT_ENTITY_KEY } from './entities';
import { forwardResolver, getNormalizedCommaSeparable } from './utils';
import {
forwardResolver,
getNormalizedCommaSeparable,
getUserPermissionCacheKey,
getUserPermissionsFromResponse,
ALLOWED_RESOURCE_ACTIONS,
} from './utils';
import { getSyncProvider } from './sync';
import { fetchBlockPatterns } from './fetch';

Expand Down Expand Up @@ -58,7 +64,7 @@ export const getCurrentUser =
*/
export const getEntityRecord =
( kind, name, key = '', query ) =>
async ( { select, dispatch } ) => {
async ( { select, dispatch, registry } ) => {
const configs = await dispatch( getOrLoadEntitiesConfig( kind, name ) );
const entityConfig = configs.find(
( config ) => config.name === name && config.kind === kind
Expand Down Expand Up @@ -165,8 +171,29 @@ export const getEntityRecord =
}
}

const record = await apiFetch( { path } );
dispatch.receiveEntityRecords( kind, name, record, query );
const response = await apiFetch( { path, parse: false } );
const record = await response.json();
const permissions = getUserPermissionsFromResponse( response );

registry.batch( () => {
dispatch.receiveEntityRecords( kind, name, record, query );

for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
const permissionKey = getUserPermissionCacheKey(
action,
{ kind, name, id: key }
);

dispatch.receiveUserPermission(
permissionKey,
permissions[ action ]
);
dispatch.finishResolution( 'canUser', [
action,
{ kind, name, id: key },
] );
}
} );
}
} finally {
dispatch.__unstableReleaseStoreLock( lock );
Expand Down Expand Up @@ -355,9 +382,7 @@ export const getEmbedPreview =
export const canUser =
( requestedAction, resource, id ) =>
async ( { dispatch, registry } ) => {
const retrievedActions = [ 'create', 'read', 'update', 'delete' ];

if ( ! retrievedActions.includes( requestedAction ) ) {
if ( ! ALLOWED_RESOURCE_ACTIONS.includes( requestedAction ) ) {
throw new Error( `'${ requestedAction }' is not a valid action.` );
}

Expand Down Expand Up @@ -389,7 +414,7 @@ export const canUser =
const { hasStartedResolution } = registry.select( STORE_NAME );

// Prevent resolving the same resource twice.
for ( const relatedAction of retrievedActions ) {
for ( const relatedAction of ALLOWED_RESOURCE_ACTIONS ) {
if ( relatedAction === requestedAction ) {
continue;
}
Expand All @@ -416,31 +441,10 @@ export const canUser =
return;
}

// Optional chaining operator is used here because the API requests don't
// return the expected result in the native version. Instead, API requests
// only return the result, without including response properties like the headers.
const allowedMethods = response.headers?.get( 'allow' ) || '';

const permissions = {};
const methods = {
create: 'POST',
read: 'GET',
update: 'PUT',
delete: 'DELETE',
};
for ( const [ actionName, methodName ] of Object.entries( methods ) ) {
permissions[ actionName ] = allowedMethods.includes( methodName );
}

const permissions = getUserPermissionsFromResponse( response );
registry.batch( () => {
for ( const action of retrievedActions ) {
const key = (
typeof resource === 'object'
? [ action, resource.kind, resource.name, resource.id ]
: [ action, resource, id ]
)
.filter( Boolean )
.join( '/' );
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
const key = getUserPermissionCacheKey( action, resource, id );

dispatch.receiveUserPermission( key, permissions[ action ] );

Expand Down
9 changes: 2 additions & 7 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isRawAttribute,
setNestedValue,
isNumericID,
getUserPermissionCacheKey,
} from './utils';
import type * as ET from './entity-types';
import type { UndoManager } from '@wordpress/undo-manager';
Expand Down Expand Up @@ -1156,13 +1157,7 @@ export function canUser(
return false;
}

const key = (
isEntity
? [ action, resource.kind, resource.name, resource.id ]
: [ action, resource, id ]
)
.filter( Boolean )
.join( '/' );
const key = getUserPermissionCacheKey( action, resource, id );

return state.userPermissions[ key ];
}
Expand Down
Loading

0 comments on commit 49e6720

Please sign in to comment.