Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
noisysocks committed Nov 28, 2018
1 parent f97617f commit a1856af
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 24 deletions.
3 changes: 2 additions & 1 deletion packages/block-library/src/block/edit-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class ReusableBlockEditPanel extends Component {
}

render() {
const { isEditing, title, isSaving, onEdit, instanceId } = this.props;
const { isEditing, title, isSaving, isEditDisabled, onEdit, instanceId } = this.props;

return (
<Fragment>
Expand All @@ -66,6 +66,7 @@ class ReusableBlockEditPanel extends Component {
ref={ this.editButton }
isLarge
className="reusable-block-edit-panel__button"
disabled={ isEditDisabled }
onClick={ onEdit }
>
{ __( 'Edit' ) }
Expand Down
6 changes: 5 additions & 1 deletion packages/block-library/src/block/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class ReusableBlockEdit extends Component {
}

render() {
const { isSelected, reusableBlock, block, isFetching, isSaving } = this.props;
const { isSelected, reusableBlock, block, isFetching, isSaving, canUpdateBlock } = this.props;
const { isEditing, title, changedAttributes } = this.state;

if ( ! reusableBlock && isFetching ) {
Expand Down Expand Up @@ -130,6 +130,7 @@ class ReusableBlockEdit extends Component {
isEditing={ isEditing }
title={ title !== null ? title : reusableBlock.title }
isSaving={ isSaving && ! reusableBlock.isTemporary }
isEditDisabled={ ! canUpdateBlock }
onEdit={ this.startEditing }
onChangeTitle={ this.setTitle }
onSave={ this.save }
Expand All @@ -151,6 +152,8 @@ export default compose( [
__experimentalIsSavingReusableBlock: isSavingReusableBlock,
getBlock,
} = select( 'core/editor' );
const { canUser } = select( 'core' );

const { ref } = ownProps.attributes;
const reusableBlock = getReusableBlock( ref );

Expand All @@ -159,6 +162,7 @@ export default compose( [
isFetching: isFetchingReusableBlock( ref ),
isSaving: isSavingReusableBlock( ref ),
block: reusableBlock ? getBlock( reusableBlock.clientId ) : null,
canUpdateBlock: canUser( 'update', 'blocks', ref ),
};
} ),
withDispatch( ( dispatch, ownProps ) => {
Expand Down
22 changes: 20 additions & 2 deletions packages/core-data/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,25 @@ export function* saveEntityRecord( kind, name, record ) {
*/
export function receiveUploadPermissions( hasUploadPermissions ) {
return {
type: 'RECEIVE_UPLOAD_PERMISSIONS',
hasUploadPermissions,
type: 'RECEIVE_USER_PERMISSIONS',
key: 'create/media',
isAllowed: hasUploadPermissions,
};
}

/**
* Returns an action object used in signalling that the current user has
* permission to perform an action on a REST resource.
*
* @param {string} key A key that represents the action and REST resource.
* @param {boolean} isAllowed Whether or not the user can perform the action.
*
* @return {Object} Action object.
*/
export function receiveUserPermissions( key, isAllowed ) {
return {
type: 'RECEIVE_USER_PERMISSIONS',
key,
isAllowed,
};
}
18 changes: 11 additions & 7 deletions packages/core-data/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,17 +218,21 @@ export function embedPreviews( state = {}, action ) {
}

/**
* Reducer managing Upload permissions.
* State which tracks whether the user can perform an action on a REST
* resource.
*
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
* @param {Object} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Object} Updated state.
*/
export function hasUploadPermissions( state = {}, action ) {
export function userPermissions( state = {}, action ) {
switch ( action.type ) {
case 'RECEIVE_UPLOAD_PERMISSIONS':
return action.hasUploadPermissions;
case 'RECEIVE_USER_PERMISSIONS':
return {
...state,
[ action.key ]: action.isAllowed,
};
}

return state;
Expand All @@ -241,5 +245,5 @@ export default combineReducers( {
themeSupports,
entities,
embedPreviews,
hasUploadPermissions,
userPermissions,
} );
49 changes: 45 additions & 4 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { find, includes, get, hasIn } from 'lodash';
import { find, includes, get, hasIn, compact } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -16,7 +16,7 @@ import {
receiveEntityRecords,
receiveThemeSupports,
receiveEmbedPreview,
receiveUploadPermissions,
receiveUserPermissions,
} from './actions';
import { getKindEntities } from './entities';
import { apiFetch } from './controls';
Expand Down Expand Up @@ -103,7 +103,46 @@ export function* getEmbedPreview( url ) {
* Requests Upload Permissions from the REST API.
*/
export function* hasUploadPermissions() {
const response = yield apiFetch( { path: '/wp/v2/media', method: 'OPTIONS', parse: false } );
yield* canUser( 'create', 'media' );
}

/**
* Checks whether the current user can perform the given action on the given
* REST resource.
*
* @param {string} action Action to check. One of: 'create', 'read', 'update',
* 'delete'.
* @param {string} resource REST resource to check, e.g. 'media' or 'posts'.
* @param {?string} id ID of the rest resource to check.
*/
export function* canUser( action, resource, id ) {
const methods = {
create: 'POST',
read: 'GET',
update: 'PUT',
delete: 'DELETE',
};

const method = methods[ action ];
if ( ! method ) {
throw new Error( `'${ action }' is not a valid action` );
}

const path = id ? `/wp/v2/${ resource }/${ id }` : `/wp/v2/${ resource }`;

let response;
try {
response = yield apiFetch( {
path,
// Ideally this would always be an OPTIONS request, but unfortunately there's
// a bug in the REST API which causes the Allow header to not be sent on
// OPTIONS requests to /posts/:id routes.
method: id ? 'GET' : 'OPTIONS',
parse: false,
} );
} catch ( error ) {
return;
}

let allowHeader;
if ( hasIn( response, [ 'headers', 'get' ] ) ) {
Expand All @@ -116,5 +155,7 @@ export function* hasUploadPermissions() {
allowHeader = get( response, [ 'headers', 'Allow' ], '' );
}

yield receiveUploadPermissions( includes( allowHeader, 'POST' ) );
const key = compact( [ action, resource, id ] ).join( '/' );
const isAllowed = includes( allowHeader, method );
yield receiveUserPermissions( key, isAllowed );
}
21 changes: 19 additions & 2 deletions packages/core-data/src/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import createSelector from 'rememo';
import { map, find, get, filter } from 'lodash';
import { map, find, get, filter, compact } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -178,5 +178,22 @@ export function isPreviewEmbedFallback( state, url ) {
* @return {boolean} Upload Permissions.
*/
export function hasUploadPermissions( state ) {
return state.hasUploadPermissions;
return canUser( state, 'create', 'media' );
}

/**
* Returns whether the current user can perform the given action on the given
* REST resource.
*
* @param {Object} state Data state.
* @param {string} action Action to check. One of: 'create', 'read', 'update',
* 'delete'.
* @param {string} resource REST resource to check, e.g. 'media' or 'posts'.
* @param {?string} id ID of the rest resource to check.
*
* @return {boolean} Whether or not the user can perform the action.
*/
export function canUser( state, action, resource, id ) {
const key = compact( [ action, resource, id ] ).join( '/' );
return get( state, [ 'userPermissions', key ], true );
}
28 changes: 28 additions & 0 deletions packages/core-data/src/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getEntityRecords,
getEmbedPreview,
isPreviewEmbedFallback,
canUser,
} from '../selectors';

describe( 'getEntityRecord', () => {
Expand Down Expand Up @@ -117,3 +118,30 @@ describe( 'isPreviewEmbedFallback()', () => {
expect( isPreviewEmbedFallback( state, 'http://example.com/' ) ).toEqual( true );
} );
} );

describe( 'canUser', () => {
it( 'returns true by default', () => {
const state = deepFreeze( {
userPermissions: {},
} );
expect( canUser( state, 'create', 'media' ) ).toBe( true );
} );

it( 'returns whether an action can be performed', () => {
const state = deepFreeze( {
userPermissions: {
'create/media': false,
},
} );
expect( canUser( state, 'create', 'media' ) ).toBe( false );
} );

it( 'returns whether an action can be performed for a given resource', () => {
const state = deepFreeze( {
userPermissions: {
'create/media/123': false,
},
} );
expect( canUser( state, 'create', 'media', 123 ) ).toBe( false );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { compose } from '@wordpress/compose';
export function ReusableBlockConvertButton( {
isVisible,
isStaticBlock,
canCreateBlocks,
onConvertToStatic,
onConvertToReusable,
} ) {
Expand All @@ -29,6 +30,7 @@ export function ReusableBlockConvertButton( {
<MenuItem
className="editor-block-settings-menu__control"
icon="controls-repeat"
disabled={ ! canCreateBlocks }
onClick={ onConvertToReusable }
>
{ __( 'Add to Reusable Blocks' ) }
Expand All @@ -54,6 +56,7 @@ export default compose( [
canInsertBlockType,
__experimentalGetReusableBlock: getReusableBlock,
} = select( 'core/editor' );
const { canUser } = select( 'core' );

const blocks = getBlocksByClientId( clientIds );

Expand All @@ -80,6 +83,7 @@ export default compose( [
! isReusableBlock( blocks[ 0 ] ) ||
! getReusableBlock( blocks[ 0 ].attributes.ref )
),
canCreateBlocks: canUser( 'create', 'blocks' ),
};
} ),
withDispatch( ( dispatch, { clientIds, onToggle = noop } ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import { __ } from '@wordpress/i18n';
import { isReusableBlock } from '@wordpress/blocks';
import { withSelect, withDispatch } from '@wordpress/data';

export function ReusableBlockDeleteButton( { reusableBlock, onDelete } ) {
if ( ! reusableBlock ) {
export function ReusableBlockDeleteButton( { id, isDisabled, onDelete } ) {
if ( ! id ) {
return null;
}

return (
<MenuItem
className="editor-block-settings-menu__control"
icon="no"
disabled={ reusableBlock.isTemporary }
onClick={ () => onDelete( reusableBlock.id ) }
disabled={ isDisabled }
onClick={ () => onDelete( id ) }
>
{ __( 'Remove from Reusable Blocks' ) }
</MenuItem>
Expand All @@ -35,9 +35,16 @@ export default compose( [
getBlock,
__experimentalGetReusableBlock: getReusableBlock,
} = select( 'core/editor' );
const { canUser } = select( 'core' );

const block = getBlock( clientId );

const id = isReusableBlock( block ) ? block.attributes.ref : null;
return {
reusableBlock: block && isReusableBlock( block ) ? getReusableBlock( block.attributes.ref ) : null,
id,
isDisabled: !! id && (
getReusableBlock( id ).isTemporary || ! canUser( 'delete', 'blocks', id )
),
};
} ),
withDispatch( ( dispatch, { onToggle = noop } ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
exports[`ReusableBlockDeleteButton matches the snapshot 1`] = `
<WithInstanceId(MenuItem)
className="editor-block-settings-menu__control"
disabled={false}
icon="no"
onClick={[Function]}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ describe( 'ReusableBlockConvertButton', () => {
<ReusableBlockConvertButton
isVisible
isStaticBlock
canCreateBlocks
onConvertToReusable={ onConvert }
/>
);
expect( wrapper.props.children[ 1 ] ).toBeFalsy();
const button = wrapper.props.children[ 0 ];
expect( button.props.children ).toBe( 'Add to Reusable Blocks' );
expect( button.props.disabled ).toBe( false );
button.props.onClick();
expect( onConvert ).toHaveBeenCalled();
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ describe( 'ReusableBlockDeleteButton', () => {
const wrapper = getShallowRenderOutput(
<ReusableBlockDeleteButton
role="menuitem"
reusableBlock={ { id: 123 } }
id={ 123 }
isDisabled={ false }
onDelete={ noop }
/>
);
Expand All @@ -32,7 +33,8 @@ describe( 'ReusableBlockDeleteButton', () => {
const onDelete = jest.fn();
const wrapper = getShallowRenderOutput(
<ReusableBlockDeleteButton
reusableBlock={ { id: 123 } }
id={ 123 }
isDisabled={ false }
onDelete={ onDelete }
/>
);
Expand Down

0 comments on commit a1856af

Please sign in to comment.