Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ebb589d
Core data: Add a private `getFieldsConfig` selector
oandregal Oct 14, 2026
50087a1
Editor: Add `useFields` to read the fields from the server
oandregal Oct 15, 2026
0e1b089
Entity fields API: Preload the fields of the screens that read them
oandregal Oct 15, 2026
684c0b6
Editor: Read the post summary fields with `useFields`
oandregal Oct 14, 2026
ed23666
Editor: Read the post fields from the server in `usePostFields`
oandregal Oct 15, 2026
f3e69c1
E2E: Exercise the Entity fields API on the Pages list
oandregal Oct 14, 2026
2940400
Changelogs: Note that the screens read their fields from the server
oandregal Oct 15, 2026
64dc0f0
Editor: Register only the attachment fields on the client
oandregal Oct 15, 2026
913eb55
Editor: Remove the unused content preview field
oandregal Oct 15, 2026
d6eba04
Media editor: Lay out the fields with a fixed or given form
oandregal Oct 15, 2026
a32f5be
View config: Provide the form of the media editor for attachments
oandregal Oct 15, 2026
330d2a3
Entity fields API: Preload the attachment fields of the media editor
oandregal Oct 15, 2026
41c0ab2
Editor: Resolve the fields of an entity from the server
oandregal Oct 15, 2026
7ad8570
Media editor: Lay out the attachment fields with the server form
oandregal Oct 15, 2026
2cb3ae3
E2E: Stop exercising fields registered on the client
oandregal Oct 15, 2026
b4fd7de
Changelogs: Note the deprecation of the client field registration
oandregal Oct 15, 2026
a8e7eee
Entity fields API: Preload the attachment form of the media editor route
oandregal Oct 15, 2026
9f434f2
Editor: Show a snackbar when the post fields fail to load
oandregal Oct 15, 2026
3cc0ce5
Fix error notice component syntax
oandregal Oct 15, 2026
7685cce
Deprecations: List the client field registration without a removal ve…
oandregal Oct 15, 2026
e3a5518
Entity fields API: Move the route preloading to the experimental pages
oandregal Oct 15, 2026
2c601d5
DataForm inspector: Call the entity fields preload helper directly
oandregal Oct 15, 2026
7992acf
Editor: Indent the fields error notice of the post summary
oandregal Oct 15, 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/13911.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ https://github.com/WordPress/wordpress-develop/pull/13911
* https://github.com/WordPress/gutenberg/pull/84168
* https://github.com/WordPress/gutenberg/pull/84691
* https://github.com/WordPress/gutenberg/pull/84696
* https://github.com/WordPress/gutenberg/pull/84700
1 change: 1 addition & 0 deletions docs/contributors/code/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ For features included in the Gutenberg plugin, the deprecation policy is intende

## Unreleased

- `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.

## 11.0.0
Expand Down
101 changes: 101 additions & 0 deletions lib/compat/wordpress-7.2/preload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php
/**
* Preload paths for the entity fields.
*
* @package gutenberg
*/

/**
* Returns the paths that serve the fields of the given post types, skipping
* the post types that are not registered.
*
* A path must match the request of the `getFieldsConfig` core data resolver,
* see packages/core-data/src/resolvers.js.
*
* @param string[] $post_types The post types.
* @return string[] One path of the `wp/v2/entity-fields` route per post type, in the
* order the post types are given, without duplicates.
*/
function _gutenberg_get_post_type_fields_preload_paths( $post_types ) {
$paths = array();
foreach ( array_unique( $post_types ) as $post_type ) {
if ( ! post_type_exists( $post_type ) ) {
continue;
}
$paths[] = add_query_arg(
array(
'kind' => 'postType',
'name' => $post_type,
),
'/wp/v2/entity-fields'
);
}

return $paths;
}

/**
* Returns the post types whose fields the screen of the site editor being
* loaded lists, from its `p` query arg.
*
* The screens are the routes in
* packages/edit-site/src/components/site-editor-routes: the lists and the
* editors of pages, templates, patterns, and template parts. The patterns
* screen lists both patterns and template parts.
*
* @return string[] The post types, empty for any other screen.
*/
function _gutenberg_get_site_editor_screen_post_types() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks what to preload.
$path = isset( $_GET['p'] ) && is_string( $_GET['p'] ) ? sanitize_text_field( wp_unslash( $_GET['p'] ) ) : '';
$segments = explode( '/', trim( $path, '/' ) );
Comment thread
oandregal marked this conversation as resolved.

switch ( $segments[0] ) {
case 'page':
return array( 'page' );
case 'template':
case 'wp_template':
return array( 'wp_template' );
case 'pattern':
return array( 'wp_block', 'wp_template_part' );
case 'wp_block':
return array( 'wp_block' );
case 'wp_template_part':
return array( 'wp_template_part' );
}
return array();
}

/**
* Preloads the fields of the post types the site editor lists.
*
* The site editor reads them from the `wp/v2/entity-fields` route before it
* can list the post fields, see `useFields` in
* packages/editor/src/dataviews/fields-loader/index.ts: those of the edited
* post, if any, and of the post types of the screen it opens on.
*
* The post editor only reads them in the post summary of the DataForm-based
* inspector, which is an experiment that preloads them on its own, see
* lib/experimental/dataform-inspector-preload.php.
*
* @param array $paths REST API paths to preload.
* @param WP_Block_Editor_Context $context Block editor context.
* @return array Filtered preload paths.
*/
function _gutenberg_preload_entity_fields( $paths, $context ) {
if ( 'core/edit-site' !== $context->name ) {
return $paths;
}

$post_types = array();
if ( isset( $context->post ) ) {
$post_types[] = $context->post->post_type;
}
$post_types = array_merge( $post_types, _gutenberg_get_site_editor_screen_post_types() );

return array_merge(
$paths,
_gutenberg_get_post_type_fields_preload_paths( $post_types )
);
}
add_filter( 'block_editor_rest_api_preload_paths', '_gutenberg_preload_entity_fields', 10, 2 );
64 changes: 63 additions & 1 deletion lib/compat/wordpress-7.2/view-config-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,69 @@ function _gutenberg_get_entity_view_config_root_site( $data ) {
);
}

/**
* Provides the view configuration for the `attachment` post type.
*
* An attachment is edited through a form (the media editor) that shares
* nothing with the default post type form, so the `form` is replaced: the
* editable fields first, in a regular layout, then the metadata and the file
* information, in panels. The generic `default_view`, `default_layouts`, and
* `view_list` built by gutenberg_get_entity_view_config() are left untouched.
*
* Core has no callback for this entity, so this is a base definition rather
* than a layer on top of one.
*
* @param Gutenberg_View_Config_Data $data The view configuration container for the entity.
* @return Gutenberg_View_Config_Data The updated view configuration container.
*/
function _gutenberg_get_entity_view_config_posttype_attachment( $data ) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This config is used for the post_type/attachment. See media-form at packages/media-editor/src/components/media-form/index.tsx.

$regular_layout = array(
'type' => 'regular',
'labelPosition' => 'top',
);

return $data->set(
array(
'form' => array(
'layout' => array( 'type' => 'panel' ),
'fields' => array(
array(
'id' => 'title',
'layout' => $regular_layout,
),
array(
'id' => 'alt_text',
'layout' => $regular_layout,
),
array(
'id' => 'caption',
'layout' => $regular_layout,
),
array(
'id' => 'description',
'layout' => $regular_layout,
),
'date',
'author',
'filename',
'mime_type',
'filesize',
'media_dimensions',
'attached_to',
),
),
),
1
);
}

/**
* Post types whose base definition provides its own `form`, without the
* `status` and `discussion` groups of the default post type form.
*
* @var string[]
*/
const GUTENBERG_VIEW_CONFIG_POST_TYPES_WITH_OWN_FORM = array( 'wp_block', 'wp_template', 'wp_template_part' );
const GUTENBERG_VIEW_CONFIG_POST_TYPES_WITH_OWN_FORM = array( 'attachment', 'wp_block', 'wp_template', 'wp_template_part' );

/**
* Makes the panel summary of the `status` and `discussion` groups explicit in
Expand Down Expand Up @@ -244,6 +300,12 @@ function gutenberg_register_entity_view_config_filters_7_2() {
5,
1
);
add_filter(
gutenberg_get_entity_view_config_hook_name( 'postType', 'attachment' ),
'_gutenberg_get_entity_view_config_posttype_attachment',
5,
1
);
add_filter(
gutenberg_get_entity_view_config_hook_name( 'root', 'site' ),
'_gutenberg_get_entity_view_config_root_site',
Expand Down
4 changes: 4 additions & 0 deletions lib/experimental/dataform-inspector-preload.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ function gutenberg_dataform_inspector_preload_paths( $paths, $context ) {
// match the resulting `_fields=form` REST API request.
$paths[] = '/wp/v2/view-config?kind=postType&name=' . $context->post->post_type . '&_fields=form';

// `DataFormPostSummary` reads the fields of the post type through
// `useFields`, from the `wp/v2/entity-fields` route.
$paths = array_merge( $paths, _gutenberg_get_post_type_fields_preload_paths( array( $context->post->post_type ) ) );

return $paths;
}
add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_dataform_inspector_preload_paths', 10, 2 );
112 changes: 112 additions & 0 deletions lib/experimental/route-entity-fields-preload.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
/**
* Preload paths for the entity fields of the routes of the experimental pages:
* the extensible site editor and the media editor.
*
* The paths themselves come from `_gutenberg_get_post_type_fields_preload_paths()`,
* see lib/compat/wordpress-7.2/preload.php.
*
* @package gutenberg
*/

/**
* Returns the post types whose fields the route being loaded lists or edits,
* from its `p` query arg.
*
* The routes are the entry points in /routes whose screens make up the
* extensible site editor and the media editor page. A route that shows no
* post fields, such as `/styles`, contributes no post type. The paths without
* a screen of their own (`/templates`, `/patterns`, `/types/<post type>`, …)
* redirect client-side to their list route, and are what the menu items of
* the page link to, so they preload what the list they land on needs.
*
* @return string[] The post types, empty for any other route.
*/
function _gutenberg_get_route_post_types() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only picks what to preload.
$path = isset( $_GET['p'] ) && is_string( $_GET['p'] ) ? sanitize_text_field( wp_unslash( $_GET['p'] ) ) : '';
// The router keeps the search of the route in `p`, as in `/types/page/list/all?page=2`.
$path = explode( '?', $path )[0];
$segments = explode( '/', trim( $path, '/' ) );

switch ( $segments[0] ) {
case 'types':
// `/types/<post type>` and its `list`, `edit`, and `new` routes.
$post_type = isset( $segments[1] ) ? sanitize_key( $segments[1] ) : '';
return '' === $post_type ? array() : array( $post_type );
case 'templates':
return array( 'wp_template' );
case 'template-parts':
return array( 'wp_template_part' );
case 'patterns':
return array( 'wp_block' );
case 'navigation':
return array( 'wp_navigation' );
case 'media-editor':
return array( 'attachment' );
}
return array();
}

/**
* Returns the paths of the view config the route being loaded resolves before
* it renders.
*
* Only the media editor route does: its loader resolves the `form` of the
* attachment view config, see routes/media-editor/route.ts. It is the only
* route whose post type is `attachment`. A path must match the request of the
* `getViewConfig` core data resolver, `_fields` included.
*
* @return string[] The paths of the `wp/v2/view-config` route, empty for any other route.
*/
function _gutenberg_get_route_view_config_preload_paths() {
if ( array( 'attachment' ) !== _gutenberg_get_route_post_types() ) {
return array();
}

return array( '/wp/v2/view-config?kind=postType&name=attachment&_fields=form' );
}

/**
* Preloads the fields of the post types the route being loaded lists or edits,
* and the view config it resolves before it renders.
*
* A page booted by `@wordpress/boot` prints a preloading middleware of its own
* with the paths every one of its routes needs, see the `page.php` template of
* `@wordpress/build`. Which fields a screen needs depends on the route, which
* that list cannot tell, so they are preloaded here as a second middleware:
* `api-fetch` asks each middleware in turn, and one that does not hold the
* path of a request passes it on.
*
* The `init` action of a page fires before the page prints its own preloaded
* data, and `wp-api-fetch` is registered by then, so the inline script is
* attached to the handle and printed along with it.
*/
function _gutenberg_preload_route_entity_fields() {
$paths = array_merge(
_gutenberg_get_post_type_fields_preload_paths( _gutenberg_get_route_post_types() ),
_gutenberg_get_route_view_config_preload_paths()
);
if ( empty( $paths ) ) {
return;
}

// `rest_preload_api_request()` only keeps the responses that succeeded, so
// an entity whose fields the current user cannot read leaves the request to the
// client rather than preloading the error.
$preload_data = array_reduce( $paths, 'rest_preload_api_request', array() );
if ( empty( $preload_data ) ) {
return;
}

wp_add_inline_script(
'wp-api-fetch',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'after'
);
}
add_action( 'site-editor-v2_init', '_gutenberg_preload_route_entity_fields' );
add_action( 'media-editor-wp-admin_init', '_gutenberg_preload_route_entity_fields' );
8 changes: 8 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function gutenberg_get_experiment_default( $name ) {
require __DIR__ . '/compat/wordpress-7.2/notes.php';
require __DIR__ . '/compat/wordpress-7.2/class-wp-entity-fields-registry.php';
require __DIR__ . '/compat/wordpress-7.2/entity-fields-api.php';
require __DIR__ . '/compat/wordpress-7.2/preload.php';
require __DIR__ . '/compat/wordpress-7.2/class-gutenberg-rest-entity-fields-controller-7-2.php';
require __DIR__ . '/compat/wordpress-7.2/class-wp-entity-actions-registry.php';
require __DIR__ . '/compat/wordpress-7.2/entity-actions-api.php';
Expand Down Expand Up @@ -168,6 +169,13 @@ function gutenberg_get_experiment_default( $name ) {
if ( gutenberg_is_experiment_enabled( 'gutenberg-dataform-inspector' ) ) {
require __DIR__ . '/experimental/dataform-inspector-preload.php';
}

if (
gutenberg_is_experiment_enabled( 'gutenberg-extensible-site-editor' ) ||
gutenberg_is_experiment_enabled( 'gutenberg-media-editor' )
) {
require __DIR__ . '/experimental/route-entity-fields-preload.php';
}
if ( gutenberg_is_experiment_enabled( 'gutenberg-media-editor' ) ) {
require __DIR__ . '/experimental/media-editor/load.php';
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

4 changes: 4 additions & 0 deletions packages/core-data/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

- Keep read-only taxonomy term data out of collaborative edits so stale counts and links do not mark unchanged categories as dirty ([#84838](https://github.com/WordPress/gutenberg/pull/84838)).

### Internal

- Add the private `getFieldsConfig( kind, name )` selector, which returns the fields an entity registered on the server, as the `/wp/v2/entity-fields` REST API route returns them ([#84700](https://github.com/WordPress/gutenberg/pull/84700)).

## 8.2.0 (2026-10-07)

### Enhancements
Expand Down
18 changes: 18 additions & 0 deletions packages/core-data/src/private-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,24 @@ export function receiveViewConfig( kind, name, config ) {
};
}

/**
* Returns an action object used to receive the fields config of an entity.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {Object} config Fields config, as returned by the `/wp/v2/entity-fields` route.
*
* @return {Object} Action object.
*/
export function receiveFieldsConfig( kind, name, config ) {
return {
type: 'RECEIVE_FIELDS_CONFIG',
kind,
name,
config,
};
}

/**
* Records that the entity sync manager opened a new undo level for a record
* it syncs, so the level takes its place in core-data's undo history next to
Expand Down
Loading
Loading