Skip to content

Fix #1303: wrap admin/admins advanced-search form in collapsible <details> - #1318

Merged
rumblefrog merged 2 commits into
mainfrom
fix/issue-1303-admin-search-collapsible
May 10, 2026
Merged

rumblefrog merged 2 commits into
mainfrom
fix/issue-1303-admin-search-collapsible

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Fixes #1303.

Summary

The post-#1207 ADM-4 advanced-search card on Admin → Admin Management
always rendered fully expanded above the admin list. Nine filter rows
(login + match-mode, SteamID + match-mode, e-mail + match-mode, web
group, SourceMod admin group, server group, web-permissions
multi-select, server-permissions multi-select, server) plus a footer
pushed the actual list well below the fold for the common unfiltered
case while filtering is occasional.

This wraps the form in a default-collapsed <details class="card filters-details"> disclosure so the unfiltered list reaches above the
fold; the disclosure auto-expands on a post-submit paint so the filter
chrome (and the Clear-filters affordance) stays visible while the user
iterates. Wire format and AND semantics are unchanged; only the chrome
wrapping the form is new.

  • View DTO (AdminAdminsSearchView) gains paired int $active_filter_count + bool $has_active_filters properties; the
    page handler increments the count once per populated value slot.
    Match-mode toggles (name_match / steam_match / admemail_match)
    deliberately don't count: they always carry a default ('0' or '1')
    and only refine the matching filter, they don't filter on their own.
  • <summary> mirrors core/admin_sidebar.tpl's mobile accordion
    vocabulary (filter icon + label + count badge + chevron with 180°
    rotation on [open]). The prefers-reduced-motion: reduce global
    rule already collapses the chevron transition to ~0ms; an explicit
    per-rule override in the new .filters-details block stays
    defensive parity.
  • Reusable shape — .filters-details is intentionally generic so
    the public banlist / commslist filter bars (page_bans.tpl /
    page_comms.tpl) can adopt the same pattern per the issue body's
    notes; left for a follow-up.
  • AGENTS.md "Where to find what" gains a row for the new
    collapsible filter pattern; the existing admin-admins
    advanced-search row is updated to flag the $active_filter_count
    increment as a paired edit when adding new filter slots.

Test plan

All gates run locally against the isolated sbpp-task-1303 stack:

  • ./sbpp.sh phpstan — pass (228 files, no errors)
  • ./sbpp.sh test — pass (407 tests, 1779 assertions; the 15
    AdminAdminsSearchTest cases include 4 new disclosure-shape tests:
    default-closed, auto-open with active filter, count matches
    populated slots and ignores match-mode toggles, empty multi-select
    arrays don't lift the count)
  • ./sbpp.sh ts-check — pass (no errors)
  • ./sbpp.sh composer api-contract — clean (no diff; the patch
    doesn't touch any handler)
  • ./sbpp.sh e2e --grep "admin/admins" — pass (12 tests; 4 new
    disclosure E2E specs + the existing density specs, including the
    one updated to open the disclosure before driving the form)

Selectors anchor on the new data-testid hooks
(search-admins-disclosure, search-admins-toggle,
search-admins-active-count) per AGENTS.md's "Selectors must use
#1123's testability hooks" rule. The native <details> [open]
attribute flips synchronously on click, so no setTimeout /
animation-driven sentinel is needed.

…#1303)

The post-#1207 ADM-4 advanced-search card on Admin > Admin Management
always rendered fully expanded above the admin list. Nine filter rows
pushed the actual list well below the fold for the common unfiltered
case while filtering is occasional. Wrap the form in a default-collapsed
`<details class="card filters-details">` disclosure so the unfiltered
list reaches above the fold; auto-expand on a post-submit paint so the
filter chrome (and the Clear-filters affordance) stays visible while
the user iterates.

- View DTO carries paired `int $active_filter_count` + `bool
  $has_active_filters` properties; the page handler increments the
  count once per populated value slot. Match-mode toggles
  (`name_match` / `steam_match` / `admemail_match`) deliberately
  don't count: they always carry a default ('0' or '1') and only
  refine the matching filter, they don't filter on their own.
- `<summary>` mirrors `core/admin_sidebar.tpl`'s mobile accordion
  vocabulary (filter icon + label + count badge + chevron with 180°
  rotation on `[open]`). The `prefers-reduced-motion: reduce` global
  rule already collapses the chevron transition to ~0ms; an extra
  per-rule override in the new `.filters-details` block stays
  defensive parity.
- Reusable `.filters-details` shape — the public banlist / commslist
  filter bars (`page_bans.tpl` / `page_comms.tpl`) are candidates
  for the same shape per the issue body's notes; left for a follow-up.
- Tests: 4 new PHPUnit cases lock the disclosure contract end-to-end
  (default-closed, auto-open with active filter, count matches
  populated slots and ignores match-mode toggles, empty multi-select
  arrays don't lift the count). 4 new Playwright specs lock the
  browser chrome (default-closed, opens on toggle click, auto-opens
  on URL with active filters, multi-filter URL count badge tracks).
  The existing density spec opens the disclosure before driving the
  form (the `[open]` attribute flips synchronously on click — no
  animation-driven sentinel).
- AGENTS.md "Where to find what" gets a row for the new collapsible
  filter pattern; the existing admin-admins advanced-search row is
  updated to flag the `$active_filter_count` increment as a paired
  edit when adding new filter slots.

Fixes #1303
…ount by perm)

Two small fixes on top of the #1303 collapsible advanced-search
disclosure:

- The `<summary>` already paints the "Advanced search" title (+ chevron
  + count badge), so the form's `card__header` emitting another
  `<h3>Advanced search</h3>` directly under it stacked two identical
  headings the moment a user opened the disclosure. Drop the H3; keep
  the explanatory paragraph (the load-bearing AND-semantics copy from
  the #1207 ADM-4 audit has no analog in the summary).

- The `admemail` filter is permission-gated by `EditAdmins | Owner` in
  both the form template AND the page handler (`admin.admins.php`
  ignores `?admemail=` from a user without the perm). The active-filter
  count was happily incrementing for that slot regardless of perm, so
  URL forgery (or a stale tab from a permission downgrade) painted
  "1 active" on the disclosure summary while every visible filter row
  read empty AND the disclosure auto-opened exposing nothing
  actionable. Mirror the gate locally so the count stays an honest
  summary of what the visible form actually filters on. Regression
  guard: `testDisclosureCountIgnoresPermissionGatedEmailSlot` (logs
  in as `ADMIN_LIST_ADMINS`-only and forges `?admemail=alice`,
  asserts zero count + closed disclosure + no e-mail input row).
@rumblefrog
rumblefrog added this pull request to the merge queue May 10, 2026
Merged via the queue into main with commit 280dea6 May 10, 2026
4 checks passed
@rumblefrog
rumblefrog deleted the fix/issue-1303-admin-search-collapsible branch May 10, 2026 20:24
rumblefrog added a commit that referenced this pull request May 10, 2026
…ble advanced search (#1315)

The v2.0 redesign collapsed the sourcebans.js-driven legacy public lists
into Smarty-only `page_bans.tpl` / `page_comms.tpl` and dropped three
power-user surfaces along the way. This PR restores them.

  1. Inline unban-meta line on admin-lifted rows. The reason cell on the
     desktop table emits "Unbanned by <admin>: <reason>"
     (`[data-testid="ban-unban-meta"]`) when `state == 'unbanned'`;
     mobile cards mirror with the `-mobile` suffix. Same shape on the
     commslist for `state == 'unmuted'` — higher priority there because
     no drawer fallback exists. Both are gated on `!$hideadminname` so
     anonymous viewers under a hidden-admins config don't get the admin
     name leaked. Read-side render only — no overlap with #1301 / #1323.
  2. Re-apply (Reban) icon affordance for expired / unbanned rows on
     the banlist desktop, gated on `ADMIN_OWNER | ADMIN_ADD_BAN`.
     Deep-links the smart-default `?p=admin&c=bans&section=add-ban&rebanid=<bid>`
     URL the existing `BansPrepareReban` JSON action already
     pre-populates. Mobile parity deferred — drawer is canonical.
  3. Advanced-search disclosure on both lists. Default-collapsed
     `<details class="filters-details">` wrapping the legacy multi-
     criterion search form (loaded via `{load_template
     file="admin.bans.search"}`). Auto-opens on a post-submit
     `?advType=&advSearch=` paint via a new
     `BanListView::$is_advanced_search_open` /
     `CommsListView::$is_advanced_search_open` boolean. Reuses the
     `.filters-details` chrome #1303 introduced for admin-admins
     (CSS rules ship in this PR; will dedupe harmlessly when #1318
     merges).

Tests:

  - PHPUnit: `PublicBanListRegressionTest` (7 cases — disclosure
    default-closed, auto-open, unban-meta render, Re-apply gating
    + URL shape, banlist + commslist parity). RunInSeparateProcess
    per method to dodge the page-handler `setPostKey()` redeclare
    trap, mirroring `Php82DeprecationsTest`'s pattern.
  - Playwright: `flows/public-banlist-regressions.spec.ts` (4 cases
    across chromium + mobile-chromium — disclosure default-closed,
    click-to-open, post-submit auto-open via URL navigation;
    closed-state axe coverage). Re-apply / unban-meta DB-state
    assertions live in the PHPUnit test where seeding is cleaner.

AGENTS.md "Where to find what" gains three rows for the new patterns.
rumblefrog added a commit that referenced this pull request May 10, 2026
…ble advanced search (#1315)

The v2.0 redesign collapsed the sourcebans.js-driven legacy public lists
into Smarty-only `page_bans.tpl` / `page_comms.tpl` and dropped three
power-user surfaces along the way. This PR restores them.

  1. Inline unban-meta line on admin-lifted rows. The reason cell on the
     desktop table emits "Unbanned by <admin>: <reason>"
     (`[data-testid="ban-unban-meta"]`) when `state == 'unbanned'`;
     mobile cards mirror with the `-mobile` suffix. Same shape on the
     commslist for `state == 'unmuted'` — higher priority there because
     no drawer fallback exists. Both are gated on `!$hideadminname` so
     anonymous viewers under a hidden-admins config don't get the admin
     name leaked. Read-side render only — no overlap with #1301 / #1323.
  2. Re-apply (Reban) icon affordance for expired / unbanned rows on
     the banlist desktop, gated on `ADMIN_OWNER | ADMIN_ADD_BAN`.
     Deep-links the smart-default `?p=admin&c=bans&section=add-ban&rebanid=<bid>`
     URL the existing `BansPrepareReban` JSON action already
     pre-populates. Mobile parity deferred — drawer is canonical.
  3. Advanced-search disclosure on both lists. Default-collapsed
     `<details class="filters-details">` wrapping the legacy multi-
     criterion search form (loaded via `{load_template
     file="admin.bans.search"}`). Auto-opens on a post-submit
     `?advType=&advSearch=` paint via a new
     `BanListView::$is_advanced_search_open` /
     `CommsListView::$is_advanced_search_open` boolean. Reuses the
     `.filters-details` chrome #1303 introduced for admin-admins
     (CSS rules ship in this PR; will dedupe harmlessly when #1318
     merges).

Tests:

  - PHPUnit: `PublicBanListRegressionTest` (7 cases — disclosure
    default-closed, auto-open, unban-meta render, Re-apply gating
    + URL shape, banlist + commslist parity). RunInSeparateProcess
    per method to dodge the page-handler `setPostKey()` redeclare
    trap, mirroring `Php82DeprecationsTest`'s pattern.
  - Playwright: `flows/public-banlist-regressions.spec.ts` (4 cases
    across chromium + mobile-chromium — disclosure default-closed,
    click-to-open, post-submit auto-open via URL navigation;
    closed-state axe coverage). Re-apply / unban-meta DB-state
    assertions live in the PHPUnit test where seeding is cleaner.

AGENTS.md "Where to find what" gains three rows for the new patterns.
Rushaway pushed a commit to srcdslab/sourcebans-pp that referenced this pull request May 15, 2026
…ban-meta + Re-apply + collapsible adv search) (sbpp#1330)

* fix(banlist+commslist): restore unban-meta line + Re-apply + collapsible advanced search (sbpp#1315)

The v2.0 redesign collapsed the sourcebans.js-driven legacy public lists
into Smarty-only `page_bans.tpl` / `page_comms.tpl` and dropped three
power-user surfaces along the way. This PR restores them.

  1. Inline unban-meta line on admin-lifted rows. The reason cell on the
     desktop table emits "Unbanned by <admin>: <reason>"
     (`[data-testid="ban-unban-meta"]`) when `state == 'unbanned'`;
     mobile cards mirror with the `-mobile` suffix. Same shape on the
     commslist for `state == 'unmuted'` — higher priority there because
     no drawer fallback exists. Both are gated on `!$hideadminname` so
     anonymous viewers under a hidden-admins config don't get the admin
     name leaked. Read-side render only — no overlap with sbpp#1301 / sbpp#1323.
  2. Re-apply (Reban) icon affordance for expired / unbanned rows on
     the banlist desktop, gated on `ADMIN_OWNER | ADMIN_ADD_BAN`.
     Deep-links the smart-default `?p=admin&c=bans&section=add-ban&rebanid=<bid>`
     URL the existing `BansPrepareReban` JSON action already
     pre-populates. Mobile parity deferred — drawer is canonical.
  3. Advanced-search disclosure on both lists. Default-collapsed
     `<details class="filters-details">` wrapping the legacy multi-
     criterion search form (loaded via `{load_template
     file="admin.bans.search"}`). Auto-opens on a post-submit
     `?advType=&advSearch=` paint via a new
     `BanListView::$is_advanced_search_open` /
     `CommsListView::$is_advanced_search_open` boolean. Reuses the
     `.filters-details` chrome sbpp#1303 introduced for admin-admins
     (CSS rules ship in this PR; will dedupe harmlessly when sbpp#1318
     merges).

Tests:

  - PHPUnit: `PublicBanListRegressionTest` (7 cases — disclosure
    default-closed, auto-open, unban-meta render, Re-apply gating
    + URL shape, banlist + commslist parity). RunInSeparateProcess
    per method to dodge the page-handler `setPostKey()` redeclare
    trap, mirroring `Php82DeprecationsTest`'s pattern.
  - Playwright: `flows/public-banlist-regressions.spec.ts` (4 cases
    across chromium + mobile-chromium — disclosure default-closed,
    click-to-open, post-submit auto-open via URL navigation;
    closed-state axe coverage). Re-apply / unban-meta DB-state
    assertions live in the PHPUnit test where seeding is cleaner.

AGENTS.md "Where to find what" gains three rows for the new patterns.

* fix(tests): update integration tests after rebase on main

Two test-only fixes surfaced when the PR rebased on top of sbpp#1320 +

- `BanListIpColumnTest::renderBanList` (from sbpp#1320) constructs a
  `BanListView` with positional + named arguments. This PR adds a 28th
  constructor parameter (`is_advanced_search_open`) which the test
  must now pass — default it to `false` so the IP-column suite stays
  exercising the column-render contract it was written for, not the
  disclosure state.
- `ServerMapImageRenderTest::testHandlerStillEmitsMapimgField` (from
  sbpp#1326) asserted a literal `"'mapimg'   => GetMapImage("` substring
  in `web/api/handlers/servers.php`. sbpp#1329's cache + debounce rewrite
  shifted the surrounding column alignment to 5 spaces, breaking the
  brittle match without changing the contract the test guards (the
  field is still emitted). Switch to a regex that tolerates any
  whitespace between the key and `=>` so a future alignment shift
  doesn't silently re-trigger the same false positive.

* fix(tests): login as admin in BanListIpColumnTest hideplayerips=false case

sbpp#1315's `<details class="filters-details">` disclosure on the public
ban list `{load_template file="admin.bans.search"}`s the legacy
advanced-search partial. The partial's backing `admin.bans.search.php`
re-derives `hideplayerips` / `hideadminname` from
`Config::getBool('banlist.hideplayerips') && !$userbank->is_admin()`
and `Renderer::render`s the result into Smarty, overwriting whatever
the parent BanListView had assigned. In production this is a no-op
because the parent (`page.banlist.php`) computes the SAME formula —
both halves converge. But `BanListIpColumnTest` hand-crafts
`BanListView::$hideplayerips` to exercise the column-gating contract
in isolation, so the partial's overwrite collapses the test's
fixture-controlled value down to whatever the test environment's
Config + $userbank happen to evaluate to.

Fix by logging in as admin in the two test methods that pass
`hideplayerips: false`. With `is_admin()=true`, the partial's
formula reduces to `(…) && !true = false`, matching the View's
hand-crafted `false`. The suppressed-state test
(`hideplayerips: true`) stays anonymous so the formula reduces to
`Config && true` — `true` under `data.sql`'s
`banlist.hideplayerips=1` default — also matching the View's input.

The test pre-dates sbpp#1315; it was written against the v2.0 template
that didn't yet `{load_template}` the partial, so the parent's
assigns were never overwritten. The new disclosure brings the
partial back into the render tree and the test has to maintain
the production invariant ("BanListView's hideplayerips matches
AdminBansSearchView's recomputed value") explicitly.
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.

Admin Management advanced search would be friendlier inside a collapsible "Filters" <details>

1 participant