diff --git a/packages/abilities/src/store/actions.ts b/packages/abilities/src/store/actions.ts index 59b850b43a973e..9c6ca30880be75 100644 --- a/packages/abilities/src/store/actions.ts +++ b/packages/abilities/src/store/actions.ts @@ -61,7 +61,7 @@ export function registerAbility( ability: Ability ) { // Validate name format matches server implementation if ( ! ABILITY_NAME_PATTERN.test( ability.name ) ) { throw new Error( - 'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.' + 'Ability name must be a string containing a namespace prefix with 2-4 segments, e.g. "my-plugin/my-ability" or "core/posts/find". It can only contain lowercase alphanumeric characters, dashes and the forward slash.' ); } diff --git a/packages/abilities/src/store/constants.ts b/packages/abilities/src/store/constants.ts index 38a42310f3fe12..8bc2a2dc744dd7 100644 --- a/packages/abilities/src/store/constants.ts +++ b/packages/abilities/src/store/constants.ts @@ -4,7 +4,7 @@ export const STORE_NAME = 'core/abilities'; // Validation patterns -export const ABILITY_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/; +export const ABILITY_NAME_PATTERN = /^[a-z0-9-]+(?:\/[a-z0-9-]+){1,3}$/; export const CATEGORY_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; // Action types diff --git a/packages/abilities/src/store/tests/actions.test.ts b/packages/abilities/src/store/tests/actions.test.ts index 8d29a0ca1ff58b..d80585467f20c6 100644 --- a/packages/abilities/src/store/tests/actions.test.ts +++ b/packages/abilities/src/store/tests/actions.test.ts @@ -141,8 +141,8 @@ describe( 'Store Actions', () => { it( 'should validate and reject ability with invalid name format', () => { const testCases = [ - 'invalid', // No namespace - 'my-plugin/feature/action', // Multiple slashes + 'invalid', // No namespace (only 1 segment) + 'my-plugin/a/b/c/d', // Too many slashes (5 segments) 'My-Plugin/feature', // Uppercase letters 'my_plugin/feature', // Underscores not allowed 'my-plugin/feature!', // Special characters not allowed @@ -170,6 +170,39 @@ describe( 'Store Actions', () => { } } ); + it( 'should accept valid nested namespace ability names (2-4 segments)', () => { + const validNames = [ + 'test/ability', // 2 segments + 'core/posts/find', // 3 segments + 'my-plugin/resource/action', // 3 segments + 'my-plugin/resource/sub/action', // 4 segments + ]; + + for ( const validName of validNames ) { + const ability: Ability = { + name: validName, + label: 'Test Ability', + description: 'Test description', + category: 'test-category', + callback: jest.fn(), + }; + + mockSelect.getAbility.mockReturnValue( null ); + mockDispatch.mockClear(); + + const action = registerAbility( ability ); + action( { select: mockSelect, dispatch: mockDispatch } ); + + expect( mockDispatch ).toHaveBeenCalledWith( { + type: REGISTER_ABILITY, + ability: { + ...ability, + meta: { annotations: { clientRegistered: true } }, + }, + } ); + } + } ); + it( 'should validate and reject ability without label', () => { const ability: Ability = { name: 'test/ability', diff --git a/packages/abilities/src/types.ts b/packages/abilities/src/types.ts index 72531f23fc03c3..82b8b597a3a210 100644 --- a/packages/abilities/src/types.ts +++ b/packages/abilities/src/types.ts @@ -25,7 +25,7 @@ export type PermissionCallback = ( export interface Ability { /** * The unique name/identifier of the ability, with its namespace. - * Example: 'my-plugin/my-ability' + * Supports 2-4 segments (e.g. 'my-plugin/my-ability', 'core/posts/find', 'my-plugin/resource/sub/action'). * @see WP_Ability::get_name() */ name: string;