Skip to content

1311 add edit with yoast bulk editor entry to wp admin content type bulk actions dropdown - #23488

Open
pls78 wants to merge 34 commits into
feature/bulk-editor-ph2from
1311-add-edit-with-yoast-bulk-editor-entry-to-wp-admin-content-type-bulk-actions-dropdown
Open

1311 add edit with yoast bulk editor entry to wp admin content type bulk actions dropdown#23488
pls78 wants to merge 34 commits into
feature/bulk-editor-ph2from
1311-add-edit-with-yoast-bulk-editor-entry-to-wp-admin-content-type-bulk-actions-dropdown

Conversation

@pls78

@pls78 pls78 commented Jul 22, 2026

Copy link
Copy Markdown
Member

Context

  • We want to add a "Bulk edit" function in WordPress overview pages (for supported post types only) so that a user can select up to 20 posts and edit them in the Yoast Bulk editor.

Summary

This PR can be summarized in the following changelog entry:

  • Adds a Bulk edit feature to overview pages related to each supported post type.

Relevant technical choices:

  • The bulk action is registered on admin_init for each supported content type via the bulk_actions-edit-{post_type} / handle_bulk_actions-edit-{post_type} filters, guarded by the wpseo_manage_options capability.
  • The ↗ arrow is appended outside the translatable string and carries U+FE0E (text-presentation selector) so wp-admin's twemoji doesn't replace it with an <img>, which is invalid inside <select>.
  • The handler returns the redirect URL instead of the issue's wp_safe_redirect() + exit: handle_bulk_actions-* is a filter and core performs the redirect itself — this follows the API contract and keeps the handler unit-testable.
  • Only the first 20 selected IDs (the bulk-editor batch limit) are carried in the URL, plus a selected_count param that drives the ">20" notice. Using selected_count allows to keep the redirect URL bounded because the overview can list up to 999 rows per page.
  • The action is hidden on the Trash view via a new Current_Page_Helper::is_trash_overview(), which mirrors core's internal WP_Posts_List_Table.
  • post_ids and selected_count are registered as removable_query_args so the address bar is scrubbed after load.
  • An auto-activated "Overview selection" filter was added: without it, carried posts that don't sit on page 1 of the bulk-editor list would be invisible.
  • The carried-over selection is pruned client-side against the first filtered fetch (listed + editable rows) rather than validated server-side, since that response is already the authoritative subset — avoiding duplicated collector logic.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR can be acceptance tested by following these steps:

Setup
  • Use a site with at least 25 published posts and a handful of pages.
  • Log in as an Administrator.
  • On Posts → All Posts, open Screen Options (top right) and set Number of items per page to 30, so you can select more than 20 posts on one page.
The dropdown entry
  • Go to Posts → All Posts and open the Bulk actions dropdown.
  • Confirm it contains a "Yoast SEO" group after the default actions (Edit, Move to Trash), with a single entry "Bulk edit ↗".
  • Confirm the arrow is a plain text glyph, not an emoji image (it should look like the surrounding text, not a colored icon).
  • Confirm the same entry appears in the bottom bulk-actions dropdown of the list table.
  • Open the Trash view (Posts → All Posts → Trash): the "Yoast SEO" group must not be there.
Carry a small selection over (happy path)
  • On Posts → All Posts, tick 3 posts, pick Bulk edit and click Apply.
  • You land on the Yoast bulk editor with the Posts tab active.
  • The list shows exactly those 3 posts, all pre-checked, and the selection toolbar reads 3 selected.
  • Open the Filters popover: an "Overview selection" checkbox is ticked, above the status filters, and it is counted in the filter badge.
  • No notice about "first 20" is shown (the selection fits).
  • Good to know: the bulk editor sorts by ID (newest first) while the overview sorts by date, so the order of the rows may differ from the overview — that is expected.
URL cleanup (one-shot parameters)
  • Right after landing on the bulk editor via the bulk action, look at the address bar: the post_ids and selected_count parameters are removed automatically shortly after the page loads (content_type stays).
  • Refresh the page: you get a clean bulk editor — full list, no "Overview selection" filter, nothing pre-checked. A bookmark of this page behaves the same.
The "Overview selection" filter
  • Carry 3 posts over again. In the Filters popover, untick "Overview selection": the full post list returns and the selection is cleared.
  • Tick it back on: the list narrows to the carried posts again, but the selection stays cleared (re-ticking restores the filter, not the selection).
  • Carry 3 posts over again, then switch to the Pages tab and back: the filter and the carried selection are discarded.
More than 20 selected: the truncation notice
  • On Posts → All Posts (30 per page), tick 25 posts, pick Bulk edit, click Apply.
  • In the bulk editor, only the first 20 arrive: 20 rows listed and pre-checked, "20 selected".
  • An info notice renders inside the table header, between the selection toolbar row and the "Generate SEO titles"/"Generate meta descriptions" row: "Only the first 20 posts from your selection were carried over. The bulk editor supports up to 20 posts at a time." (check the FIgma design here)
  • The noun matches the content type (carry >20 pages over and it reads "pages").
  • Dismiss the notice (X): it disappears, and if you have no selection and no unsaved edits the row it occupied collapses.
  • Repeat with exactly 20 selected: no notice.
The exclusion notice (carried posts the bulk editor cannot show)
  • Edit one post and set its visibility to Private (or give it a password). Make sure you have at least 2 normal published posts as well.
  • On Posts → All Posts, tick the private post plus 2 published posts, pick Bulk edit in the bulk-actions dropdown and click Apply.
  • In the bulk editor, only the 2 published posts are listed and pre-checked, and the count reads "2 of 2 posts selected" — the private post was dropped from the selection.
  • An info notice explains why, rendered between the selection toolbar row and the "Generate SEO titles"/"Generate meta descriptions" row: "Your selection has been updated. Private, password-protected, or non-indexed posts can't be bulk edited and were excluded."
  • The noun follows the content type: repeat the steps on Pages → All Pages with a private page and the notice reads "… non-indexed pages can't be bulk edited …".
  • Click the notice's X: it disappears. If nothing else occupies that row (no selection with AI enabled, no unsaved edits), the row collapses.
  • Counter-check: carry over only published posts (no private/password ones) — the notice must not appear.
Both notices at the same time (>20 selected AND some excluded)
  • On Posts → All Posts, set Number of items per page to 30 via Screen Options, and make sure a private post sits among the newest 20 posts (e.g. create it last).
  • Tick 25 posts including the private one, pick Bulk edit and click Apply.
  • In the bulk editor, two stacked notices appear between the selection toolbar and the Generate row, separated by a small gap:
    1. First: "Only the first 20 posts from your selection were carried over. The bulk editor supports up to 20 posts at a time."
    2. Below it: "Your selection has been updated. Private, password-protected, or non-indexed posts can't be bulk edited and were excluded."
  • The selected count reads "19 of 19 posts selected" — 20 were carried, 1 (the private post) was excluded.
  • Dismiss the exclusion notice (bottom): only it disappears; the "first 20" notice stays and the row stays open.
  • Dismiss the remaining notice too: with nothing else occupying the row, it collapses.
Posts the bulk editor cannot show are dropped from the selection
  • Edit one post and set its visibility to Private (or give it a password).
  • On the overview, select that private post plus 2 published posts and apply Bulk edit.
  • The bulk editor lists only the 2 published posts, and the count reads 2 selected — the private post is dropped from the selection entirely, so it cannot be sent to the bulk actions (e.g. AI generation) invisibly.
Permissions
  • Log in as an Editor (no wpseo_manage_options capability).
  • On Posts → All Posts, open the bulk-actions dropdown: the "Yoast SEO" group is absent.
Other content types
  • Repeat the happy path on Pages → All Pages: the entry is there, and applying it lands on the bulk editor with the Pages tab active and the carried pages pre-checked.
  • With WooCommerce active, check the Products overview the same way.
  • A post type that the bulk editor does not support must not get the entry on its overview.
Hand-crafted URLs (defensive handling)
  • Open the bulk editor directly with an invalid content type, e.g. wp-admin/admin.php?page=wpseo_page_bulk_edit&content_type=bogus&post_ids=1,2,3&selected_count=3.
  • The page loads normally on the default content type with no carried selection, no filter, no notice, and no console errors.
  • Try post_ids=abc,-5,0: same clean result.
(Optional) RTL
  • Set the admin language to an RTL one (e.g. Hebrew) in your user profile.
  • The dropdown entry shows a mirrored arrow (↖), rendered at the reading end of the label.
Confirm the bulk editor still works when opened directly (no regression)
  • Open the bulk editor from the Yoast SEO menu (not via the bulk action): full list, no "Overview selection" filter in the popover, nothing pre-checked, no notice.
  • Select rows manually, edit and save a field, and switch content types to confirm nothing changed in the normal flow.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

  • N/A

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change. For example, comments in the Relevant technical choices, comments in the code, documentation on Confluence / shared Google Drive / Yoast developer portal, or other.

Quality assurance

  • I have tested this code to the best of my abilities.
  • During testing, I had activated all plugins that Yoast SEO provides integrations for.
  • I have added unit tests to verify the code works as intended.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and committed the results, if my PR introduces or edits images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes https://github.com/Yoast/reserved-tasks/issues/1311

@pls78 pls78 added this to the feature/bulk-editor-ph2 milestone Jul 22, 2026
@pls78 pls78 added the changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog label Jul 22, 2026
@coveralls

coveralls commented Jul 22, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 0

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.1%) to 55.747%

Details

  • Coverage increased (+0.1%) from the base build.
  • Patch coverage: 165 of 165 lines across 15 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 73017
Covered Lines: 40605
Line Coverage: 55.61%
Relevant Branches: 18952
Covered Branches: 10665
Branch Coverage: 56.27%
Branches in Coverage %: Yes
Coverage Strength: 41218.17 hits per line

💛 - Coveralls

@pls78
pls78 marked this pull request as ready for review July 23, 2026 14:31
@vraja-pro
vraja-pro requested a review from Copilot July 24, 2026 10:55

Copilot AI 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.

Pull request overview

Adds a Yoast “Bulk edit” bulk action to supported WordPress post-type overview screens, redirecting into the Yoast bulk editor with the selected IDs (capped to the batch limit) carried over and reflected in the bulk editor UI.

Changes:

  • Introduces a new wp-admin list-table bulk action integration that redirects to the bulk editor with a carried selection (and hides the action on Trash view).
  • Extends the bulk editor REST/query pipeline to support an include (ID restriction) and adds client-side “Overview selection” filtering plus truncation/exclusion notices.
  • Adds/updates unit tests across PHP and JS for the new selection-carryover, filtering, and notice behaviors.

Reviewed changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/Unit/Helpers/Current_Page_Helper_Test.php Adds unit coverage for Current_Page_Helper::is_trash_overview().
tests/Unit/Bulk_Editor/User_Interface/Posts_Route/Register_Routes_Test.php Verifies REST route schema includes the new include parameter.
tests/Unit/Bulk_Editor/User_Interface/Posts_Route/Get_Posts_Test.php Adds test coverage ensuring included IDs are deduped and propagated into Posts_Query.
tests/Unit/Bulk_Editor/User_Interface/Posts_Overview_Bulk_Actions_Integration/Register_Hooks_Test.php Tests that the integration registers its admin_init hook.
tests/Unit/Bulk_Editor/User_Interface/Posts_Overview_Bulk_Actions_Integration/Register_Bulk_Actions_Test.php Tests capability gating and per-post-type filter registration.
tests/Unit/Bulk_Editor/User_Interface/Posts_Overview_Bulk_Actions_Integration/Handle_Bulk_Action_Test.php Tests redirect URL building, ID sanitization, and batch truncation behavior.
tests/Unit/Bulk_Editor/User_Interface/Posts_Overview_Bulk_Actions_Integration/Get_Conditionals_Test.php Verifies conditionals configuration for the integration.
tests/Unit/Bulk_Editor/User_Interface/Posts_Overview_Bulk_Actions_Integration/Constructor_Test.php Confirms constructor wiring for dependencies.
tests/Unit/Bulk_Editor/User_Interface/Posts_Overview_Bulk_Actions_Integration/Add_Bulk_Action_Test.php Verifies optgroup/label output, RTL arrow behavior, and Trash suppression.
tests/Unit/Bulk_Editor/User_Interface/Posts_Overview_Bulk_Actions_Integration/Abstract_Posts_Overview_Bulk_Actions_Integration_Test.php Provides shared setup/mocks for the new integration’s test suite.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Register_Hooks_Test.php Updates expectations to include the removable_query_args filter on the bulk editor page.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Get_Initial_Selection_Test.php Adds unit tests for parsing/sanitizing carried-over selection from URL params.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Enqueue_Assets_Test.php Updates localized script data expectations to include initialSelection.
tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Add_Removable_Query_Args_Test.php Adds coverage for registering post_ids/selected_count as removable query args.
tests/Unit/Bulk_Editor/Infrastructure/Posts/Indexable_Posts_Collector/Get_Posts_Test.php Adds coverage for applying an include-ID restriction at the indexables query layer.
tests/Unit/Bulk_Editor/Domain/Posts/Posts_Query_Test.php Adds coverage for new include-IDs API on Posts_Query.
src/helpers/current-page-helper.php Adds is_trash_overview() helper to detect Trash view on admin overviews.
src/bulk-editor/user-interface/posts-route.php Extends REST schema + request handling to accept/dedupe included post IDs.
src/bulk-editor/user-interface/posts-overview-bulk-actions-integration.php New integration adding the “Bulk edit” dropdown entry and redirect handler.
src/bulk-editor/user-interface/bulk-editor-integration.php Adds URL param constants, initial selection parsing, and removable query args hook.
src/bulk-editor/infrastructure/posts/post-meta-posts-collector.php Applies post__in constraint when Posts_Query has include IDs.
src/bulk-editor/infrastructure/posts/indexable-posts-collector.php Applies where_in(object_id, …) constraint when include IDs are present.
src/bulk-editor/domain/posts/posts-query.php Extends Posts_Query to carry include IDs and expose has_include()/get_include_ids().
packages/js/tests/bulk-editor/store/selection.test.js Adds test coverage for carried-over selection totals, pruning, and exclusion notice state.
packages/js/tests/bulk-editor/store/query.test.js Adds test coverage for overview selection filter state and selectors.
packages/js/tests/bulk-editor/services/use-posts.test.js Adds coverage for include requests and selection pruning behavior while overview filter is active.
packages/js/tests/bulk-editor/overview-selection-notice.test.js New tests for the truncation notice and its integration into the bulk-actions band.
packages/js/tests/bulk-editor/overview-exclusion-notice.test.js New tests for the exclusion notice and stacked-notice behavior.
packages/js/tests/bulk-editor/initialize.test.js Adds seeding tests for carried-over preselection state from wpseoBulkEditorData.initialSelection.
packages/js/tests/bulk-editor/bulk-editor-filters.test.js Adds tests for offering/toggling the “Overview selection” filter.
packages/js/tests/bulk-editor/bulk-editor-content.test.js Adds tests for selection-view semantics with off-page IDs + bulk-actions band expansion logic.
packages/js/src/bulk-editor/store/selection.js Extends selection state with carried-over totals, pruning, and notice dismissal actions.
packages/js/src/bulk-editor/store/query.js Adds overview selection IDs + “overview filter active” state and reducers/selectors.
packages/js/src/bulk-editor/services/use-posts.js Adds conditional include querying and selection pruning based on fetched editable rows.
packages/js/src/bulk-editor/initialize.js Seeds the store with carried-over selection and activates the overview filter when applicable.
packages/js/src/bulk-editor/components/overview-selection-notice.js Adds truncation notice component for “> batch size selected” carry-over scenarios.
packages/js/src/bulk-editor/components/overview-exclusion-notice.js Adds exclusion notice component for carried-over IDs dropped during pruning.
packages/js/src/bulk-editor/components/bulk-editor-filters.js Adds “Overview selection” checkbox filter and counts it in the applied-filters badge.
packages/js/src/bulk-editor/components/bulk-editor-content.js Ensures selection semantics work with off-page IDs and expands the band for overview notices.
packages/js/src/bulk-editor/components/bulk-action-bar.js Renders truncation/exclusion notices in the notices region and wires dismissal handlers.

Comment thread src/bulk-editor/user-interface/bulk-editor-integration.php Outdated
Comment thread src/bulk-editor/user-interface/posts-overview-bulk-actions-integration.php Outdated
…rating the "needs improvement group" filters

@vraja-pro vraja-pro 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.

Two non-blocking nits on the PR itself:

Changelog label — the PR is labelled changelog: non-user-facing but adds a visible "Bulk edit ↗" entry to every supported post overview, plus notices and an auto-filter in the bulk editor. This is user-facing and should be changelog: enhancement.

Trailing space in changelog bullet"…each supported post type ." has a space before the period.

Comment thread src/helpers/current-page-helper.php
Comment thread src/bulk-editor/infrastructure/posts/post-meta-posts-collector.php
Comment thread packages/js/src/bulk-editor/store/selection.js
@pls78 pls78 added changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog and removed changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog labels Jul 29, 2026
@github-actions

Copy link
Copy Markdown

A merge conflict has been detected for the proposed code changes in this PR. Please resolve the conflict by either rebasing the PR or merging in changes from the base branch.

pls78 added 3 commits July 29, 2026 11:58
…11-add-edit-with-yoast-bulk-editor-entry-to-wp-admin-content-type-bulk-actions-dropdown

# Conflicts:
#	packages/js/src/bulk-editor/initialize.js
#	packages/js/tests/bulk-editor/initialize.test.js
#	src/bulk-editor/user-interface/bulk-editor-integration.php
#	tests/Unit/Bulk_Editor/User_Interface/Bulk_Editor_Integration/Enqueue_Assets_Test.php
@pls78

pls78 commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

Changelog label — the PR is labelled changelog: non-user-facing but adds a visible "Bulk edit ↗" entry to every supported post overview, plus notices and an auto-filter in the bulk editor. This is user-facing and should be changelog: enhancement.

I think we want it this way because this PR is going to be merged in a feature branch, which will expose the "real" changelog label (presumably enhancement).

@pls78
pls78 requested a review from vraja-pro July 29, 2026 10:38
@vraja-pro vraja-pro added changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog and removed changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog labels Jul 29, 2026
Comment thread packages/js/src/bulk-editor/components/overview-selection-notice.js Outdated
Comment thread packages/js/src/bulk-editor/components/overview-exclusion-notice.js Outdated
Comment thread packages/js/src/bulk-editor/components/overview-selection-notice.js Outdated
Comment thread packages/js/src/bulk-editor/components/overview-exclusion-notice.js Outdated

@vraja-pro vraja-pro 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.

Lets follow the same pattern in https://github.com/Yoast/wordpress-seo-premium/pull/5040 by removing the content type label and remove the space between the notices untill we get a clear answer from UX.

@github-actions

Copy link
Copy Markdown

A merge conflict has been detected for the proposed code changes in this PR. Please resolve the conflict by either rebasing the PR or merging in changes from the base branch.

pls78 added 5 commits July 31, 2026 15:03
…11-add-edit-with-yoast-bulk-editor-entry-to-wp-admin-content-type-bulk-actions-dropdown

# Conflicts:
#	packages/js/src/bulk-editor/components/bulk-editor-content.js
#	packages/js/tests/bulk-editor/bulk-editor-content.test.js
#	packages/js/tests/bulk-editor/hooks/use-posts.test.js
@pls78
pls78 requested a review from vraja-pro July 31, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: non-user-facing Needs to be included in the 'Non-userfacing' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants