Skip to content

Add core/term-create, core/term-update, and core/term-delete abilities - #1103

Draft
jorgefilipecosta wants to merge 33 commits into
add/core-terms-queryfrom
add/core-term-write-abilities
Draft

jorgefilipecosta wants to merge 33 commits into
add/core-terms-queryfrom
add/core-term-write-abilities

Conversation

@jorgefilipecosta

@jorgefilipecosta jorgefilipecosta commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

What?

Adds core/term-create, core/term-update, and core/term-delete, abilities that create, update, and delete categories, tags, and other taxonomy terms. Based on #1099.

How?

  • The abilities live next to core/terms-query in includes/Abilities/Terms/Terms.php, behind the Custom Abilities experiment, and write only to the taxonomies exposed to core/terms-query.
  • core/term-create takes a taxonomy and a name, plus an optional slug, description, and parent (hierarchical taxonomies only). core/term-update takes the term's id and the values to change. core/term-delete takes the term's id and force, which must be true because terms can't be trashed. On update and delete, taxonomy is an optional guard.
  • Creating a term needs edit_terms in a hierarchical taxonomy or assign_terms in a flat one. Updating needs edit_term, and deleting needs delete_term. A missing term, or one outside the exposed taxonomies or the guard, is denied like a missing capability. The default category can't be deleted.
  • The term comes back in the shape core/terms-query uses, limited by fields. A delete returns { deleted, previous }.

Testing Instructions

  1. Enable the Custom Abilities experiment.
  2. Run core/term-create with { "taxonomy": "post_tag", "name": "Example" }, then core/term-update with { "id": <its ID>, "name": "Renamed" }, then core/term-delete with { "id": <its ID>, "force": true }.
  3. npm run test:php -- --filter 'TermCreateTest|TermUpdateTest|TermDeleteTest'
  4. npm run test:e2e -- tests/e2e/specs/abilities/core-term-write.spec.js

Changelog Entry

Added - New core/term-create, core/term-update, and core/term-delete abilities.

Open WordPress Playground Preview

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.29577% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.47%. Comparing base (6fa0579) to head (6f26583).

Files with missing lines Patch % Lines
includes/Abilities/Terms/Terms.php 99.29% 2 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                     @@
##             add/core-terms-query    #1103      +/-   ##
==========================================================
+ Coverage                   82.09%   82.47%   +0.37%     
- Complexity                   3166     3230      +64     
==========================================================
  Files                         131      131              
  Lines                       12673    12957     +284     
==========================================================
+ Hits                        10404    10686     +282     
- Misses                       2269     2271       +2     
Flag Coverage Δ
unit 82.47% <99.29%> (+0.37%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

@jorgefilipecosta

Copy link
Copy Markdown
Member Author

Tested in the browser console on WordPress 7.1.2, as an administrator, from a post's editor with the Excerpt Generation and Custom Abilities experiments enabled (Excerpt Generation puts @wordpress/abilities and @wordpress/core-abilities in the import map).

Test data: the category Fruit (28554).

await ( await import( '@wordpress/core-abilities' ) ).ready;
const { executeAbility } = await import( '@wordpress/abilities' );

Create a term

const tag = await executeAbility( 'core/term-create', { taxonomy: 'post_tag', name: 'Green', description: 'Like <em>grass</em>.' } );
// { id: 28556, count: 0, name: 'Green', slug: 'green', taxonomy: 'post_tag' }

// The ID is always included.
const citrus = await executeAbility( 'core/term-create', { taxonomy: 'category', name: 'Citrus', parent: 28554, fields: [ 'name', 'slug', 'parent', 'link' ] } );
// { id: 28557, link: 'http://localhost:8896/?cat=28557', name: 'Citrus', slug: 'citrus', parent: 28554 }

// The name is taken under that parent.
await executeAbility( 'core/term-create', { taxonomy: 'category', name: 'Citrus', parent: 28554 } );
// Rejects with { code: 'term_exists', message: 'A term with the name provided already exists with this parent.', data: { status: 400, term_id: 28557 } }

Update a term

// Renaming keeps the slug.
await executeAbility( 'core/term-update', { id: tag.id, name: 'Lime green', fields: [ 'name', 'slug', 'description' ] } );
// { id: 28556, description: 'Like <em>grass</em>.', name: 'Lime green', slug: 'green' }

await executeAbility( 'core/term-update', { id: citrus.id, parent: 0, fields: [ 'name', 'parent' ] } );
// { id: 28557, name: 'Citrus', parent: 0 }

// `taxonomy` is an optional guard: a term outside it is denied like a missing one.
await executeAbility( 'core/term-update', { id: citrus.id, taxonomy: 'post_tag', name: 'Lemon' } );
// Rejects with { code: 'rest_ability_cannot_execute', message: 'Sorry, you are not allowed to execute this ability.', data: { status: 403 } }

// Tags aren't hierarchical.
await executeAbility( 'core/term-update', { id: tag.id, parent: 28554 } );
// Rejects with { code: 'terms_taxonomy_not_hierarchical', message: 'Cannot set parent term, taxonomy is not hierarchical.', data: { status: 400 } }

Delete a term

// Terms can't be trashed, so `force` must be true.
await executeAbility( 'core/term-delete', { id: tag.id } );
// Rejects with { code: 'terms_trash_not_supported', message: 'Terms do not support trashing. Set `force` to true to delete.', data: { status: 501 } }

await executeAbility( 'core/term-delete', { id: tag.id, force: true, fields: [ 'name', 'slug' ] } );
// { deleted: true, previous: { id: 28556, name: 'Lime green', slug: 'green' } }

await executeAbility( 'core/term-delete', { id: citrus.id, taxonomy: 'category', force: true } );
// { deleted: true, previous: { id: 28557, count: 0, name: 'Citrus', slug: 'citrus', taxonomy: 'category', parent: 0 } }

// A deleted term can't be read, and the default category can't be deleted.
await executeAbility( 'core/terms-query', { id: tag.id } );
await executeAbility( 'core/term-delete', { id: 1, force: true } );
// Both reject with { code: 'rest_ability_cannot_execute', message: 'Sorry, you are not allowed to execute this ability.', data: { status: 403 } }

Rejected by the client

// The client rejects these before sending a request.
await executeAbility( 'core/term-create', { taxonomy: 'nav_menu', name: 'Menu' } );
// Rejects with { code: 'ability_invalid_input', message: 'Ability "core/term-create" has invalid input. Reason: input[taxonomy] is not one of category, post_tag.' }

await executeAbility( 'core/term-create', { taxonomy: 'post_tag', name: 'Blue', meta: {} } );
// Rejects with { code: 'ability_invalid_input', message: 'Ability "core/term-create" has invalid input. Reason: meta is not a valid property of Object.' }

@jeffpaul

jeffpaul commented Oct 4, 2026

Copy link
Copy Markdown
Member

@jorgefilipecosta note that I'm holding off on requesting reviews until this comes out of draft status

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

2 participants