Skip to content

Comments: Hide zero-count status filters on the Comments list table - #14056

Open
AbdiTolesa wants to merge 11 commits into
WordPress:trunkfrom
AbdiTolesa:fix/hide-zero-count-comment-status-filters
Open

AbdiTolesa wants to merge 11 commits into
WordPress:trunkfrom
AbdiTolesa:fix/hide-zero-count-comment-status-filters

Conversation

@AbdiTolesa

@AbdiTolesa AbdiTolesa commented Oct 6, 2026 •

Copy link
Copy Markdown

Trac ticket: https://core.trac.wordpress.org/ticket/66260

Summary

WP_Posts_List_Table::get_views() hides any status filter link (other than "All") whose post count is zero. WP_Comments_List_Table::get_views() had no equivalent, so "Pending", "Approved", "Spam", "Trash", and "Mine" were always rendered even at (0).

Per review feedback, applying the Posts list table's logic as-is doesn't fit the Comments screen, which is primarily a moderation queue rather than a plain content list:

  • Hiding "Pending (0)" removes a reassuring "inbox zero" signal and could read as moderation being broken.
  • Hiding "Approved (0)" removes the default working view.
  • Hiding whichever status is currently selected (e.g. landing on ?comment_status=moderated from a stale notification, or ?comment_status=spam right after "Empty Spam") leaves the page with no visible "current" tab at all — no indication of what's being filtered.

Changes

  • "Mine", "Spam", and "Trash" are hidden when their count is zero (unless currently active). "All", "Pending", and "Approved" always stay visible regardless of count. The currently active $comment_status is never hidden, even at zero count.
  • Removed test_get_views_should_return_views_by_default (added for #42066), which asserted the old all-statuses-always-shown behavior this change intentionally revises.

How empty views are hidden: CSS class, not DOM omission

An earlier version of this PR had get_views() omit empty/non-active status entries from its returned array entirely, and had an AJAX moderation action re-render and replace the whole .subsubsub tab list over the wire so a status could appear/disappear live. Per review feedback, that re-render approach was replaced: re-rendering and swapping .subsubsub's HTML on every moderation action would destroy any DOM nodes, event handlers, or data a plugin may have attached to the existing view links — an architectural regression for plugin extensibility, even though it worked fine in isolation on a clean install.

Current approach:

  • get_views() always returns every status key (never omits one); it separately records which ones are currently empty in a new $empty_views property.
  • A new WP_Comments_List_Table::views() override (the base WP_List_Table::views() has no hook for this) adds an is-empty-view class to those views' <li> elements. A new CSS rule (.subsubsub li.is-empty-view { display: none; }) hides them — the markup itself always stays in the DOM.
  • edit-comments.js's delAfter handler toggles that class on the relevant <li> (via a new toggleEmptyView() helper) after a spam/trash count changes, instead of replacing any markup. The currently active view is still never hidden, even at zero count.

Test plan

  • php -l / node --check / npx eslint on all changed files — no errors.
  • Ran Tests_Admin_wpCommentsListTable + Tests_Admin_wpPostCommentsListTable — 15 tests, 54 assertions, all passing.
  • Ran Tests_Ajax_wpAjaxDeleteComment (--group ajax) — 5 tests, 84 assertions, all passing (back to its original count/shape, since the AJAX re-render approach and its test were reverted).
  • Verified the "never hide the active view" test fails when that exception is removed from get_views(), and passes with it restored.
  • Rebuilt edit-comments.js/.min.js and list-tables.css/.min.css/-rtl.css via grunt build:js --dev / grunt build:css --dev for local testing.

Fix: dangling "|" separator after a hidden trailing view

Spotted after the above: the | separator between views was attached as the last character inside the preceding view's own <li> (mirroring the parent WP_List_Table::views()), which assumed every view was actually visible. Now that empty views stay in the DOM (just CSS-hidden), a visible view followed only by hidden ones (e.g. "Approved" when "Spam"/"Trash" are both empty) still rendered its own trailing |, dangling with nothing after it. Fixed by attaching the separator as a leading marker on each view instead, skipped for hidden views and the first view that actually renders visible — so it disappears along with whatever's hidden around it, regardless of which combination of views are empty.

Fix: separator didn't appear when a hidden view became visible via JS

The previous fix baked the | separator into each view as a leading marker, computed from which views were empty at render time. That only held for the initial page load: when edit-comments.js later reveals a previously-hidden view by removing its is-empty-view class (e.g. "Trash" becoming non-empty after trashing a comment), that view's HTML never had a separator in it to begin with, so it rendered flush against the preceding view with nothing between them.

Fixed properly this time: the separator is no longer written as literal text in PHP at all. It's generated by CSS instead (::before on every non-first <li>, scoped to the Comments screen so other list tables — which still embed it as literal text and don't use is-empty-view — aren't affected). A hidden view's own separator is part of its own box, so it disappears and reappears together with the view itself automatically, regardless of which direction (initial PHP render, or a later JS toggle) its visibility changes in.

Refactor: share WP_List_Table::views() scaffolding instead of duplicating it

WP_Comments_List_Table::views() duplicated nearly all of WP_List_Table::views() (fetching/filtering views, the empty check, the screen-reader heading, the <ul> wrapper) just to customize how individual <li> elements are printed. Extracted that per-item printing into a new protected WP_List_Table::print_views( $views ) method that subclasses can override on its own — WP_Comments_List_Table now only overrides print_views(), nothing else. No behavior change for any other list table; this only moves existing code into its own method.

WP_Posts_List_Table::get_views() hides any status filter link (other
than "All") whose post count is zero. WP_Comments_List_Table::get_views()
had no equivalent check, so "Pending", "Approved", "Spam", "Trash", and
"Mine" were always shown even with a count of (0), cluttering the
screen with filters that return no results.

Apply the same logic: skip adding a status link when its count is
empty, except for "All" which always stays visible.

This reverses the behavior asserted by test_get_views_should_return_views_by_default
(added for #42066), which is removed since it codified the opposite,
now-undesired behavior. No replacement test is added in this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 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.

Core Committers: Use this line as a base for the props when committing in SVN:

Props abditsori.

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

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

AbdiTolesa and others added 10 commits October 6, 2026 22:32
Covers WP_Comments_List_Table::get_views() hiding "Pending", "Approved",
"Spam", and "Trash" links when their count is zero, while keeping "All"
visible, and showing a status link again once its count becomes nonzero.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ive view

Addresses review feedback on the zero-count filtering added in the
previous commit. Comments is a moderation queue, not a plain list like
Posts, so:

- "Pending" and "Approved" now always stay visible regardless of count.
  A "(0)" on Pending is reassuring confirmation of an empty queue, not
  clutter, and hiding it could read as moderation being broken.
- The currently active $comment_status is never hidden, even at zero
  count, so an admin who lands on e.g. ?comment_status=moderated (from
  a stale notification) or ?comment_status=spam (after "Empty Spam")
  always has a visible, "current" tab showing what they're filtered to.

"Mine", "Spam", and "Trash" are still hidden when empty and not the
active view.

Updated the existing get_views() tests for the new rules and added a
regression test for the active-view exception.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… hiding

WP_Post_Comments_List_Table extends WP_Comments_List_Table and inherits
its get_views(), so it was asserting the same superseded
all-statuses-always-shown behavior (also added for #42066) that was
already updated in wpCommentsListTable.php. Missed in the earlier
commit since it lives in a separate test file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n-zero via AJAX

Status links (e.g. "Trash", "Spam") are now hidden entirely when their
count is zero (see WP_Comments_List_Table::get_views()). edit-comments.js
only updates the *text* of existing count spans after an AJAX
moderation action, so when a status link was never rendered in the
first place, incrementing its (non-existent) count text does nothing:
trashing the first comment on a site with no prior trashed comments
left no way to reach the Trash view without a full page reload.

_wp_ajax_delete_comment_response() now re-renders the comment status
view tabs (WP_Comments_List_Table::views()), using the requesting
page's own comment_status/p context (recovered from the posted _url),
and includes the fresh HTML in the AJAX response's supplemental data.
edit-comments.js replaces the whole .subsubsub list with it, after the
existing incremental count updates (since it supersedes them), so a
status that just crossed zero appears or disappears immediately.

Added a test asserting the "Trash" link is present in the AJAX
response after trashing the only trashable comment; confirmed it fails
without the fix and passes with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r AJAX

Per review feedback, replace the previous AJAX approach to keeping
status view tabs ("Mine", "Spam", "Trash") in sync after a moderation
action. Re-rendering WP_Comments_List_Table::views() server-side and
replacing the whole .subsubsub node over AJAX (previous commit) works
in isolation, but destroys any DOM nodes, event handlers, or data a
plugin may have attached to the existing view links on every single
moderation action - an architectural regression for plugin
extensibility.

Instead:
- WP_Comments_List_Table::get_views() no longer omits empty, non-active
  status entries from its returned array; every status key is always
  present now, and get_views() instead records which ones are empty in
  a new $empty_views property.
- A new WP_Comments_List_Table::views() override (the base
  WP_List_Table::views() doesn't support this) adds an `is-empty-view`
  class to those views' <li> elements, hidden via a new CSS rule,
  rather than omitting their markup.
- edit-comments.js's delAfter handler toggles that class on the
  relevant <li> (via a new toggleEmptyView() helper) after a spam/trash
  count changes, instead of replacing the view tabs' HTML wholesale.
  The currently active view is still never hidden, even at zero count.
- Reverted the AJAX supplemental data / full-HTML-replace added to
  _wp_ajax_delete_comment_response() and edit-comments.js in the
  previous commit.

Updated the get_views()/views() tests in wpCommentsListTable.php and
wpPostCommentsListTable.php for the new always-present-keys /
CSS-class-based behavior, and removed the AJAX response test that
asserted the reverted approach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
views() attached each view's " | " separator as the last thing inside
the *preceding* view's own <li>, mirroring the parent class. That
relied on every view actually being visible, which no longer holds now
that empty views stay in the DOM (hidden via is-empty-view) instead of
being omitted. A visible view followed only by hidden ones (e.g.
"Approved" when "Spam" and "Trash" are both empty) still rendered its
own trailing "|", with nothing visible after it to pair with.

Attach the separator as a leading marker on each view instead, skipped
for hidden views and for the first view that actually renders visible,
so a run of hidden views never leaves an orphaned separator next to
them regardless of which views are empty.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…to HTML

The previous fix attached the " | " separator as a leading marker on
each view, computed from which views were empty at render time. That
only holds at the moment of rendering: when edit-comments.js later
reveals a previously-hidden view by removing its is-empty-view class
(e.g. "Trash" becoming non-empty after a comment is trashed), that
view's HTML never had a separator baked into it to begin with, so it
renders flush against the preceding view with no separator between
them.

Stop writing the separator as literal text entirely and generate it
with CSS instead (`::before` on every non-first <li>, scoped to the
Comments screen so other list tables - which still embed the separator
as literal text and don't use is-empty-view - aren't affected). A
hidden view's own separator is part of its own box, so it hides and
reappears together with the view itself automatically, regardless of
which direction (PHP's initial render or JS's later toggle) changes
its visibility.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…erriding

WP_Comments_List_Table::views() duplicated nearly all of
WP_List_Table::views() (fetching views, filtering, the empty check,
the screen-reader heading, the <ul> wrapper) just to customize how
individual <li> elements are printed (adding an is-empty-view class).

Split that per-item printing out of WP_List_Table::views() into a new
protected print_views() method, so a subclass that needs to customize
it can override just that, instead of the whole method. Behavior for
every other list table is unchanged - this only moves existing code
into its own method.

WP_Comments_List_Table now only overrides print_views().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant