Skip to content

Add pagination and server-side filtering to eligible users action and REST Endpoint #11889

@benbowler

Description

@benbowler

Feature Description

The Invite component (implemented in #11857) currently fetches all eligible subscribers in a single request and filters them client-side. This works for small sites but does not scale - a site with hundreds of administrators and shared-role users would transfer a large payload on every panel open and search keystroke would filter in-memory.

This issue moves search and pagination to the server. The existing GET core/site/data/email-reporting-eligible-subscribers endpoint gains optional page and search query parameters and returns a paginated response with metadata (users, total, totalPages). The frontend datastore is updated to pass these parameters through, and the Invite component is refactored to delegate filtering to the server instead of performing it locally.


Do not alter or remove anything below. The following sections will be managed by moderators only.

Acceptance criteria

  • GET /core/site/data/email-reporting-eligible-subscribers accepts optional query params:
    • page (int, default 1) – current page number.
    • per_page (int, default 20, max 100) – results per page.
    • search (string) – filters users by display name or email (case-insensitive partial match).
  • Response includes pagination metadata alongside the user list:
    • users[] – array of eligible user objects.
    • total – total matching users count.
    • totalPages – calculated total pages.
  • Empty search returns all eligible users paginated.
  • Non-matching search returns empty users[] with total: 0.
  • Frontend datastore action/resolver updated to support pagination and search params.
  • The existing Invite component implemented in Create the “Invite others to subscribe” component #11857 should be updated to use this new server-side search/filtering.

Implementation Brief

Backend (PHP)

  • Update file includes/Core/Email_Reporting/Eligible_Subscribers_Query.php:

    • Define a class constant PER_PAGE = 20.
    • Extend get_eligible_users( int $exclude_user_id, array $args = array() ): array to accept an optional $args array with keys page (default 1) and search (default ''). Use PER_PAGE for the fixed page size.
    • Refactor query_admins() and query_shared_roles() to accept and forward search args. Add 'search' => '*' . $search . '*' with 'search_columns' => array( 'display_name', 'user_email' ) to WP_User_Query args when a search term is provided.
    • Continue querying all matching IDs from both sources (admins + shared roles), deduplicating via array keys, then apply pagination (offset calculated from page and PER_PAGE / number = PER_PAGE) to the merged set.
    • Set count_total to true in queries to enable total count retrieval.
    • Add new method get_eligible_users_count( int $exclude_user_id, string $search = '' ): int that returns the total matching user count without pagination (needed for the total and totalPages response fields).
  • Update file includes/Core/Email_Reporting/REST_Email_Reporting_Controller.php:

    • Add query parameter schema to the email-reporting-eligible-subscribers route registration:
      • page: integer, default 1, minimum 1.
      • search: string, default ''.
    • Update the route callback to extract page and search from WP_REST_Request and pass them to get_eligible_users().
    • Change the response from a flat array of user objects to an object with three keys:
      • users: array of eligible user objects (same fields as today).
      • total: integer from get_eligible_users_count().
      • totalPages: calculated as ceil( total / PER_PAGE ) (using the constant from Eligible_Subscribers_Query).

Frontend (JavaScript)

  • Update file assets/js/googlesitekit/datastore/site/email-reporting.js:
    • Update the existing fetchGetEligibleSubscribersStore (or create a replacement) to accept { page, search } parameters in the control callback, passing them as { page, search } to the API request.
    • Change initial state for eligibleSubscribers from a flat value to an object keyed by stringified params (e.g. {}) so each parameter combination is cached independently.
    • Update the getEligibleSubscribers( state, { page, search } ) selector to return { users, total, totalPages } from the cached entry, or undefined on cache miss.
    • Update the resolver for getEligibleSubscribers to trigger a fetch when the requested param combination is not yet cached.
    • Ensure the existing getEligibleSubscribers selector continues to filter out the current user from the users array.

Frontend — Component Integration (JavaScript)

  • Update file assets/js/components/email-reporting/InviteOthersToSubscribe/index.js:

    • Move debounce logic from InviteSearchInput up to this component: derive debouncedSearchTerm from searchTerm using the existing useDebounce hook (300 ms) so the debounced value drives the API call.
    • Replace the parameter-less getEligibleSubscribers() call with getEligibleSubscribers( { search: debouncedSearchTerm } ).
    • Update isLoading to check hasFinishedResolution( 'getEligibleSubscribers', [ { search: debouncedSearchTerm } ] ).
    • Use total from the selector response (with empty search) to decide whether to show the search input (threshold remains > 6).
    • Remove the client-side invitableUsers filter (user.subscribed). The server already excludes subscribed users.
    • Pass users directly from the selector response to InviteUserList.
    • Remove page-related state — pagination is handled transparently by the resolver.
    • Keep existing reset behaviour: clear searchTerm and inviteResults when isOpen becomes false.
  • Update file assets/js/components/email-reporting/InviteOthersToSubscribe/InviteSearchInput.js:

    • Remove internal debounce logic (useDebounce, debouncedOnChange). The parent now handles debouncing.
    • Simplify to a controlled input: call onChange directly on every keystroke.
    • Keep the clear button — call onChange('') on clear.
  • Update file assets/js/components/email-reporting/InviteOthersToSubscribe/InviteUserList.js:

    • Remove the useMemo-based client-side filtering. The users prop is now pre-filtered by the server.
    • Remove the searchTerm prop entirely — no longer needed.
    • Keep existing loading skeleton, empty state, and user row rendering.

Tests

  • PHPUnit tests in tests/phpunit/integration/Core/Email_Reporting/Eligible_Subscribers_QueryTest.php:

    • Test pagination returns the correct subset of users.
    • Test search filters by display name and email.
    • Test get_eligible_users_count() returns the correct total.
    • Test combined search + pagination.
    • Test deduplication of admin + shared-role users before pagination.
  • PHPUnit tests in tests/phpunit/integration/Core/Email_Reporting/REST_Email_Reporting_ControllerTest.php:

    • Test default request (no params) returns first 20 users with users, total, totalPages.
    • Test page param returns the correct page.
    • Test search param filters results.
    • Test response shape includes users, total, totalPages.
    • Test non-matching search returns { users: [], total: 0, totalPages: 0 }.
  • Jest tests in assets/js/googlesitekit/datastore/site/email-reporting.test.js:

    • Test getEligibleSubscribers selector returns cached data for matching params.
    • Test resolver fetches with correct params on cache miss.
    • Test different param combinations are cached separately.
  • Jest tests in assets/js/components/email-reporting/InviteOthersToSubscribe/index.test.js:

    • Test initial render fetches with empty search term.
    • Test typing in search triggers server fetch after 300 ms debounce.
    • Test clearing search resets to unfiltered results.
    • Test panel close resets all state.
  • Jest tests in assets/js/components/email-reporting/InviteOthersToSubscribe/InviteUserList.test.js:

    • Remove or update tests that relied on client-side searchTerm filtering.
  • Jest tests in assets/js/components/email-reporting/InviteOthersToSubscribe/InviteSearchInput.test.js:

    • Update tests to reflect removed internal debounce (direct onChange calls).

QA Brief

  • Setup Site Kit with proactiveUserEngagement feature flag
  • Open dev tools, networking tab, and filter requests for email-reporting-eligible-subscriber
  • Open a user menu > manage email reports
  • Verify that requests are made, and params for page=X and search are included and response is formatted in: {"users":[],"total":X"totalPages":X}
    • If neither SC or GA4 modules are shared, there should be no eligible users returned
    • When one or both modules are shared, all users with access should be showing
  • Close the panel and reopen, if nothing changed requests should be cached and no new request should be made
  • When using a search box, requests should be made with search param including the search teerm and proper pagination (page=X)

Note: Ping @zutigrm for a small user generating plugin, so you can quickly create X number of users with set roles on the website for testing

Changelog entry

  • Improve user filtering and requests for subscriptions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0High priorityPHPTeam SIssues for Squad 1Type: EnhancementImprovement of an existing featurejavascriptPull requests that update Javascript code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions