Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7fb1df4
Core Data: Add a private selector for the actions registered on the s…
jorgefilipecosta Oct 14, 2026
c84e822
Editor: Show the actions registered on the server
jorgefilipecosta Oct 14, 2026
1c595ea
Actions API: Preload the actions of the post type the post editor edits
jorgefilipecosta Oct 14, 2026
0aac32f
E2E: Add a test plugin that adds an action to pages
jorgefilipecosta Oct 14, 2026
c21d4ee
Core Actions: Register the core actions on the server
jorgefilipecosta Oct 13, 2026
e6e93e7
Editor: Read the core actions from the server
jorgefilipecosta Oct 14, 2026
da2e08e
Core Actions: Keep only the JavaScript parts in the script modules
jorgefilipecosta Oct 13, 2026
bf374c9
E2E: Replace and remove a core action
jorgefilipecosta Oct 14, 2026
ededfb4
Editor: Show a notice when the actions of an entity cannot be loaded
jorgefilipecosta Oct 14, 2026
cae2e7b
Core Actions: Unhook the core actions WordPress registers
jorgefilipecosta Oct 14, 2026
8e12f68
Core Actions: Keep the core definition when replacing an action
jorgefilipecosta Oct 14, 2026
d423bda
Editor: Update the references to where the actions live
jorgefilipecosta Oct 14, 2026
1305ec2
Add the pull request link to the changelog entries
jorgefilipecosta Oct 14, 2026
d9557c3
Core Data: Type the actions configs like the view configs
jorgefilipecosta Oct 14, 2026
8d6e2c3
Editor: Keep the order of the element imports of usePostActions
jorgefilipecosta Oct 14, 2026
5940a74
Editor: Drop the changelog entry of the actions shown after the others
jorgefilipecosta Oct 14, 2026
f0e70af
Core Actions: Make the package private
jorgefilipecosta Oct 16, 2026
52fe2c8
Build: Stop registering the script modules a script imports
jorgefilipecosta Oct 16, 2026
828d19d
Add the pull request to the backport changelog of the entity actions API
jorgefilipecosta Oct 16, 2026
b8873c4
Core Data: Type the actions configs like the fields configs
oandregal Oct 16, 2026
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
1 change: 1 addition & 0 deletions backport-changelog/7.2/14276.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
https://github.com/WordPress/wordpress-develop/pull/14276

* https://github.com/WordPress/gutenberg/pull/84650
* https://github.com/WordPress/gutenberg/pull/84660
4 changes: 4 additions & 0 deletions docs/contributors/code/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ For features included in the Gutenberg plugin, the deprecation policy is intende
- `wp.editor.registerEntityField` and `wp.editor.unregisterEntityField` are deprecated since Gutenberg 24.3 and have no effect: the fields of an entity are registered on the server. Register and unregister fields in PHP instead, with `wp_register_entity_fields()` and `wp_unregister_entity_fields()` on the `wp_entity_fields_api_init` action, and a script module for the parts PHP cannot serialize (`render`, `Edit`, `getElements`…). Both functions remain exported, so existing calls keep working and only log a deprecation warning.
- `wp.blocks.isValidBlockContent` has been removed. Please use `wp.blocks.validateBlock` instead.

## 24.5

- `wp.editor.registerEntityAction` and `wp.editor.unregisterEntityAction`, deprecated since 24.3 and available only in the Gutenberg plugin, have no effect: every action of an entity is registered on the server. Register and unregister actions in PHP on the `wp_entity_actions_api_init` action instead, with a script module for the parts PHP cannot serialize (`callback`, `RenderModal`, `isEligible`…).

## 11.0.0

- `wp.blocks.registerBlockTypeFromMetadata` method has been removed. Use `wp.blocks.registerBlockType` method instead.
Expand Down
6 changes: 0 additions & 6 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2327,12 +2327,6 @@
"markdown_source": "../packages/core-abilities/README.md",
"parent": "packages"
},
{
"title": "@wordpress/core-actions",
"slug": "packages-core-actions",
"markdown_source": "../packages/core-actions/README.md",
"parent": "packages"
},
{
"title": "@wordpress/core-commands",
"slug": "packages-core-commands",
Expand Down
7 changes: 3 additions & 4 deletions docs/private-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,12 @@ Private exports:
### `core` store

Private actions:
- `receiveEntityActionsConfig`
- `receiveRegisteredPostMeta`
- `editMediaEntity`

Private selectors:
- `getEntityActionsConfig`
- `getBlockPatternsForPostType`
- `getEntityRecordPermissions`
- `getEntityRecordsPermissions`
Expand Down Expand Up @@ -289,18 +291,15 @@ Private exports:
Private actions:
- `createTemplate`
- `hideBlockTypes`
- `registerEntityAction`
- `registerPostTypeActions`
- `registerPostTypeSchema`
- `removeTemplates`
- `revertTemplate`
- `saveDirtyEntities`
- `setCurrentTemplateId`
- `setIsReady`
- `showBlockTypes`
- `unregisterEntityAction`

Private selectors:
- `getEntityActions`
- `getInserter`
- `getInserterSidebarToggleRef`
- `getListViewToggleRef`
Expand Down
33 changes: 32 additions & 1 deletion lib/compat/wordpress-7.2/entity-actions-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*
* Actions are registered on the `wp_entity_actions_api_init` action, and only there,
* with {@see wp_register_entity_actions()} and {@see wp_unregister_entity_actions()}. The
* other functions read the registry.
* other functions read the registry. WordPress core registers its own
* actions the same way, from packages/core-actions.
*
* The functions are defined when core does not ship them. When it does, the
* core functions read the plugin registry, which replaces the core one on
Expand Down Expand Up @@ -193,3 +194,33 @@ function _wp_add_entity_action_modules_to_editor_script( $scripts = null ) {
}
add_action( 'admin_footer', '_wp_add_entity_action_modules_to_editor_script', 10, 0 );
}

if ( ! function_exists( '_wp_preload_entity_actions' ) ) {
/**
* Preloads the actions of the post type the post editor edits.
*
* The actions menu of the post editor reads them from the `wp/v2/entity-actions`
* route as it loads, see `useEntityActions()` in
* packages/editor/src/dataviews/entity-actions-loader.ts. The path must match the
* request of the `getEntityActionsConfig` core data resolver, see
* packages/core-data/src/resolvers.js.
*
* @param array $paths REST API paths to preload.
* @param WP_Block_Editor_Context $context Block editor context.
* @return array Filtered preload paths.
*/
function _wp_preload_entity_actions( $paths, $context ) {
if ( 'core/edit-post' === $context->name && isset( $context->post ) ) {
$paths[] = add_query_arg(
array(
'kind' => 'postType',
'name' => $context->post->post_type,
),
'/wp/v2/entity-actions'
);
}

return $paths;
}
add_filter( 'block_editor_rest_api_preload_paths', '_wp_preload_entity_actions', 10, 2 );
}
2 changes: 1 addition & 1 deletion lib/compat/wordpress-7.2/entity-fields-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function _wp_add_entity_field_modules_to_editor_script( $scripts = null ) {
$scripts = wp_scripts();
}

// Field registration as well as actions live in packages/editor/src/dataviews/store/private-actions.ts
// The fields are loaded in packages/editor/src/dataviews/store/private-resolvers.ts
// which means any screen that wants to use this mechanism needs to load the editor script.
if ( ! $scripts->query( 'wp-editor', 'enqueued' ) && ! $scripts->query( 'wp-editor', 'done' ) ) {
return;
Expand Down
6 changes: 6 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ function gutenberg_get_experiment_default( $name ) {
}
require_once __DIR__ . '/../build/scripts/core-fields/index.php';
}
if ( is_dir( __DIR__ . '/../build/scripts/core-actions' ) ) {
require_once __DIR__ . '/../build/scripts/core-actions/index.php';
// The plugin registers its copy of the core actions instead of the one
// WordPress ships, which would otherwise register them first.
remove_action( 'wp_entity_actions_api_init', 'register_core_entity_actions', 0 );
}

// Block supports overrides.
require __DIR__ . '/block-supports/settings.php';
Expand Down
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 32 additions & 15 deletions packages/core-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The actions WordPress core registers for its entities, such as "Rename", "Duplicate" or "Move to trash". They are shown in the DataViews lists of the site editor and in the inspector of the editor.

Each collection of actions is a script module, which `registerPostTypeSchema()` of `@wordpress/editor` imports on demand to register the actions of a post type. The actions moved here from `@wordpress/fields`.
WordPress registers them on the server through the entity actions API, like a plugin registers its own: the data of each action, such as its label, is registered in PHP, and its JavaScript parts, such as its callback or its modal, are in a script module. The editor reads the actions of an entity from the `/wp/v2/entity-actions` REST API route, imports the script modules the route lists, and merges each action with its JavaScript parts. The actions moved here from `@wordpress/fields`.

## Collections

Expand All @@ -12,9 +12,9 @@ The actions of this package live in folders of `src`, one per collection:
- `wp_block`: the actions patterns have on top of the defaults (`duplicate-pattern` and `export-pattern`).
- `wp_template_part`: the actions template parts have on top of the defaults (`duplicate-template-part`).

Each action has a folder named after its id, with the action in `action.tsx`. The `index.ts` of each collection is its script module, whose default export maps the id of each action to the action.
Each action has a folder named after its id, with the data of the action in `action.php` and its JavaScript parts in `action.tsx`. The `index.ts` of each collection is its script module, whose default export maps the id of each action to its JavaScript parts.

`registerPostTypeSchema()` decides which post type gets which action:
`src/index.php` registers the actions of each post type exposed in the REST API, on the `wp_entity_actions_api_init` action at priority 0, so a plugin hooking it at the default priority can remove or replace them:

- `view-post`, for the viewable post types.
- `view-post-revisions`, for the post types supporting `revisions`.
Expand All @@ -26,31 +26,48 @@ Each action has a folder named after its id, with the action in `action.tsx`. Th
- `export-pattern`, for patterns.
- `restore`, `reset-post`, `delete-post`, `move-to-trash`, and `permanently-delete`, for every post type. Each one checks which posts it applies to.

The actions are registered in that order, which is the order they are shown in, for the current user: the actions depending on what the user can do are only registered for the users who can. The Gutenberg plugin copies the PHP files into its build, with its `gutenberg_` prefix.

## Prerequisites

The modules use the `wp.*` globals the `wp-editor` script loads, such as `wp.data` and `wp.components`, so they only run on pages that load the editor script. They are dynamic module dependencies of `wp-editor`, which adds them to the import map of every page loading it.

## Installation

Install the module:

```bash
npm install @wordpress/core-actions --save
```

_This package assumes that your code will run in an ES2015+ environment. If you're using an environment that has limited or no support for such language features and/or APIs, you should include the polyfill shipped in `@wordpress/babel-preset-default` in your code._

## Usage

The package provides the `@wordpress/core-actions/post_type_supports`, `@wordpress/core-actions/wp_block`, and `@wordpress/core-actions/wp_template_part` script modules. Their default export maps the id of each action to a DataViews action:
The package provides the `@wordpress/core-actions/post_type_supports`, `@wordpress/core-actions/wp_block`, and `@wordpress/core-actions/wp_template_part` script modules. Their default export maps the id of each action to the properties of the DataViews action that PHP cannot serialize, which complete the action the `/wp/v2/entity-actions` route returns:

```js
const { default: actions } = await import(
'@wordpress/core-actions/post_type_supports'
);
const renamePost = actions[ 'rename-post' ];
const renamePost = { ...actionFromTheServer, ...actions[ 'rename-post' ] };
```

A plugin replacing a core action unregisters it, then registers it again with its changes and with the script module of the core action, so the action keeps its JavaScript parts. Merging the changes into the definition `wp_unregister_entity_actions()` returns keeps the rest of the core action, such as its modal options, and registering only when something was unregistered keeps the action away from the users WordPress does not register it for:

```php
add_action( 'wp_entity_actions_api_init', function () {
$removed = wp_unregister_entity_actions( 'postType', 'page', array( 'move-to-trash' ) );
if ( ! $removed ) {
return;
}
wp_register_entity_actions(
'acme',
'postType',
'page',
array(
array_merge(
$removed[0],
array( 'label' => __( 'Move to the bin…', 'acme' ) )
),
),
'@wordpress/core-actions/post_type_supports'
);
} );
```

The actions are shown in registration order, so an action registered again moves to the end of the actions of the entity. A property the script module provides takes precedence over the one registered in PHP: the label of `view-post-revisions`, which shows the number of revisions, comes from its module.

## Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects.
Expand Down
11 changes: 11 additions & 0 deletions packages/core-actions/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "@wordpress/core-actions",
"version": "0.1.0-prerelease",
"private": true,
"description": "The actions WordPress core registers for its entities, shown in DataViews and in the editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
Expand Down Expand Up @@ -49,6 +50,16 @@
"./wp_block": "./build-module/wp_block/index.mjs",
"./wp_template_part": "./build-module/wp_template_part/index.mjs"
},
"wpCopyFiles": {
"files": [
"src/**/*.php"
],
"transforms": {
"php": {
"functionPrefix": "gutenberg_"
}
}
},
"types": "build-types",
"sideEffects": false,
"dependencies": {
Expand Down
88 changes: 88 additions & 0 deletions packages/core-actions/src/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php
/**
* Registers the actions of WordPress core through the entity actions API, like a
* plugin registers its own.
*
* The folders next to this file are the script modules of the actions:
* `post_type_supports` for the actions of every post type, `wp_block` for
* the actions of patterns, and `wp_template_part` for the actions of
* template parts. Each action has a folder named after its id, whose
* `action.php` returns the serializable part of the action, and whose
* `action.tsx` holds its JavaScript parts.
*
* @package WordPress
*/

/**
* Registers the actions of WordPress core on each post type exposed in the
* REST API, from what it supports and what the current user can do:
*
* - `view-post`, for the viewable post types.
* - `view-post-revisions`, for the post types supporting `revisions`.
* - `duplicate-post`, for the post types the user can create, except
* patterns, templates, and template parts. Only in the Gutenberg plugin.
* - `duplicate-template-part`, for template parts, when the user can create
* them and the theme is a block theme.
* - `duplicate-pattern`, for patterns, when the user can create them.
* - `rename-post`, for the post types supporting `title`.
* - `order-pages`, for the post types supporting `page-attributes`.
* - `export-pattern`, for patterns.
* - `restore`, `reset-post`, `delete-post`, `move-to-trash`, and
* `permanently-delete`, for every post type: each one checks which posts
* it applies to.
*
* The actions are registered in that order, which is the order they are
* shown in, each with the script module of its folder. The registry is built
* for each request, so the actions only depending on the user are registered
* for the current one.
*
* Hooked at priority 0, so a plugin hooking `wp_entity_actions_api_init` at the
* default priority sees the core actions registered, and can unregister or
* replace them.
*
* @since 7.2.0
*/
function register_core_entity_actions() {
$is_gutenberg = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN;
$is_block_theme = wp_is_block_theme();
$design_types = array( 'wp_block', 'wp_template', 'wp_template_part' );
$definitions = array();

// Each file is required in its own variable scope.
$require = static function ( $file ) {
return require $file;
};

foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
$name = $post_type->name;
$can_create = current_user_can( $post_type->cap->create_posts );

// The actions of the post type, keyed by the folder of their script
// module and their id, in the order they are shown.
$applies = array(
'post_type_supports/view-post' => is_post_type_viewable( $post_type ),
'post_type_supports/view-post-revisions' => post_type_supports( $name, 'revisions' ),
'post_type_supports/duplicate-post' => $is_gutenberg && $can_create && ! in_array( $name, $design_types, true ),
'wp_template_part/duplicate-template-part' => 'wp_template_part' === $name && $can_create && $is_block_theme,
'wp_block/duplicate-pattern' => 'wp_block' === $name && $can_create,
'post_type_supports/rename-post' => post_type_supports( $name, 'title' ),
'post_type_supports/order-pages' => post_type_supports( $name, 'page-attributes' ),
'wp_block/export-pattern' => 'wp_block' === $name,
'post_type_supports/restore' => true,
'post_type_supports/reset-post' => true,
'post_type_supports/delete-post' => true,
'post_type_supports/move-to-trash' => true,
'post_type_supports/permanently-delete' => true,
);

foreach ( array_keys( array_filter( $applies ) ) as $key ) {
list( $module, $id ) = explode( '/', $key );
if ( ! isset( $definitions[ $key ] ) ) {
// The id comes first, like in the actions registered directly.
$definitions[ $key ] = array_merge( array( 'id' => $id ), $require( __DIR__ . "/{$key}/action.php" ) );
}
wp_register_entity_actions( 'core', 'postType', $name, array( $definitions[ $key ] ), "@wordpress/core-actions/{$module}" );
}
}
}
add_action( 'wp_entity_actions_api_init', 'register_core_entity_actions', 0 );
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/**
* The "Delete…" action, for every post type: it applies to the templates,
* template parts, and patterns the user created. Its JavaScript parts
* (`icon`, `isEligible`, and `RenderModal`) are in `action.tsx`, next to
* this file.
*
* @package WordPress
*/

return array(
'label' => __( 'Delete…', 'gutenberg' ),
'isPrimary' => true,
'supportsBulk' => true,
'hideModalHeader' => true,
'modalFocusOnMount' => 'firstContentElement',
);
Loading
Loading