Skip to content

Conversation

@emdashcodes
Copy link
Contributor

@emdashcodes emdashcodes commented Oct 15, 2025

This PR adds improved category support to the client package. It adds methods for listing categories (getAbilityCategories, getAbilityCategory), methods for registering and unregistering categories (registerAbilityCategory, unregisterAbilityCategory), and it validates ability categories during ability registration.

Follows up on #102 (categories system), #114 (basic category support), and #120 (REST endpoints).

Why

In the previous client PR we only addressed filtering abilities by category slug but had no way to discover available categories or validate that a category exists before registering an ability. There was also no way to register a category only on the client side.

How

  • Added getAbilityCategories() function to fetch all categories
  • Added getAbilityCategory(slug) function to fetch a single category
  • Added registerAbilityCategory to register a category on the client side
  • Added unregisterAbilityCategory to remove a category on the client side
  • Added resolvers that automatically fetch categories from the REST API
  • Enhanced validation to check category existence, not just format
  • Added unit tests for all new selectors, resolvers, and actions
  • Updated README with usage examples for all new functions

Breaking Change

  • Breaking Change - registerAbility() now returns Promise<void> instead of void. This is. to. ensure that categories are loaded on first registerAbility() call if store is empty. Since categories are already a breaking change, I feel like this is OK. It does still work without awaiting, but it's possible the categories are not loaded before the register / validation check.

Testing Steps

  1. Run npm run test:client

  2. Register a few test categories and abilities

  3. Open WordPress admin in browser with dev tools console

  4. Test getAbilityCategories():

    const categories = await wp.abilities.getAbilityCategories();
    console.log(categories);
  5. Test getAbilityCategory():

    const nav = await wp.abilities.getAbilityCategory('navigation');
    console.log(nav);
    
    const missing = await wp.abilities.getAbilityCategory('does-not-exist');
    console.log(missing);
  6. Test registerAbility() validation:

Test with a valid category:

await wp.abilities.registerAbility({
  name: 'test/my-ability',
  label: 'Test Ability',
  description: 'A test ability',
  category: 'navigation', // Should validate successfully
  callback: () => ({ success: true })
});

Test with an invalid category:

await wp.abilities.registerAbility({
  name: 'test/invalid',
  label: 'Test',
  description: 'Test',
  category: 'does-not-exist', // Should throw validation error
  callback: () => ({})
});
// Expected error: 'Ability "test/invalid" references non-existent category "does-not-exist". Please register the category first.'
  1. Test registering a category and assigning some abilities to it:
  // Register category
  await wp.abilities.registerAbilityCategory('block-editor', {
    label: 'Block Editor',
    description: 'Abilities for interacting with the block editor',
    meta: { icon: 'dashicons-editor-code' }
  });

  // Register multiple abilities
  await wp.abilities.registerAbility({
    name: 'my-plugin/insert-paragraph',
    label: 'Insert Paragraph',
    description: 'Inserts a paragraph block',
    category: 'block-editor',
    callback: async ({ content }) => {
      return { success: true, content };
    }
  });

  await wp.abilities.registerAbility({
    name: 'my-plugin/insert-heading',
    label: 'Insert Heading',
    description: 'Inserts a heading block',
    category: 'block-editor',
    callback: async ({ level, text }) => {
      return { success: true, level, text };
    }
  });

  // Get all abilities in this category
  const allAbilities = await wp.abilities.getAbilities({ category: 'block-editor' });
  console.log('Abilities in block-editor category:', allAbilities);
  1. Test unregistering a category
  wp.abilities.unregisterAbilityCategory('block-editor');
  console.log('Category unregistered!');

@emdashcodes emdashcodes self-assigned this Oct 15, 2025
@codecov
Copy link

codecov bot commented Oct 15, 2025

Codecov Report

❌ Patch coverage is 95.14563% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.17%. Comparing base (118cd0a) to head (91d8bf8).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
packages/client/src/api.ts 66.66% 4 Missing ⚠️
packages/client/src/store/resolvers.ts 94.73% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##              trunk     #121      +/-   ##
============================================
+ Coverage     88.50%   89.17%   +0.66%     
  Complexity      162      162              
============================================
  Files            19       19              
  Lines          1140     1238      +98     
  Branches         89      117      +28     
============================================
+ Hits           1009     1104      +95     
- Misses          131      134       +3     
Flag Coverage Δ
javascript 94.20% <95.14%> (+1.16%) ⬆️
unit 87.36% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@emdashcodes emdashcodes marked this pull request as ready for review October 15, 2025 13:05
@github-actions
Copy link

github-actions bot commented Oct 15, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: emdashcodes <emdashcodes@git.wordpress.org>
Co-authored-by: JasonTheAdams <jason_the_adams@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@emdashcodes emdashcodes added the [Type] Enhancement New feature or request label Oct 15, 2025
@emdashcodes emdashcodes force-pushed the add/update-client-package-categories-list branch from 7185ec4 to 3d92902 Compare October 15, 2025 21:25
Copy link
Member

@JasonTheAdams JasonTheAdams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking pretty good! One question comes to mind: Should it be possible to register categories here the same way abilities can? If a user is registering abilities on the front-end, is it also possible they may be introducing new categories of abilities? I would think so, actually, in such cases as interacting with the block editor.

A category here obviously shouldn't conflict with a category already registered from the server. But I think that's about it.

Base automatically changed from add/categories-rest-endpoints to trunk October 16, 2025 10:53
@gziolo gziolo added this to the pre WP 6.9 milestone Oct 16, 2025
@emdashcodes emdashcodes force-pushed the add/update-client-package-categories-list branch from 3d92902 to 1e273c3 Compare October 16, 2025 12:39
@emdashcodes emdashcodes force-pushed the add/update-client-package-categories-list branch from 1e273c3 to 0366450 Compare October 16, 2025 12:41
@emdashcodes
Copy link
Contributor Author

emdashcodes commented Oct 16, 2025

One question comes to mind: Should it be possible to register categories here the same way abilities can? If a user is registering abilities on the front-end, is it also possible they may be introducing new categories of abilities? I would think so, actually, in such cases as interacting with the block editor.

Yeah, I think that we should allow for this, and that there's going to be a lot of use cases where a set of abilities is just all client-side.

I went ahead and added support for this here: cc55e76. I have also updated the docs and the PR description to include the new changes.

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks close to ready. I did first pass of review of production code and it looks solid 👍

@emdashcodes emdashcodes requested a review from gziolo October 16, 2025 16:56
@emdashcodes
Copy link
Contributor Author

@gziolo Thanks for the review! I've pushed a couple final changes here.

@gziolo
Copy link
Member

gziolo commented Oct 16, 2025

I’ll have another look later. The follow up changes look good after quick check 👍

Comment on lines +186 to +192
await resolveSelect( STORE_NAME ).getAbilityCategories();
const existingCategory = select.getAbilityCategory( slug );
if ( existingCategory ) {
throw new Error(
sprintf( 'Category "%s" is already registered.', slug )
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emdashcodes Just double-checking: It's this await resolveSelect here that ensures the server categories are loaded prior to checking for a collision, right?

Copy link
Contributor Author

@emdashcodes emdashcodes Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! See https://developer.wordpress.org/block-editor/reference-guides/packages/packages-data/#resolveselect. The promise here will resolve once the resolvers fetch from the entity store, which will fetch from the REST API if needed.

}
```

#### `registerAbility( ability: Ability ): Promise<void>`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need an update in another document the example with missing await, too:

https://github.com/WordPress/abilities-api/blob/trunk/docs/7.javascript-client.md#registerability-ability-

A larger question for later is how to avoid redundancy between these two documents. Is this one auto-generated from JSDoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I updated this document with all the new methods and updated the await in the example.

A larger question for later is how to avoid redundancy between these two documents. Is this one auto-generated from JSDoc?

This one is not auto-generated. I'm not sure the best way to keep them in sync to be honest.

I do think it's valuable for the client package README to have a function definition and such. Especially if it gets split off into Gutenberg. But we also don't want to duplicate stuff in multiple places obviously..

Copy link
Member

@gziolo gziolo Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gutenberg has tooling in place that allows auto-generating the list of public methods based on the JSDoc, so that would cover the README file nicely, as these methods are very well documented.

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit complicated to handle entities for abilities, and ability categories, while it's possible to register more items on the client for both registries. We probably need a small tweak as outlined in https://github.com/WordPress/abilities-api/pull/121/files#r2437279266, but it also raises a larger question what would be the ideal design to take advantage of the fact that these entities are already nicely abstracted, and maybe, we could avoid using these resolvers, and instead make selectors more complex with direct calls that resolvers do today. I have no idea if that is feasible, but I am leaving a note to revisit later.

…ng from server if a client category is registered
Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No more feedback from my side. I'm glad to see we reached feature parity with the server implementaiton for categories 🎉

@gziolo gziolo merged commit d9c061f into trunk Oct 17, 2025
22 checks passed
@gziolo gziolo deleted the add/update-client-package-categories-list branch October 17, 2025 06:48
gziolo added a commit that referenced this pull request Oct 17, 2025
* add: category listing endpoints for the REST API

* add: client support for category listing and valdating before registering an ability

* fix: increase test coverage

* add: registerAbilityCategory and unregisterAbilityCategory

* fix: use resolveSelect and  use createSelector for getAbilityCategories

* fix: update docs/ on client, clarify selector call, and fix for loading from server if a client category is registered

Co-authored-by: emdashcodes <emdashcodes@git.wordpress.org>
Co-authored-by: JasonTheAdams <jason_the_adams@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants