diff --git a/package-lock.json b/package-lock.json index 0318a97778e11..44d5e1a7210ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52817,6 +52817,7 @@ "@wordpress/sync": "file:../sync", "@wordpress/undo-manager": "file:../undo-manager", "@wordpress/url": "file:../url", + "@wordpress/warning": "file:../warning", "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", @@ -67661,6 +67662,7 @@ "@wordpress/sync": "file:../sync", "@wordpress/undo-manager": "file:../undo-manager", "@wordpress/url": "file:../url", + "@wordpress/warning": "file:../warning", "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", diff --git a/packages/block-library/src/navigation-link/link-ui.js b/packages/block-library/src/navigation-link/link-ui.js index 6619c46253546..deed35145d6de 100644 --- a/packages/block-library/src/navigation-link/link-ui.js +++ b/packages/block-library/src/navigation-link/link-ui.js @@ -147,15 +147,18 @@ function LinkUIBlockInserter( { clientId, onBack, onSelectBlock } ) { } function UnforwardedLinkUI( props, ref ) { + const { label, url, opensInNewTab, type, kind } = props.link; + const postType = type || 'page'; + const [ addingBlock, setAddingBlock ] = useState( false ); const [ focusAddBlockButton, setFocusAddBlockButton ] = useState( false ); const { saveEntityRecord } = useDispatch( coreStore ); - const pagesPermissions = useResourcePermissions( 'pages' ); - const postsPermissions = useResourcePermissions( 'posts' ); + const permissions = useResourcePermissions( { + kind: 'postType', + name: postType, + } ); async function handleCreate( pageTitle ) { - const postType = props.link.type || 'page'; - const page = await saveEntityRecord( 'postType', postType, { title: pageTitle, status: 'draft', @@ -180,15 +183,6 @@ function UnforwardedLinkUI( props, ref ) { }; } - const { label, url, opensInNewTab, type, kind } = props.link; - - let userCanCreate = false; - if ( ! type || type === 'page' ) { - userCanCreate = pagesPermissions.canCreate; - } else if ( type === 'post' ) { - userCanCreate = postsPermissions.canCreate; - } - // Memoize link value to avoid overriding the LinkControl's internal state. // This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/50976#issuecomment-1568226407. const link = useMemo( @@ -241,7 +235,7 @@ function UnforwardedLinkUI( props, ref ) { hasRichPreviews value={ link } showInitialSuggestions - withCreateSuggestion={ userCanCreate } + withCreateSuggestion={ permissions.canCreate } createSuggestion={ handleCreate } createSuggestionButtonText={ ( searchTerm ) => { let format; diff --git a/packages/block-library/src/navigation/test/use-navigation-menu.js b/packages/block-library/src/navigation/test/use-navigation-menu.js index eb7e90aff22d1..7eaf648fab936 100644 --- a/packages/block-library/src/navigation/test/use-navigation-menu.js +++ b/packages/block-library/src/navigation/test/use-navigation-menu.js @@ -9,6 +9,12 @@ import { store as coreStore } from '@wordpress/core-data'; */ import useNavigationMenu from '../use-navigation-menu'; +const BASE_ENTITY = { + kind: 'postType', + name: 'wp_navigation', + id: undefined, +}; + function createRegistryWithStores() { // Create a registry and register used stores. const registry = createRegistry(); @@ -63,37 +69,70 @@ function resolveRecords( registry, menus ) { function resolveReadPermission( registry, allowed ) { const dispatch = registry.dispatch( coreStore ); - dispatch.receiveUserPermission( 'create/navigation', allowed ); - dispatch.startResolution( 'canUser', [ 'read', 'navigation' ] ); - dispatch.finishResolution( 'canUser', [ 'read', 'navigation' ] ); + dispatch.receiveUserPermission( 'read/postType/wp_navigation', allowed ); + dispatch.startResolution( 'canUser', [ 'read', BASE_ENTITY ] ); + dispatch.finishResolution( 'canUser', [ 'read', BASE_ENTITY ] ); } function resolveReadRecordPermission( registry, ref, allowed ) { const dispatch = registry.dispatch( coreStore ); - dispatch.receiveUserPermission( 'create/navigation', allowed ); - dispatch.startResolution( 'canUser', [ 'read', 'navigation', ref ] ); - dispatch.finishResolution( 'canUser', [ 'read', 'navigation', ref ] ); + dispatch.receiveUserPermission( + `read/postType/wp_navigation/${ ref }`, + allowed + ); + dispatch.startResolution( 'canUser', [ + 'read', + { ...BASE_ENTITY, id: ref }, + ] ); + dispatch.finishResolution( 'canUser', [ + 'read', + { ...BASE_ENTITY, id: ref }, + ] ); } function resolveCreatePermission( registry, allowed ) { const dispatch = registry.dispatch( coreStore ); - dispatch.receiveUserPermission( 'create/navigation', allowed ); - dispatch.startResolution( 'canUser', [ 'create', 'navigation' ] ); - dispatch.finishResolution( 'canUser', [ 'create', 'navigation' ] ); + dispatch.receiveUserPermission( 'create/postType/wp_navigation', allowed ); + dispatch.startResolution( 'canUser', [ + 'create', + { kind: 'postType', name: 'wp_navigation' }, + ] ); + dispatch.finishResolution( 'canUser', [ + 'create', + { kind: 'postType', name: 'wp_navigation' }, + ] ); } function resolveUpdatePermission( registry, ref, allowed ) { const dispatch = registry.dispatch( coreStore ); - dispatch.receiveUserPermission( `update/navigation/${ ref }`, allowed ); - dispatch.startResolution( 'canUser', [ 'update', 'navigation', ref ] ); - dispatch.finishResolution( 'canUser', [ 'update', 'navigation', ref ] ); + dispatch.receiveUserPermission( + `update/postType/wp_navigation/${ ref }`, + allowed + ); + dispatch.startResolution( 'canUser', [ + 'update', + { ...BASE_ENTITY, id: ref }, + ] ); + dispatch.finishResolution( 'canUser', [ + 'update', + { ...BASE_ENTITY, id: ref }, + ] ); } function resolveDeletePermission( registry, ref, allowed ) { const dispatch = registry.dispatch( coreStore ); - dispatch.receiveUserPermission( `delete/navigation/${ ref }`, allowed ); - dispatch.startResolution( 'canUser', [ 'delete', 'navigation', ref ] ); - dispatch.finishResolution( 'canUser', [ 'delete', 'navigation', ref ] ); + dispatch.receiveUserPermission( + `delete/postType/wp_navigation/${ ref }`, + allowed + ); + dispatch.startResolution( 'canUser', [ + 'delete', + { ...BASE_ENTITY, id: ref }, + ] ); + dispatch.finishResolution( 'canUser', [ + 'delete', + { ...BASE_ENTITY, id: ref }, + ] ); } describe( 'useNavigationMenus', () => { diff --git a/packages/block-library/src/navigation/use-navigation-menu.js b/packages/block-library/src/navigation/use-navigation-menu.js index 3834661ed6101..5fd942c485bf8 100644 --- a/packages/block-library/src/navigation/use-navigation-menu.js +++ b/packages/block-library/src/navigation/use-navigation-menu.js @@ -14,7 +14,11 @@ import { useSelect } from '@wordpress/data'; import { PRELOADED_NAVIGATION_MENUS_QUERY } from './constants'; export default function useNavigationMenu( ref ) { - const permissions = useResourcePermissions( 'navigation', ref ); + const permissions = useResourcePermissions( { + kind: 'postType', + name: 'wp_navigation', + id: ref, + } ); const { navigationMenu, diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 2f2094bed7f4d..de28e4b86be4b 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -1173,7 +1173,10 @@ _Usage_ import { useResourcePermissions } from '@wordpress/core-data'; function PagesList() { - const { canCreate, isResolving } = useResourcePermissions( 'pages' ); + const { canCreate, isResolving } = useResourcePermissions( { + kind: 'postType', + name: 'page', + } ); if ( isResolving ) { return 'Loading ...'; @@ -1196,7 +1199,11 @@ import { useResourcePermissions } from '@wordpress/core-data'; function Page( { pageId } ) { const { canCreate, canUpdate, canDelete, isResolving } = - useResourcePermissions( 'pages', pageId ); + useResourcePermissions( { + kind: 'postType', + name: 'page', + id: pageId, + } ); if ( isResolving ) { return 'Loading ...'; @@ -1222,8 +1229,8 @@ the store state using `canUser()`, or resolved if missing. _Parameters_ -- _resource_ `string`: The resource in question, e.g. media. -- _id_ `IdType`: ID of a specific resource entry, if needed, e.g. 10. +- _resource_ `string | EntityResource`: Entity resource to check. Accepts entity object `{ kind: 'root', name: 'media', id: 1 }` or REST base as a string - `media`. +- _id_ `IdType`: Optional ID of the resource to check, e.g. 10. Note: This argument is discouraged when using an entity object as a resource to check permissions and will be ignored. _Returns_ diff --git a/packages/core-data/package.json b/packages/core-data/package.json index d94eb37528725..bed2ca97791bc 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -47,6 +47,7 @@ "@wordpress/sync": "file:../sync", "@wordpress/undo-manager": "file:../undo-manager", "@wordpress/url": "file:../url", + "@wordpress/warning": "file:../warning", "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", "fast-deep-equal": "^3.1.3", diff --git a/packages/core-data/src/hooks/test/use-resource-permissions.js b/packages/core-data/src/hooks/test/use-resource-permissions.js index bb6c3a85c191f..b1c43b7947874 100644 --- a/packages/core-data/src/hooks/test/use-resource-permissions.js +++ b/packages/core-data/src/hooks/test/use-resource-permissions.js @@ -93,4 +93,97 @@ describe( 'useResourcePermissions', () => { } ) ); } ); + + it( 'retrieves the relevant permissions for a id-less entity', async () => { + let data; + const TestComponent = () => { + data = useResourcePermissions( { + kind: 'root', + name: 'media', + } ); + return
; + }; + render( + + + + ); + expect( data ).toEqual( { + status: 'IDLE', + isResolving: false, + hasResolved: false, + canCreate: false, + canRead: false, + } ); + + await waitFor( () => + expect( data ).toEqual( { + status: 'SUCCESS', + isResolving: false, + hasResolved: true, + canCreate: true, + canRead: false, + } ) + ); + } ); + + it( 'retrieves the relevant permissions for an entity', async () => { + let data; + const TestComponent = () => { + data = useResourcePermissions( { + kind: 'root', + name: 'media', + id: 1, + } ); + return
; + }; + render( + + + + ); + expect( data ).toEqual( { + status: 'IDLE', + isResolving: false, + hasResolved: false, + canCreate: false, + canRead: false, + canUpdate: false, + canDelete: false, + } ); + + await waitFor( () => + expect( data ).toEqual( { + status: 'SUCCESS', + isResolving: false, + hasResolved: true, + canCreate: true, + canRead: false, + canUpdate: false, + canDelete: false, + } ) + ); + } ); + + it( 'should warn when called with incorrect arguments signature', () => { + const TestComponent = () => { + useResourcePermissions( + { + kind: 'root', + name: 'media', + }, + 1 + ); + return null; + }; + render( + + + + ); + + expect( console ).toHaveWarnedWith( + `When 'resource' is an entity object, passing 'id' as a separate argument isn't supported.` + ); + } ); } ); diff --git a/packages/core-data/src/hooks/use-resource-permissions.ts b/packages/core-data/src/hooks/use-resource-permissions.ts index 7da7189e2e506..38dfc1ed77a3e 100644 --- a/packages/core-data/src/hooks/use-resource-permissions.ts +++ b/packages/core-data/src/hooks/use-resource-permissions.ts @@ -2,6 +2,7 @@ * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; +import warning from '@wordpress/warning'; /** * Internal dependencies @@ -41,20 +42,34 @@ type ResourcePermissionsResolution< IdType > = [ ( IdType extends void ? SpecificResourcePermissionsResolution : {} ), ]; +type EntityResource = { kind: string; name: string; id?: string | number }; + +function useResourcePermissions< IdType = void >( + resource: string, + id?: IdType +): ResourcePermissionsResolution< IdType >; + +function useResourcePermissions< IdType = void >( + resource: EntityResource, + id?: never +): ResourcePermissionsResolution< IdType >; + /** * Resolves resource permissions. * * @since 6.1.0 Introduced in WordPress core. * - * @param resource The resource in question, e.g. media. - * @param id ID of a specific resource entry, if needed, e.g. 10. + * @param resource Entity resource to check. Accepts entity object `{ kind: 'root', name: 'media', id: 1 }` + * or REST base as a string - `media`. + * @param id Optional ID of the resource to check, e.g. 10. Note: This argument is discouraged + * when using an entity object as a resource to check permissions and will be ignored. * * @example * ```js * import { useResourcePermissions } from '@wordpress/core-data'; * * function PagesList() { - * const { canCreate, isResolving } = useResourcePermissions( 'pages' ); + * const { canCreate, isResolving } = useResourcePermissions( { kind: 'postType', name: 'page' } ); * * if ( isResolving ) { * return 'Loading ...'; @@ -82,7 +97,7 @@ type ResourcePermissionsResolution< IdType > = [ * canUpdate, * canDelete, * isResolving - * } = useResourcePermissions( 'pages', pageId ); + * } = useResourcePermissions( { kind: 'postType', name: 'page', id: pageId } ); * * if ( isResolving ) { * return 'Loading ...'; @@ -109,15 +124,35 @@ type ResourcePermissionsResolution< IdType > = [ * @return Entity records data. * @template IdType */ -export default function useResourcePermissions< IdType = void >( - resource: string, +function useResourcePermissions< IdType = void >( + resource: string | EntityResource, id?: IdType ): ResourcePermissionsResolution< IdType > { + // Serialize `resource` to a string that can be safely used as a React dep. + // We can't just pass `resource` as one of the deps, because if it is passed + // as an object literal, then it will be a different object on each call even + // if the values remain the same. + const isEntity = typeof resource === 'object'; + const resourceAsString = isEntity ? JSON.stringify( resource ) : resource; + + if ( isEntity && typeof id !== 'undefined' ) { + warning( + `When 'resource' is an entity object, passing 'id' as a separate argument isn't supported.` + ); + } + return useQuerySelect( ( resolve ) => { + const hasId = isEntity ? !! resource.id : !! id; const { canUser } = resolve( coreStore ); - const create = canUser( 'create', resource ); - if ( ! id ) { + const create = canUser( + 'create', + isEntity + ? { kind: resource.kind, name: resource.name } + : resource + ); + + if ( ! hasId ) { const read = canUser( 'read', resource ); const isResolving = create.isResolving || read.isResolving; @@ -168,10 +203,12 @@ export default function useResourcePermissions< IdType = void >( canDelete: hasResolved && _delete.data, }; }, - [ resource, id ] + [ resourceAsString, id ] ); } +export default useResourcePermissions; + export function __experimentalUseResourcePermissions( resource: string, id?: unknown diff --git a/packages/core-data/tsconfig.json b/packages/core-data/tsconfig.json index 5db0de19f111e..26602d82ab0c0 100644 --- a/packages/core-data/tsconfig.json +++ b/packages/core-data/tsconfig.json @@ -21,7 +21,8 @@ { "path": "../rich-text" }, { "path": "../sync" }, { "path": "../undo-manager" }, - { "path": "../url" } + { "path": "../url" }, + { "path": "../warning" } ], "include": [ "src/**/*" ] } diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index 6d014a589252d..af844fe9da4fe 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -5,11 +5,7 @@ import { SlotFillProvider } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { uploadMedia } from '@wordpress/media-utils'; import { useDispatch, useSelect } from '@wordpress/data'; -import { - useEntityBlockEditor, - store as coreStore, - useResourcePermissions, -} from '@wordpress/core-data'; +import { useEntityBlockEditor, store as coreStore } from '@wordpress/core-data'; import { useMemo } from '@wordpress/element'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; @@ -37,9 +33,9 @@ export default function WidgetAreasBlockEditorProvider( { children, ...props } ) { - const mediaPermissions = useResourcePermissions( 'media' ); const isLargeViewport = useViewportMatch( 'medium' ); const { + hasUploadPermissions, reusableBlocks, isFixedToolbarActive, keepCaretInsideBlock, @@ -55,6 +51,11 @@ export default function WidgetAreasBlockEditorProvider( { ? getEntityRecord( 'root', 'site' ) : undefined; return { + hasUploadPermissions: + canUser( 'create', { + kind: 'root', + name: 'media', + } ) ?? true, reusableBlocks: ALLOW_REUSABLE_BLOCKS ? getEntityRecords( 'postType', 'wp_block' ) : EMPTY_ARRAY, @@ -74,7 +75,7 @@ export default function WidgetAreasBlockEditorProvider( { const settings = useMemo( () => { let mediaUploadBlockEditor; - if ( mediaPermissions.canCreate ) { + if ( hasUploadPermissions ) { mediaUploadBlockEditor = ( { onError, ...argumentsObject } ) => { uploadMedia( { wpAllowedMimeTypes: blockEditorSettings.allowedMimeTypes, @@ -95,11 +96,11 @@ export default function WidgetAreasBlockEditorProvider( { pageForPosts, }; }, [ + hasUploadPermissions, blockEditorSettings, isFixedToolbarActive, isLargeViewport, keepCaretInsideBlock, - mediaPermissions.canCreate, reusableBlocks, setIsInserterOpened, pageOnFront,