Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show a UI warning when user does not have permission to update/edit an existing Navigation block #37286

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 103 additions & 16 deletions packages/block-library/src/navigation/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
import noop from 'lodash';
import { noop, uniqueId } from 'lodash';

/**
* WordPress dependencies
Expand All @@ -26,7 +26,12 @@ import {
getColorClassName,
Warning,
} from '@wordpress/block-editor';
import { EntityProvider, useEntityProp } from '@wordpress/core-data';
import {
EntityProvider,
useEntityProp,
store as coreStore,
} from '@wordpress/core-data';

import { useDispatch, useSelect } from '@wordpress/data';
import {
PanelBody,
Expand All @@ -38,6 +43,7 @@ import {
Button,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { store as noticeStore } from '@wordpress/notices';

/**
* Internal dependencies
Expand Down Expand Up @@ -103,6 +109,8 @@ function Navigation( {
customPlaceholder: CustomPlaceholder = null,
customAppender: CustomAppender = null,
} ) {
const noticeRef = useRef();

const {
openSubmenusOnClick,
overlayMenu,
Expand Down Expand Up @@ -142,7 +150,15 @@ function Navigation( {
`navigationMenu/${ ref }`
);

const { innerBlocks, isInnerBlockSelected, hasSubmenus } = useSelect(
const {
canUserUpdateNavigationEntity,
hasResolvedCanUserUpdateNavigationEntity,
canUserDeleteNavigationEntity,
hasResolvedCanUserDeleteNavigationEntity,
innerBlocks,
isInnerBlockSelected,
hasSubmenus,
} = useSelect(
( select ) => {
const { getBlocks, hasSelectedInnerBlock } = select(
blockEditorStore
Expand All @@ -152,13 +168,29 @@ function Navigation( {
( block ) => block.name === 'core/navigation-submenu'
);

const { canUser, hasFinishedResolution } = select( coreStore );

return {
hasSubmenus: firstSubmenu,
innerBlocks: blocks,
isInnerBlockSelected: hasSelectedInnerBlock( clientId, true ),
canUserUpdateNavigationEntity: ref
? canUser( 'update', 'navigation', ref )
getdave marked this conversation as resolved.
Show resolved Hide resolved
: undefined,
hasResolvedCanUserUpdateNavigationEntity: hasFinishedResolution(
'canUser',
[ 'update', 'navigation', ref ]
),
canUserDeleteNavigationEntity: ref
? canUser( 'delete', 'navigation', ref )
: undefined,
hasResolvedCanUserDeleteNavigationEntity: hasFinishedResolution(
'canUser',
[ 'delete', 'navigation', ref ]
),
getdave marked this conversation as resolved.
Show resolved Hide resolved
};
},
[ clientId ]
[ clientId, ref ]
talldan marked this conversation as resolved.
Show resolved Hide resolved
);
const hasExistingNavItems = !! innerBlocks.length;
const {
Expand All @@ -167,6 +199,8 @@ function Navigation( {
__unstableMarkNextChangeAsNotPersistent,
} = useDispatch( blockEditorStore );

const { createWarningNotice, removeNotice } = useDispatch( noticeStore );

const [
hasSavedUnsavedInnerBlocks,
setHasSavedUnsavedInnerBlocks,
Expand Down Expand Up @@ -303,6 +337,53 @@ function Navigation( {
// with the snapshot from the time when ref became undefined.
}, [ clientId, ref, innerBlocks ] );

useEffect( () => {
const setPermissionsNotice = () => {
if ( noticeRef.current ) {
return;
}

noticeRef.current = uniqueId( 'navBlockNoEditPermissions' );

createWarningNotice(
__(
'You do not have permission to edit this Navigation. Any edits made will not be saved.'
getdave marked this conversation as resolved.
Show resolved Hide resolved
),
{
id: noticeRef.current,
getdave marked this conversation as resolved.
Show resolved Hide resolved
type: 'snackbar',
}
);
};

const removePermissionsNotice = () => {
if ( ! noticeRef.current ) {
return;
}
removeNotice( noticeRef.current );
noticeRef.current = null;
};

if ( ! isSelected && ! isInnerBlockSelected ) {
removePermissionsNotice();
}

if (
( isSelected || isInnerBlockSelected ) &&
hasResolvedCanUserUpdateNavigationEntity &&
! canUserUpdateNavigationEntity
) {
setPermissionsNotice();
}
}, [
ref,
isEntityAvailable,
hasResolvedCanUserUpdateNavigationEntity,
canUserUpdateNavigationEntity,
isSelected,
isInnerBlockSelected,
] );

const startWithEmptyMenu = useCallback( () => {
if ( navigationArea ) {
setAreaMenu( 0 );
Expand Down Expand Up @@ -507,18 +588,24 @@ function Navigation( {
</InspectorControls>
{ isEntityAvailable && (
<InspectorControls __experimentalGroup="advanced">
<NavigationMenuNameControl />
<NavigationMenuDeleteControl
onDelete={ () => {
if ( navigationArea ) {
setAreaMenu( 0 );
}
setAttributes( {
ref: undefined,
} );
setIsPlaceholderShown( true );
} }
/>
{ hasResolvedCanUserUpdateNavigationEntity &&
canUserUpdateNavigationEntity && (
<NavigationMenuNameControl />
) }
{ hasResolvedCanUserDeleteNavigationEntity &&
canUserDeleteNavigationEntity && (
<NavigationMenuDeleteControl
onDelete={ () => {
if ( navigationArea ) {
setAreaMenu( 0 );
}
setAttributes( {
ref: undefined,
} );
setIsPlaceholderShown( true );
} }
/>
) }
</InspectorControls>
) }
<nav { ...blockProps }>
Expand Down
58 changes: 58 additions & 0 deletions packages/e2e-tests/specs/editor/blocks/navigation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import {
ensureSidebarOpened,
__experimentalRest as rest,
publishPost,
createUser,
loginUser,
deleteUser,
} from '@wordpress/e2e-test-utils';

/**
Expand Down Expand Up @@ -111,6 +114,7 @@ const PLACEHOLDER_ACTIONS_CLASS = 'wp-block-navigation-placeholder__actions';
const PLACEHOLDER_ACTIONS_XPATH = `//*[contains(@class, '${ PLACEHOLDER_ACTIONS_CLASS }')]`;
const START_EMPTY_XPATH = `${ PLACEHOLDER_ACTIONS_XPATH }//button[text()='Start empty']`;
const ADD_ALL_PAGES_XPATH = `${ PLACEHOLDER_ACTIONS_XPATH }//button[text()='Add all pages']`;
const SELECT_MENU_XPATH = `${ PLACEHOLDER_ACTIONS_XPATH }//button[text()='Select menu']`;

async function turnResponsivenessOn() {
const blocks = await getAllBlocks();
Expand Down Expand Up @@ -766,4 +770,58 @@ describe( 'Navigation', () => {
expect( await page.$x( NAV_ENTITY_SELECTOR ) ).toHaveLength( 1 );
} );
} );

describe( 'Permission based restrictions', () => {
it( 'shows a warning if user does not have permission to edit / update Navigations', async () => {
await createNewPost();
await insertBlock( 'Navigation' );

const startEmptyButton = await page.waitForXPath(
START_EMPTY_XPATH
);

// This creates an empty Navigation post type entity.
await startEmptyButton.click();

// Publishing the Post ensures the Navigation entity is saved.
// The Post itself is irrelevant.
await publishPost();

const username = 'contributoruser';

const contribUserPassword = await createUser( username, {
role: 'contributor',
} );

// Switch to a Contributor role user - they should not have
// permission to update Navigations.
await loginUser( username, contribUserPassword );

await createNewPost();

await insertBlock( 'Navigation' );

// Select the Navigation post created by the Admin early
// in the test.
const navigationPostCreatedByAdminName = 'Navigation';
const dropdown = await page.waitForXPath( SELECT_MENU_XPATH );
await dropdown.click();
const theOption = await page.waitForXPath(
`//*[contains(@class, 'components-menu-item__item')][ text()="${ navigationPostCreatedByAdminName }" ]`
);
await theOption.click();

// Make sure the snackbar error shows up
await page.waitForXPath(
`//*[contains(@class, 'components-snackbar__content')][ text()="You do not have permission to edit this Navigation. Any edits made will not be saved." ]`
);

// Tidy up after ourselves.
await deleteUser( username );

// Expect a console 403 for request to Navigation Areas.
// Todo: removed once Nav Areas are removed from the Gutenberg Plugin.
expect( console ).toHaveErrored();
getdave marked this conversation as resolved.
Show resolved Hide resolved
} );
} );
} );