Repository navigation
Fields API - #83368
Fields API#83368oandregal wants to merge 188 commits into
Conversation
🤖 PR meta 🤖🏷️ LabelsThis pull request needs exactly one label indicating its type, and has 0.
Read more about Type labels in Gutenberg. If you cannot add labels yourself, a reviewer can do it for you. 📦 Bundle sizeSize Change: +138 kB (+1.65%) Total Size: 8.52 MB 📦 View Changed
⚡ PerformanceShow the resultsClient side metrics exclude the server response time. front-end-block-theme
front-end-classic-theme
media-processing
media-upload
post-editor
site-editor
|
2c93cb7 to
71b76f0
Compare
ded11f8 to
9597fb8
Compare
| * 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. |
There was a problem hiding this comment.
This plugin is highlighting how to:
- Register a declarative field (only PHP).
- Register a field with javascript parts.
- Update an existing field.
- Replace an existing field (unregister + register).
- 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 ) { |
There was a problem hiding this comment.
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, andmy/plugin-2that override the same field (let’s sayproduct-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.
There was a problem hiding this comment.
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).
|
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. |
dmallory42
left a comment
There was a problem hiding this comment.
@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.
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:
- Open the Site Editor’s Pages screen with at least one published page.
- Switch to Table layout and open View options. Confirm Comments and Written by are available.
- Open the browser’s Network panel and reload the page.
- Filter requests by wp/v2/fields, right-click the matching request and select Block URL (or the Chrome equivalent).
- 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.
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
left a comment
There was a problem hiding this comment.
I did a first pass here and continue on Monday. Good work!
2b42694 to
803ca42
Compare
|
Small update: The 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. |
|
Thanks for the update!
Commented. I definitely think the surface should be amended while the dough is soft. |
Follow up PR for using the same registry instance here. |
|
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:
Side note: annoyingly, |
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>
| */ | ||
| 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' ), |
There was a problem hiding this comment.
If the user has no sticky post permissions should we not even register the field?
There was a problem hiding this comment.
Reviewed some other fields and at least these other fields are also affected:
- author
- date
- password
|
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 Alternatively, we could just remove fields from 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. |
|
Hi @oandregal, I think the script modules of #84651 (core part: WordPress/wordpress-develop#14260) fixes the three, with a PHPUnit test for field modules. The labels from |
|
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: |
|
After the past days of work we have landed the PHP fields API in 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 ( I'll continue extracting parts of this PR that make sense to land and will update/close this in the following days. |
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_initaction to register their fields. Two examples ship in this PR:field.phpfiles:Why?
Part of #74865
How?
Server:
fields_api_inithook, the registry, and the APIkind,name) and its script module. It exposesregister(),update()andunregister().fields_api_initaction 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 afterinitbut before the REST route/script modules are requested.wp_register_field_collection(), the declarative format: a folder withindex.phpand one subfolder per field whosefield.phpreturns the serializable part of that field. See core-fields/src/page/field.php for an example.Transport:
/wp/v2/fieldsand preloadingGET /wp/v2/fields?kind=…&name=…, returning the entity'sfieldsplus thescript_modulesthat provide their JavaScript parts.Client:
core-data→fields-loader→ script modules@wordpress/core-datagains a publicgetFieldsConfig( kind, name )selector (plus resolver, reducer and theFieldsConfigtype) that requests and caches the route.@wordpress/fields-loader, a new package, exposesuseFields()for components andloadFields()for route loaders and other code outside React. They merge each script module's default export into the fields that came from REST._gutenberg_add_field_modules_to_editor_script()runs onadmin_footer, reads the registry, and declares every registered module as a dynamicmodule_dependenciesentry of thewp-editorscript. Dynamic means the module is listed in the import map but only fetched when the loader imports it.Core's fields:
@wordpress/core-fieldsattachment,page,wp_template,wp_template_part,wp_block.post_type_supportsfolder) 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 thefields_api_post_type_supports_exclusionsfilter (example), or just unregister it infields_api_init.Screens
usePostFieldsto the newloadFields()/useFields()utils.core-fields. This table lists all the exceptions so far:core-fields)core-fields)useReviewRowsunsavedandparentrows the server knows nothing aboutcore-fieldsdepends onmedia-utils, so reading the server'sattachmentfields here is a genuine circular dependency; the modal's list also diverges from the registered one (the thumbnail cannot be hidden,titlefalls back to(no title), andattachedTofilters on the modal's post context)core-fields)Previewfrom@wordpress/lazy-editor, which thecore-fieldsscript modules cannot pull inPreviewfrom@wordpress/lazy-editor, which thecore-fieldsscript modules cannot pull inPreviewfrom@wordpress/lazy-editor, which thecore-fieldsscript modules cannot pull inwp_blockrecords: use-patterns merges theme patterns, which are not posts, with user patterns and synthesizescategoriesas slugs; the field'selementscome from the two client-side category sourcesWhat changes for consumers
wp.editor.registerEntityFieldandwp.editor.unregisterEntityFieldare deprecated and have no effect. Fields need to be registered in PHP onfields_api_initinstead.usePostFieldshook is gone, and thecore/editorstore no longer holds fields: its privateregisterEntityField,unregisterEntityFieldandgetEntityFieldsare gone, andregisterPostTypeSchemanow registers only actions.post-formats. Before, it also checked thedisablePostFormatseditor setting, which only the post editor screen set — so it is now hidden consistently in the site editor for themes without post formats.content-previewfield is no longer registered for post types supportingeditor. 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.
lib/compat/wordpress-7.1/class-gutenberg-rest-view-config-controller-7-1.phplib/compat/wordpress-7.2/class-gutenberg-fields-registry.php\lib/compat/wordpress-7.2/class-gutenberg-rest-fields-controller-7-2.phplib/compat/wordpress-7.2/fields-api.phplib/compat/wordpress-7.2/preload.phplib/compat/wordpress-7.2/rest-api.phplib/load.phpphpunitbutphpunit/view-config-api-test.php.MediaEditto media-utils as private API:packages/media-utils.MediaEditfrom@wordpress/fields(optional, see thread)tools/eslint/suppressions.json.core-fieldspackage:packages/private-apis/.packages/core-fields/.wp.*..github/labeler.yml.packages/core-data.packages/fields-loader.wp.*..github/labeler.yml.tools/validation/check-private-apis.config.jsonpackages/dependency-extraction-webpack-plugin/.docs/private-apis.mdStage 2: switch consumers.
It can be done one-by-one.
packages/e2e-tests/plugins/fields-api/.test/e2e/specs/site-editor/fields-api.spec.jspackages/edit-site/butpackages/edit-site/src/components/sidebar-identity/index.jsxpackages/media-editor.routes/media-editor.lib/compat/wordpress-7.2/view-config-api.phpphpunit/view-config-api-test.phppackages/edit-site/src/components/sidebar-identity/index.jsxpackages/editor/src/components/media/media-editor-modal.jsxroutes/identity/routes/navigation-list/.routes/pattern-list/.routes/post-list/.routes/template-list/.routes/template-part-list/.packages/edit-post/src/index.jsxdocs/contributors/code/deprecations.md.packages/editor/.MediaEditfrom@wordpress/fields. (thread)@wordpress/fieldsand@wordpress/media-fields. To be decided: lint rule, docs?Stage 3: after WordPress 7.2 release.
@wordpress/fieldsand@wordpress/media-fields.Testing Instructions
Visit the existing screens and verify they still work as expected:
Use of AI Tools
Used Claude to analyze and create parts of this pull request. The author reviewed all the changes.