Skip to content

View Config: Add versioning handling#79809

Open
ntsekouras wants to merge 4 commits into
trunkfrom
view-config-versioning-handling
Open

View Config: Add versioning handling#79809
ntsekouras wants to merge 4 commits into
trunkfrom
view-config-versioning-handling

Conversation

@ntsekouras

Copy link
Copy Markdown
Contributor

What?

Part of: #76544

This PR adds versioning to the server-side view configuration API (gutenberg_get_entity_view_config() and the dynamic get_entity_view_config_{$kind}_{$name} filter). Until now callbacks received and returned a raw config array.

Now callbacks receive a Gutenberg_View_Config_Data container and contribute through its methods — set() for an entity's base definition, update_with( $patch, $version ) for versioned, identity-aware patches, and remove_view_list_items() / remove_fields() for removals — instead of mutating the array directly.

Why?

Versioned from the start

update_with( $patch, $version ) requires the schema version the patch was authored against, even though there is only version 1 today and no migrations exist.

This is because without the container, callbacks would customize by walking the raw payload (find the right index, foreach, unset), and imperative array manipulation can never be migrated. A later shape change could break existing callbacks in the wild. The explicit $version argument keeps each patch unambiguous about the shape it assumed. This mirrors theme.json, where documents declare a version and WP_Theme_JSON_Data->update_with() migrates older data forward.

update_with() — describe intent, not array surgery

update_with( $patch, $version ) is the main contribution verb: it merges a partial configuration onto the current one, so a patch supplies only what it changes. Object-shaped keys (default_view, default_layouts) merge recursively; the identity-keyed lists merge by identity — view_list entries by slug, form fields by id — so a contribution names the member it targets and core walks its own current structure to apply it: a matching member merges in place, an unknown one appends. A null patch value unsets a nested key, and null for a whole top-level key resets it to its default.

The remove_* helpers

This applies to the identity-keyed lists — view_list entries (keyed by slug) and form fields (keyed by id). For them, removal can't be expressed as a merge: a patch only adds or modifies identity-matched members, and null never deletes list content (unlike map keys, where null unsets). So removal gets its own verbs: remove_view_list_items( $slugs ) and remove_fields( $ids ) name identities and core drops them from its own current structure — including fields nested inside a group's children — keeping removals version-safe and independent of ordering.

set() — base definitions, not customizations

set( $key, $value ) replaces a whole top-level key. Because it replaces rather than merges, a contribution made with it stops inheriting core's future changes to that key — a freeze — so it should not be the default choice. It exists for authoritative base definitions — e.g. a CPT that doesn't want the default post form at all and provides its own from scratch:

function example_filter_book_view_config( $data ) {
    // The default post form doesn't fit books; define one from scratch.
    return $data->set(
        'form',
        array(
            'layout' => array( 'type' => 'panel' ),
            'fields' => array( 'isbn', 'publisher' ),
        )
    );
}
add_filter( 'get_entity_view_config_postType_book', 'example_filter_book_view_config' );

Examples

Noting that the below examples are called for pages (get_entity_view_config_postType_page). You can test different post types with the respective filter.

function example_filter_page_view_config( $data ) {
    // Merge a partial configuration: add a saved view to the list, and
    // retitle the existing Drafts view — matched by slug, only the given
    // keys change.
    $data->update_with(
        array(
            'view_list' => array(
                array(
                    'title' => __( 'My drafts', 'example' ),
                    'slug'  => 'my-drafts',
                    'view'  => array(
                        'filters' => array(
                            array(
                                'field'    => 'status',
                                'operator' => 'isAny',
                                'value'    => 'draft',
                                'isLocked' => true,
                            ),
                        ),
                    ),
                ),
                array(
                    'slug'  => 'drafts',
                    'title' => __( 'In progress', 'example' ),
                ),
            ),
        ),
        1
    );

    // Unset a nested value with null: drop the grid layout option.
    $data->update_with(
        array( 'default_layouts' => array( 'grid' => null ) ),
        1
    );

    // Remove a view from the list by its slug.
    $data->remove_view_list_items( 'trash' );

    // Remove some fields from the form by their identity.
    $data->remove_fields( array( 'slug', 'author' ) );

    return $data;
}
add_filter( 'get_entity_view_config_postType_page', 'example_filter_page_view_config' );

Testing Instructions

  1. Add the example callback above in PHP.
  2. Open Site Editor → Pages:
    • The "My drafts" view appears in the views list; the Drafts view is retitled "In progress"; the Trash view is gone.
    • The Grid layout option is no longer offered.
  3. Open Quick Edit on a page: the Slug and Author fields are gone from the form.
  4. Play around with different payloads

Use of AI Tools

Opus 4.8 and Fable 5 with direction, changes and review

@ntsekouras ntsekouras self-assigned this Jul 2, 2026
@ntsekouras ntsekouras added [Type] Enhancement A suggestion for improvement. [Feature] DataViews Work surrounding upgrading and evolving views in the site editor and beyond labels Jul 2, 2026
Comment on lines +82 to +136
## Server-side view configuration filter

DataViews-powered screens (such as the Pages list and its Quick Edit form) build their configuration on the server. A dynamic filter, `get_entity_view_config_{$kind}_{$name}`, lets you customize that configuration for a specific entity, where the dynamic portions are the entity kind (e.g. `postType`) and name (e.g. `page`).

The configuration has four keys: `default_view`, `default_layouts`, `view_list` (the saved views shown in the list), and `form` (the DataForm used by consumers like Quick Edit).

In the following example, a custom saved view is added to the `page` list and the Trash view is removed from it, the `grid` layout option is unset, and `slug` and `author` fields are removed from the form.

```php
function example_filter_page_view_config( $data ) {
// Merge a partial configuration: add a saved view to the list.
$data->update_with(
array(
'view_list' => array(
array(
'title' => __( 'My drafts', 'example' ),
'slug' => 'my-drafts',
'view' => array(
'filters' => array(
array(
'field' => 'status',
'operator' => 'isAny',
'value' => 'draft',
'isLocked' => true,
),
),
),
),
),
),
1
);

// Unset a nested value with null: drop the grid layout option.
$data->update_with(
array( 'default_layouts' => array( 'grid' => null ) ),
1
);

// Remove a view from the list by its slug.
$data->remove_view_list_items( 'trash' );

// Remove some fields from the form by their identity.
$data->remove_fields( array( 'slug', 'author' ) );

return $data;
}
add_filter( 'get_entity_view_config_postType_page', 'example_filter_page_view_config' );
```

The filter receives an object holding the entity's view configuration. Contribute through its methods and return it:

- `update_with( $patch, $version )` merges a partial configuration. Collection members are matched by identity — views by `slug` and form fields by `id` — so a matching member is merged in place and an unknown one is appended to the end. A value of `null` deletes that key — the way to unset a nested value such as a `default_layouts` entry, a nested layout property, or a field's `label` — and passing `null` for a whole top-level key resets it to its default. Null never deletes list content: use the remove helpers below for that. A schema version is required so the contribution can be migrated forward if the configuration shape changes.
- `remove_view_list_items( $slugs )` drops one or more `view_list` entries by `slug`.
- `remove_fields( $ids )` drops one or more `form` fields by `id`, including fields nested inside a group. Both remove helpers accept a single identity or an array.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the added part. The rest is auto-format.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@Mamaduka

Mamaduka commented Jul 2, 2026

Copy link
Copy Markdown
Member

Some surface notes, it's been ages since I've worked on the PHP-side APIs 😅

  • Do we want a more general filter that can change multiple configs? Usually filters like get_entity_view_config_{$kind}_{$name} come with something like get_entity_view_config in Core.
  • The get_entity_view_config_postType_post doesn't match Core filter naming conventions. Will that be okay?
  • The ::update_with method name is okay, but it requires reading the PHPDoc to understand what it really does. Do we have better alternatives?
  • Working with private methods can be hard in the future. It's better to have a good plan for how the Gutenberg plugin can patch those when needed.

cc @aaronjorbin, @peterwilsoncc

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Flaky tests detected in e65c604.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/28583296410
📝 Reported issues:

@ntsekouras

Copy link
Copy Markdown
Contributor Author

Do we want a more general filter that can change multiple configs? Usually filters like get_entity_view_config_{$kind}_{$name} come with something like get_entity_view_config in Core.

I'm assuming you mean a single filter to update all entities for my reply. TBH I don't think so - at least from the start.. Even current core usages have quite a few differences and I'm not sure how updating something for everything makes sense.

Working with private methods can be hard in the future. It's better to have a good plan for how the Gutenberg plugin can patch those when needed.

I think that this class will stay forever in GB to always override like WP_Theme_JSON_Data_Gutenberg.

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

Labels

[Feature] DataViews Work surrounding upgrading and evolving views in the site editor and beyond [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants