Skip to content

Fields API - #83368

Draft
oandregal wants to merge 188 commits into
trunkfrom
add/fields-api-endpoint
Draft

oandregal wants to merge 188 commits into
trunkfrom
add/fields-api-endpoint

Conversation

@oandregal

@oandregal oandregal commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Part of #74865
Backport PR at WordPress/wordpress-develop#13911

What?

This PR implements an extensible Fields API on the server. Consumers tap a fields_api_init action to register their fields. Two examples ship in this PR:

  1. An e2e test plugin registering fields imperatively in a variety of scenarios: declarative-only fields, fields with JavaScript (script modules), fields with data (REST), fields with CSS.
function gutenberg_test_fields_api_add_field_declarative( $registry ) {
	$registry->register(
		'gutenberg-test-fields-api', // Origin.
		'postType',                  // Kind.
		'page',                      // Name.
		array(
			array(
				'id'            => 'menu_order',
				'type'          => 'integer',
				'label'         => 'Order',
				'description'   => 'Position of the page among its siblings.',
				'enableSorting' => false,
				'filterBy'      => false,
			),
		)
	);
}
add_action( 'fields_api_init', 'gutenberg_test_fields_api_add_field_declarative' );
  1. Core's own fields, registered declaratively from folders of field.php files:
function register_core_field_collections( $registry ) {
	wp_register_field_collection( $registry, __DIR__ . '/page' );
}
add_action( 'fields_api_init', 'register_core_field_collections', 0 );

Why?

Part of #74865

How?

Server: fields_api_init hook, the registry, and the API

  • Gutenberg_Fields_Registry holds the fields per entity (kind, name) and its script module. It exposes register(), update() and unregister().
  • The registry only accepts writes while the fields_api_init action fires, and it fires that action lazily the first time something reads it. This is because fields are registered based on the post types and supports, so they need to be registered after init but before the REST route/script modules are requested.
  • lib/compat/wordpress-7.2/fields-api.php wraps the registry in functions and adds wp_register_field_collection(), the declarative format: a folder with index.php and one subfolder per field whose field.php returns the serializable part of that field. See core-fields/src/page/field.php for an example.

Transport: /wp/v2/fields and preloading

Client: core-data → fields-loader → script modules

  • @wordpress/core-data gains a public getFieldsConfig( kind, name ) selector (plus resolver, reducer and the FieldsConfig type) that requests and caches the route.
  • @wordpress/fields-loader, a new package, exposes useFields() for components and loadFields() for route loaders and other code outside React. They merge each script module's default export into the fields that came from REST.
  • The modules reach the browser through the import map: _gutenberg_add_field_modules_to_editor_script() runs on admin_footer, reads the registry, and declares every registered module as a dynamic module_dependencies entry of the wp-editor script. Dynamic means the module is listed in the import map but only fetched when the loader imports it.

Core's fields: @wordpress/core-fields

  • A new package holding every field definition, PHP for the serializable part and TypeScript for the rest, structured per entity: attachment, page, wp_template, wp_template_part, wp_block.
  • We also register (see post_type_supports folder) the default fields for every REST-exposed post type from what that post type/theme supports. This serves to bootstrap the fields. A post type can opt out of a registered default field using the fields_api_post_type_supports_exclusions filter (example), or just unregister it in fields_api_init.

Screens

  • Every screen has been migrated from usePostFields to the new loadFields()/useFields() utils.
  • Some screens still define local fields. Sometimes all of them; some other times in addition to the ones coming from core-fields. This table lists all the exceptions so far:
Screen Local field Reason
dataform-post-summary revisions Depends on the editor store (circular dependency if moved to core-fields)
dataform-post-summary readingSettings Depends on the editor store (circular dependency if moved to core-fields)
post-revisions-timeline all of them Post revisions are not a registered entity: the rows come from the editor store's revision pages, and the fields render the picker row out of editor-only parts (autosave badge, content information for the selected revision)
apply-globally-modal all of them No entity behind the rows: they are an in-memory diff of the block's styles against the merged global styles, built by useReviewRows
revisions-list all of them Global styles revisions are not a registered entity, and the list adds the synthetic unsaved and parent rows the server knows nothing about
media-upload-modal all of them core-fields depends on media-utils, so reading the server's attachment fields here is a genuine circular dependency; the modal's list also diverges from the registered one (the thumbnail cannot be hidden, title falls back to (no title), and attachedTo filters on the modal's post context)
page-patterns preview Depends on editor components (circular dependency if moved to core-fields)
page-templates preview Depends on editor components (circular dependency if moved to core-fields)
template-list preview Depends on editor components, Preview from @wordpress/lazy-editor, which the core-fields script modules cannot pull in
template-part-list preview Depends on editor components, Preview from @wordpress/lazy-editor, which the core-fields script modules cannot pull in
pattern-list preview Depends on editor components, Preview from @wordpress/lazy-editor, which the core-fields script modules cannot pull in
pattern-list category The rows are not wp_block records: use-patterns merges theme patterns, which are not posts, with user patterns and synthesizes categories as slugs; the field's elements come from the two client-side category sources
site-health all of them No entity behind the rows: they are site health check results normalized from the REST responses
experiments-home all of them No entity behind the form: one boolean field per experiment, generated from the experiments the plugin reports
block-guidelines all of them No entity behind the rows: they are the registered block types alongside their guideline text
guideline-accordion-form all of them No entity behind the form: a single textarea over the guideline text of the block type the accordion has open

What changes for consumers

  • wp.editor.registerEntityField and wp.editor.unregisterEntityField are deprecated and have no effect. Fields need to be registered in PHP on fields_api_init instead.
  • The private usePostFields hook is gone, and the core/editor store no longer holds fields: its private registerEntityField, unregisterEntityField and getEntityFields are gone, and registerPostTypeSchema now registers only actions.
  • The format field comes from the server, offered when the post type supports post-formats. Before, it also checked the disablePostFormats editor setting, which only the post editor screen set — so it is now hidden consistently in the site editor for themes without post formats.
  • The content-preview field is no longer registered for post types supporting editor. No view referenced it; the views that want a preview declare their own.

How to merge this PR

I'm going to be away a few days, so I wanted to share how would I do it in case anyone is available/willing to pick it up. Feel free to use your own approach. This can also serve as a map of relationships.

Stage 1: prepare, no actual behaviour changes.

Each can be its own PR, some can be merged together.

  1. Merge server side APIs (registry, fields api, endpoint, preload):
    • lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.php
    • lib/compat/wordpress-7.2/class-gutenberg-fields-registry.php\
    • lib/compat/wordpress-7.2/class-gutenberg-rest-fields-controller-7-2.php
    • lib/compat/wordpress-7.2/fields-api.php
    • lib/compat/wordpress-7.2/preload.php
    • lib/compat/wordpress-7.2/rest-api.php
    • lib/load.php
    • All files under phpunit but phpunit/view-config-api-test.php.
  2. Copy MediaEdit to media-utils as private API:
    • All files under packages/media-utils.
    • Stop re-exporting MediaEdit from @wordpress/fields (optional, see thread)
    • tools/eslint/suppressions.json.
  3. Merge new core-fields package:
    • All files under packages/private-apis/.
    • All files under packages/core-fields/.
    • Verify it doesn't become a global under wp.*.
    • Updates to .github/labeler.yml.
  4. Merge data mechanism:
    • All files under packages/core-data.
    • All files under packages/fields-loader.
      • Verify the new package doesn't become a global under wp.*.
      • Updates to .github/labeler.yml.
      • tools/validation/check-private-apis.config.json
      • All files under packages/dependency-extraction-webpack-plugin/.
  5. Fix docs:
    • docs/private-apis.md

Stage 2: switch consumers.

It can be done one-by-one.

  1. Demo plugin
    • All files under packages/e2e-tests/plugins/fields-api/.
    • test/e2e/specs/site-editor/fields-api.spec.js
  2. Site Editor v1
    • All files under packages/edit-site/ but packages/edit-site/src/components/sidebar-identity/index.jsx
  3. Media editor:
    • All files under packages/media-editor.
    • All files under routes/media-editor.
    • lib/compat/wordpress-7.2/view-config-api.php
    • phpunit/view-config-api-test.php
    • packages/edit-site/src/components/sidebar-identity/index.jsx
    • packages/editor/src/components/media/media-editor-modal.jsx
    • All fields under routes/identity/
  4. Site Editor v2
    • All files under routes/navigation-list/.
    • All files under routes/pattern-list/.
    • All files under routes/post-list/.
    • All files under routes/template-list/.
    • All files under routes/template-part-list/.
  5. Editor
    • packages/edit-post/src/index.jsx
    • docs/contributors/code/deprecations.md.
    • All remaining files under packages/editor/.
  6. Do not re-export MediaEdit from @wordpress/fields. (thread)
  7. Prevent PRs from being merged into @wordpress/fields and @wordpress/media-fields. To be decided: lint rule, docs?

Stage 3: after WordPress 7.2 release.

  1. Deprecate packages @wordpress/fields and @wordpress/media-fields.

Testing Instructions

Visit the existing screens and verify they still work as expected:

  • Post and page lists, including quick edit and the per-view field controls.
  • Templates, template parts, patterns and navigation lists.
  • The post summary sidebar in the post editor.
  • The media library and the attachment editor form.

Use of AI Tools

Used Claude to analyze and create parts of this pull request. The author reviewed all the changes.

@oandregal oandregal self-assigned this Sep 22, 2026
@github-actions github-actions Bot added [Package] Editor /packages/editor [Package] Fields /packages/fields labels Sep 22, 2026
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

🏷️ Labels

This pull request needs exactly one label indicating its type, and has 0.

  • Required: any label starting with [Type].
  • Found: none.

Read more about Type labels in Gutenberg. If you cannot add labels yourself, a reviewer can do it for you.

📦 Bundle size

Size Change: +138 kB (+1.65%)

Total Size: 8.52 MB

📦 View Changed
Filename Size Change
build/modules/core-fields/attachment.min.js 42.8 kB +42.8 kB (new file) 🆕
build/modules/core-fields/page.min.js 6.41 kB +6.41 kB (new file) 🆕
build/modules/core-fields/post_type_supports.min.js 32.2 kB +32.2 kB (new file) 🆕
build/modules/core-fields/root_site.min.js 750 B +750 B (new file) 🆕
build/modules/core-fields/wp_block.min.js 40.4 kB +40.4 kB (new file) 🆕
build/modules/core-fields/wp_template_part.min.js 40.1 kB +40.1 kB (new file) 🆕
build/modules/core-fields/wp_template.min.js 7.35 kB +7.35 kB (new file) 🆕
build/scripts/core-data/index.min.js 79 kB +119 B (+0.15%)
build/scripts/edit-post/index.min.js 53 kB +16 B (+0.03%)
build/scripts/edit-site/index.min.js 386 kB -388 B (-0.1%)
build/scripts/editor/index.min.js 591 kB -31.5 kB (-5.06%) ✅
build/scripts/private-apis/index.min.js 1.16 kB +4 B (+0.35%)
build/styles/editor/style-rtl.css 32.6 kB -64 B (-0.2%)
build/styles/editor/style-rtl.min.css 27.6 kB -58 B (-0.21%)
build/styles/editor/style.css 32.6 kB -65 B (-0.2%)
build/styles/editor/style.min.css 27.6 kB -58 B (-0.21%)

7a3749f Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric c7c9fac trunk % Change
timeToFirstByte 53.5 ms +4.3% -2.43% 53.05 ms +6.22% -2.07% 0.85%
largestContentfulPaint 90 ms +8.89% -2.22% 96 ms +2.08% -8.33% -6.25%
lcpMinusTtfb 36.6 ms +14.62% -7.92% 42.05 ms +7.25% -20.45% -12.96%
wpBeforeTemplate 28.22 ms +6.87% -1.59% 28.54 ms +4.56% -2.03% -1.12%
wpTemplate 21.52 ms +0.98% -6.92% 20.82 ms +3.41% -3.6% 3.36%
wpTotal 50.02 ms +3.82% -3.04% 49.37 ms +6.26% -2.15% 1.32%
wpMemoryUsage 7.62 MB +0% -0% 7.57 MB +0% -0% 0.59%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric c7c9fac trunk % Change
timeToFirstByte 41.9 ms +5.97% -3.22% 45.4 ms +3.63% -1.21% -7.71%
largestContentfulPaint 92 ms +0% -4.35% 100 ms +0% -0% -8%
lcpMinusTtfb 48.3 ms +2.8% -1.55% 54.7 ms +1.01% -0.64% -11.7%
wpBeforeTemplate 26.39 ms +5.72% -2.5% 26.39 ms +1.14% -0.91% 0%
wpTemplate 12.11 ms +9.25% -1.32% 16.1 ms +1.06% -2.05% -24.78%
wpTotal 38.86 ms +6.9% -2.78% 42.33 ms +3.92% -0.99% -8.2%
wpMemoryUsage 6.14 MB +0% -0% 6.20 MB +0% -0% -0.92%
wpDbQueries 10 +0% -0% 14 +0% -0% -28.57%

media-processing

Metric c7c9fac trunk % Change
mediaProcessingJpeg 399.83 ms +0.39% -0.72% 396.64 ms +1.97% -0.61% 0.8%
mediaProcessingAvif 6073.68 ms +0.52% -0.07% 6049.79 ms +0.03% -0.23% 0.39%
mediaProcessingJpegToAvif 4179.86 ms +0.12% -0.17% 4171.51 ms +0.1% -0.19% 0.2%

media-upload

Metric c7c9fac trunk % Change
jpegUploadProcessing 1426.78 ms +37.03% -1.2% 1412.82 ms +0.6% -0.39% 0.99%
pngUploadProcessing 180.19 ms +5.21% -3.54% 198.39 ms +8.41% -12.91% -9.17%
largeJpegUploadProcessing 1403.38 ms +0.5% -0.43% 1415.74 ms +1.19% -0.37% -0.87%
multipleImageUploadProcessing 1556.88 ms +0.38% -0.64% 1565.89 ms +11.97% -1.76% -0.58%

post-editor

Metric c7c9fac trunk % Change
serverResponse 342.27 ms +3.03% -11.01% 331.86 ms +4.36% -8.57% 3.14%
firstPaint 202.38 ms +11.49% -6.25% 175.78 ms +32.47% -7.21% 15.13%
domContentLoaded 895.75 ms +2.22% -0.79% 893.52 ms +0.45% -1.05% 0.25%
loaded 897.01 ms +2.21% -0.81% 894.71 ms +0.46% -1.06% 0.26%
firstContentfulPaint 387.51 ms +5.91% -1.67% 381.49 ms +2.04% -3.3% 1.58%
firstBlock 2615.94 ms +0.98% -0.87% 2609.85 ms +0.36% -0.75% 0.23%
type 17.87 ms +0.34% -6.55% 16.83 ms +5.35% -6.77% 6.18%
typeWithoutInspector 17.17 ms +3.32% -7.63% 17.16 ms +1.22% -2.86% 0.06%
typeWithTopToolbar 21.73 ms +3.5% -5.48% 22.49 ms +5.65% -5.91% -3.38%
typeContainer 7.99 ms +2.88% -10.39% 7.89 ms +3.8% -5.7% 1.27%
focus 94.93 ms +4.25% -4.23% 95.03 ms +2.16% -2.54% -0.11%
firstFocus 198.93 ms +0% -0% 195.77 ms +0% -0% 1.61%
selectAll 448.84 ms +3.7% -2.25% 458.01 ms +4.33% -0.29% -2%
listViewOpen 65.89 ms +2.76% -3.37% 63.98 ms +8.5% -6.72% 2.99%
inserterOpen 36.08 ms +6.15% -8.48% 32.99 ms +11.15% -1.64% 9.37%
inserterHover 11.67 ms +4.37% -4.46% 11.31 ms +5.04% -6.72% 3.18%
inserterSearch 7.74 ms +4.91% -6.46% 7.79 ms +4.36% -3.98% -0.64%
loadPatterns 522.34 ms +5.78% -1.62% 527.51 ms +8.79% -4.15% -0.98%
wpTotal 333.5 ms +3.06% -11.22% 323.06 ms +4.48% -8.88% 3.23%
wpMemoryUsage 13.43 MB +0% -0% 13.27 MB +0% -0% 1.26%
wpDbQueries 54 +0% -1.85% 54 +0% -0% 0%

site-editor

Metric c7c9fac trunk % Change
serverResponse 522.1 ms +2.07% -5.4% 518.45 ms +4.32% -3.55% 0.7%
firstPaint 294.31 ms +16.29% -19.81% 266.81 ms +6.97% -11.08% 10.31%
domContentLoaded 1227.8 ms +0.3% -0.18% 1246.09 ms +0.3% -0.9% -1.47%
loaded 1228.86 ms +0.3% -0.16% 1247.42 ms +0.31% -0.91% -1.49%
firstContentfulPaint 498.88 ms +0.2% -1.19% 495.48 ms +0.99% -1.32% 0.69%
firstBlock 4646.1 ms +0.6% -0.87% 4693.6 ms +1.87% -1.4% -1.01%
type 18.69 ms +13.43% -3.32% 20.25 ms +7.11% -7.06% -7.7%
navigate 131.67 ms +10.18% -9.58% 130.1 ms +14.46% -6.41% 1.21%
loadPatterns 1351.45 ms +8.96% -4.09% 1425.15 ms +6.71% -1.9% -5.17%
loadPages 1063.47 ms +1.16% -1.24% 1065 ms +1.33% -1.04% -0.14%
wpTotal 512.29 ms +2.06% -5.43% 508.47 ms +4.49% -3.67% 0.75%
wpMemoryUsage 12.47 MB +0% -0% 12.31 MB +0% -0% 1.28%
wpDbQueries 43 +2.33% -0% 43 +2.33% -0% 0%

7a3749f Run

@oandregal oandregal added [Status] In Progress Tracking issues with work in progress and removed [Package] Editor /packages/editor [Package] Fields /packages/fields labels Sep 22, 2026
@oandregal
oandregal force-pushed the add/fields-api-endpoint branch from 2c93cb7 to 71b76f0 Compare September 23, 2026 14:31
@github-actions github-actions Bot added [Package] Editor /packages/editor [Package] E2E Tests /packages/e2e-tests [Package] Fields /packages/fields labels Sep 23, 2026
@oandregal
oandregal force-pushed the add/fields-api-endpoint branch from ded11f8 to 9597fb8 Compare September 24, 2026 15:02
Comment thread lib/compat/wordpress-7.2/class-gutenberg-fields-registry.php Outdated
Comment on lines +12 to +28
* 1. `add_field_declarative`: a declarative field (`menu_order`), plain data,
* no JavaScript.
* 2. `add_field_with_script_module`: a field backed by a script module and a
* stylesheet (`reading_time`), its value and render coming from
* `fields-api/reading-time.js`. The same module exports a complete
* `word_count` field no registration names: the module augments the fields
* it was registered with, so the entry is ignored and the field is not
* registered.
* 3. `update_field`: a patch of a default field (`comment_status`), which
* becomes sortable and hideable, and gets a render from
* `fields-api/comment-status.js`, keeping the rest of its definition.
* 4. `replace_field`: a substitute for a default field (`author`),
* unregistered and registered again as a plain integer with a new label.
* 5. `add_field_with_data`: a field whose value is data the plugin adds to
* the Pages REST endpoint (`subtitle`), a REST field backed by post meta,
* readable in the list and writable from the Quick Edit form, which the
* function adds the field to.

@oandregal oandregal Sep 24, 2026 •

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 plugin is highlighting how to:

  1. Register a declarative field (only PHP).
  2. Register a field with javascript parts.
  3. Update an existing field.
  4. Replace an existing field (unregister + register).
  5. Register a new field and the corresponding data to the REST API.

* the JavaScript parts of the fields, if any.
* @return bool Whether the fields were registered. False when an argument is invalid.
*/
public function register( $kind, $name, $fields, $script_module = null ) {

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.

I've run this by @gigitux and he brought up an interesting thought: would it beneficial to be able to identify where a field (or script module) comes from? Like adding a source/namespace of sorts at some level.

We don't need it to support all (current use cases). The only use case we could come up with was this:

  • A site has two plugins, my/plugin-1, and my/plugin-2 that override the same field (let’s say product-categories). Each plugin does other things that are useful to the builder (the person/agency who sets up the site for the customer).
  • As it stands today the plugin with the latest hook priority wins (let’s say it’s my/plugin-2).

The question is: is there a way for the builder of the site to have both plugins installed, but pick a field from the plugin with the less priority (my/plugin-1)? This is an operation that only the builder can do (pick from a selection), the plugin authors can’t (because they only know about themselves and core).

We discussed some alternatives but none was satisfactory, hence the lack of source/namespace.

@oandregal oandregal Sep 28, 2026 •

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.

I've pushed a change so that registering fields requires a namespace, register( $namespace, ... ). Additionally, there's an update( $namespace, ... ) method as well.

This means that registering the same field twice is a frown-upon operation: any two plugins can’t accidentally register the same field id. However, a field can still be updated (any number of times by anyone).

This doesn't solve the use case Luigi brought up, but, after reconsidering it and running it by other folks, it doesn't seem something we actually want to support. What introducing a namespace for the field collection allows us to do is to determine who registered and updated any given field. This metadata is exposed via a new origin prop in the endpoint for fields (but it's unused so far).

@oandregal

oandregal commented Sep 24, 2026 •

Copy link
Copy Markdown
Member Author

In this PR, I'm mostly interested in getting the PHP API right. We can iterate in parallel on client-side changes, porting all fields, all screens, etc.

It's at 3k lines of code but most of it is PHP verbosity and tests.

Comment thread lib/compat/wordpress-7.2/fields-api.php Outdated
Comment thread packages/editor/src/dataviews/store/private-actions.ts Outdated
Comment thread packages/fields/package.json Outdated

@dmallory42 dmallory42 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@oandregal, I reviewed and tested this and had a couple of possible issues to feedback on. I know this PR focuses on the PHP API, so these could be follow-ups in the client work.

Module ordering

I know the override/namespacing rules are still being discussed. Whatever we choose, could we separate loading modules from applying their definitions?

Currently, whichever module finishes loading last wins. We could load them in parallel, then apply them in a deterministic order. For example, “later registration wins” should give the same result at any download speed.

Relevant code

Loading failures

A failed fields request (e.g. because of a server error or network loading interruption) becomes an empty array, so fields disappear without a visible error. I reproduced this in Firefox with your Fields API test plugin active:

  1. Open the Site Editor’s Pages screen with at least one published page.
  2. Switch to Table layout and open View options. Confirm Comments and Written by are available.
  3. Open the browser’s Network panel and reload the page.
  4. Filter requests by wp/v2/fields, right-click the matching request and select Block URL (or the Chrome equivalent).
  5. Keep DevTools open, reload, and open View options again.

My concern here is that if the request to load field definitions fails, the page still opens, but some fields are missing and there’s no message explaining why.

To the user, it looks like those fields are no longer available, rather than that part of the page failed to load.

From the API POV, the main thing is the error gets propogated. The JS loader currently coerces it to [], IMO it should instead return an error response (this also distinguishes from a successful-but-empty response). It would be up to each screen to decide how best to handle that error.

Relevant code

Overall, I think this work is looking good though! The registration model is clear. Registering fields, updating definitions and replacing one worked well upon testing. So any issues with the client handler could probably be tackled outside of this PR. Nice work!

@ntsekouras ntsekouras left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I did a first pass here and continue on Monday. Good work!

Comment thread lib/compat/wordpress-7.2/class-gutenberg-fields-registry.php Outdated
Comment thread lib/compat/wordpress-7.2/class-gutenberg-fields-registry.php Outdated
Comment thread lib/compat/wordpress-7.2/class-gutenberg-fields-registry.php Outdated
Comment thread lib/compat/wordpress-7.2/fields-api.php Outdated
Comment thread lib/compat/wordpress-7.2/fields-api.php Outdated
Comment thread lib/compat/wordpress-7.2/fields-api.php Outdated
Comment thread packages/e2e-tests/plugins/fields-api/reading-time.css
@ntsekouras
ntsekouras force-pushed the add/fields-api-endpoint branch from 2b42694 to 803ca42 Compare October 7, 2026 09:27
@ntsekouras

ntsekouras commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Small update:

The MediaEdit move (#84046) and the PHP part (#84078) are now merged in trunk. I've rebased this branch on top.

There's a small follow-up on how core and the plugin share the registry (extend core's class or duck type the check, see #83368 (comment)), it'd be good to have @oandregal's take there too.

And this thread about the wp.* globals the field modules rely on (#83368 (comment)) could use @jorgefilipecosta's feedback, to see how we follow up on it.

@mcsf

mcsf commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Thanks for the update!

There's a small follow-up on how core and the plugin share the registry (extend core's class or duck type the check, see #83368 (comment)), it'd be good to have @oandregal's take there too.

Commented. I definitely think the surface should be amended while the dough is soft.

@ntsekouras

ntsekouras commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

#83368 (comment). I definitely think the surface should be amended while the dough is soft.

Follow up PR for using the same registry instance here.

@mcsf

mcsf commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

I'm sorry to drop this as another drive-by comment (though at least, unlike Pascal's old quote, I didn't have time and I did make it short!):

Regarding preloading:

  • Is parsing $_GET['p'] the best we have to determine the editing context (post types, path)?
    • Seems very fragile. It tangles client-side (and Gutenberg-borne) routing decisions with server-side loading. The client-side code is likely to drift without anyone noticing regressions in this server-side logic.
    • I see that we do already handle GET parameters in wp-admin/site-editor.php, but the logic is simpler (e.g. look for a specific match of /page/\d+)
  • Honest question regarding _gutenberg_preload_route_entity_fields: does the Site Editor v2 not honour block_editor_rest_api_preload_paths? Why not?
    • Given that we already have WP_Block_Editor_Context, could it absorb some of this? Or, if we're not ready to make that sort of commitment yet, how else can we "establish a contract" more visibly between client-side routing and server-side preloading?

Side note: annoyingly, WP_Block_Editor_Context is defined as final class, so we can't just extend it in Gutenberg. :/

ntsekouras and others added 4 commits October 9, 2026 13:06
Brings in #84168, the single registry that takes over core's singleton like the icons API, and adapts the branch to it: the core-fields loader and the e2e plugin register through wp_register_fields(), wp_update_fields(), wp_unregister_fields() and wp_register_field_collection( $directory ); the callbacks of `wp_fields_api_init` take no argument; the Fields API functions keep their core names in the build, so the two `prefixFunctions` entries of core-fields go away. The fields API tests keep the branch's core collections tests on the new API, and the changelogs keep both sides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot removed the [Package] Fields /packages/fields label Oct 9, 2026
*/
export const fieldExtensions: FieldsScriptParts< ItemWithLinks >[ string ] = {
// Only the users who can make the post sticky see the control.
isVisible: ( item ) => hasActionLink( item, 'wp:action-sticky' ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the user has no sticky post permissions should we not even register the field?

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.

Reviewed some other fields and at least these other fields are also affected:

  • author
  • date
  • password

@jorgefilipecosta

Copy link
Copy Markdown
Member

We are duplicating the fields across from @wordress/fields code, what is the longterm plan are the fields going to be on the two packages ore they are going to be deleted from @wordress/fields ?

@oandregal

Copy link
Copy Markdown
Member Author

We are duplicating the fields across from @wordress/fields code, what is the longterm plan are the fields going to be on the two packages ore they are going to be deleted from @wordress/fields ?

The idea is to define fields in a single place.

Initially, I wanted to deprecate the @wordpress/fields package entirely (as well as the @wordpress/media-fields one).

Alternatively, we could just remove fields from @wordpress/fields (and still deprecate @wordpress/media-fields). I've started looking into this approach this afternoon, as per this feedback, which points in the same direction. The idea would be 1) removing the fields from @wordpress/fields and 2) export the useFields / loadFields utils from @wordpress/fields instead of creating a new @wordpress/fields-loader package. The tricky part here is timing and make reverts easy (in case we need to do them during beta).

So, the interim state is going to look like there are a few places defining the fields, but the idea is the there's only one.

@jorgefilipecosta

Copy link
Copy Markdown
Member

Hi @oandregal, I think the script modules of @wordpress/core-fields will hit a translation problem once this lands. They only load as module_dependencies of wp-editor, through _wp_add_field_modules_to_editor_script(), and WordPress doesn't print the translations of script modules that only a classic script imports. In the plugin, two more things block them: the modules served from build/ look up the wrong language pack file names, and the full-page template of wp-build never prints module translations.

#84651 (core part: WordPress/wordpress-develop#14260) fixes the three, with a PHPUnit test for field modules. The labels from field.php are fine since they arrive translated through REST; it's the strings in the modules' JS ("Copied Permalink to clipboard.", "Comments open", "%1$s, %2$s read time.", …) that would show in English.

@Mamaduka

Copy link
Copy Markdown
Member

This and related core PR are still listed for backporting in WP 7.2. What's the status of this feature? I remember some of the parts were extracted into smaller PRs; do we plan to do the same for backports?

@oandregal

Copy link
Copy Markdown
Member Author

This and related core PR are still listed for backporting in WP 7.2. What's the status of this feature? I remember some of the parts were extracted into smaller PRs; do we plan to do the same for backports?

I just gave an update of the current plans. Jorge and I are pairing to get this PR (extracted in smaller parts) and the actions PRs into core.

My next steps for fields are:

Jorge has similar PRs for actions:

@oandregal

oandregal commented Oct 15, 2026 •

Copy link
Copy Markdown
Member Author

After the past days of work we have landed the PHP fields API in trunk. It works for the screens that used usePostFields: site editor v1 (pages, templates, patterns, parts) as well as some experiments (DataForm Post Inspector, etc.). Client-side we haven't changed anything: those screens still use the existing private usePostFields hook that, under the hood, uses the useFields hook (bundled in the editor package for now). See #84700

This is less ambitious than what I pushed before: all screens that use DataViews/DataForm (stable, experimental, plus migrate some that were not using usePostFields) add also adding stable client site utils (useFields, loadFields). It's a compromise that allows us to make progress and take the time to make the right call about the client APIs, which didn't have as clear a path as initially understood. See #84323

I'll continue extracting parts of this PR that make sense to land and will update/close this in the following days.

This branch has not been deployed

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

Labels

[Package] Core data /packages/core-data [Package] E2E Tests /packages/e2e-tests [Package] Edit Post /packages/edit-post [Package] Edit Site /packages/edit-site [Package] Editor /packages/editor [Package] Private APIs /packages/private-apis [Status] In Progress Tracking issues with work in progress [Tool] Dependency Extraction Webpack Plugin /packages/dependency-extraction-webpack-plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants