Skip to content

Commit a363eab

Browse files
committed
fix(admin-admins): wrap advanced-search form in collapsible <details> (#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
1 parent a9bd72f commit a363eab

9 files changed

Lines changed: 467 additions & 21 deletions

File tree

‎AGENTS.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,8 @@ audit (#1207) locked in. New CTAs:
12711271
| Render sub-views inside a Pattern A section (e.g. protests / submissions current-vs-archive) | `?view=<slug>` query param + a server-rendered `.chip-row` of real anchors (each carries `data-active="true|false"` + `aria-selected`). Reference: the protests / submissions chip rows in `web/pages/admin.bans.php` (`?section=protests&view=archive` / `?section=submissions&view=archive`). Pre-#1275 the chips called `Swap2ndPane()` — a `web/scripts/sourcebans.js` helper deleted at #1123 D1, leaving them dead — and the page rendered both views simultaneously. The new shape only renders the active view's data path; back/forward and link sharing both work. |
12721272
| Lay out a sub-paged admin route's chrome (the 14rem vertical sidebar at `>=1024px`, the `<details open>` accordion at `<1024px`) | `web/themes/default/core/admin_sidebar.tpl` (the partial) + the `.admin-sidebar-shell` / `.admin-sidebar` / `.admin-sidebar__details` / `.admin-sidebar__summary` / `.admin-sidebar__nav` / `.admin-sidebar__link` / `.admin-sidebar-content` rules in `web/themes/default/css/theme.css` (#1259). The active link reuses the shared `.sidebar__link[aria-current="page"]` rule from the main app shell so the dark-pill-in-light / brand-orange-in-dark treatment is single-source. |
12731273
| Render the trailing "Back" link on edit-* admin pages (the only surface that calls `new AdminTabs([], …)`) | `web/themes/default/core/admin_tabs.tpl` is the back-link-only partial (it still has a defensive `{foreach}` for legacy themes, but `web/includes/View/AdminTabs.php` only routes here when `$tabs === []`). Page handlers like `admin.edit.ban.php` / `admin.rcon.php` / `admin.email.php` call `new AdminTabs([], $userbank, $theme)` and the partial emits the right-aligned Back anchor (`.admin-tabs__back` in theme.css). |
1274-
| Add or rename an admin-admins advanced-search filter | `web/pages/admin.admins.php` (filter-building loop + active-filter map for pagination) + `web/pages/admin.admins.search.php` (DTO population) + `web/includes/View/AdminAdminsSearchView.php` (`active_filter_*` properties) + `web/themes/default/box_admin_admins_search.tpl` (input + pre-fill). The form is single-submit AND-semantics with a backward-compat shim for legacy `advType=…&advSearch=…` URLs (#1207 ADM-4); cover new filters in `web/tests/integration/AdminAdminsSearchTest.php`. |
1274+
| Add or rename an admin-admins advanced-search filter | `web/pages/admin.admins.php` (filter-building loop + active-filter map for pagination) + `web/pages/admin.admins.search.php` (DTO population + `$active_filter_count` increment for the new slot) + `web/includes/View/AdminAdminsSearchView.php` (`active_filter_*` properties) + `web/themes/default/box_admin_admins_search.tpl` (input + pre-fill). The form is single-submit AND-semantics with a backward-compat shim for legacy `advType=…&advSearch=…` URLs (#1207 ADM-4); cover new filters in `web/tests/integration/AdminAdminsSearchTest.php`. |
1275+
| Wrap a filter `<form>` in a default-collapsed `<details>` disclosure (admin-admins advanced search; the public banlist / commslist filter bars are candidates for the same shape per #1303's notes) | `.filters-details` rules in `web/themes/default/css/theme.css` + reference shape in `web/themes/default/box_admin_admins_search.tpl` (`<details class="card filters-details" {if $has_active_filters}open{/if}>` with a `<summary data-testid="…-toggle">` carrying the title + chevron + optional "N active" count badge). The View carries paired `int $active_filter_count` + `bool $has_active_filters` properties (#1303); the page handler increments the count once per populated value slot, NEVER per match-mode toggle. The disclosure auto-expands on a post-submit paint so the Clear-filters affordance stays one click away. Visual vocabulary mirrors `core/admin_sidebar.tpl`'s mobile `<details open>` accordion (chevron + `prefers-reduced-motion: reduce` override). |
12751276
| Add a shared "1 of these required" badge for an either/or input pair | `web/themes/default/page_submitban.tpl` (`data-required-group="…"` + the inline guard script — vanilla JS `// @ts-check`, blocks submit when both are empty) |
12761277
| Bootstrap (paths, autoload, theme) | `web/init.php` |
12771278
| Routing (`?p=…&c=…&o=…`) | `web/includes/page-builder.php` — unrecognised admin `c=…` returns the 404 page slot via `web/pages/page.404.php` + `Sbpp\View\NotFoundView` (#1207 ADM-1) |

‎web/includes/View/AdminAdminsSearchView.php‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@
3939
* (admin.admins.search.php) is the only place that knows whether a
4040
* given $_GET shape came from a modern submit, a legacy
4141
* `advType=…&advSearch=…` URL, or nothing at all.
42+
*
43+
* #1303 — collapsible disclosure
44+
* ------------------------------
45+
* The form is wrapped in a `<details class="card filters-details">`
46+
* default-collapsed disclosure so the unfiltered admin list paints
47+
* above the fold. `$has_active_filters` (derived from the nine
48+
* `active_filter_*` value slots — match-mode toggles don't count
49+
* because they always carry a default) drives the `[open]` attribute,
50+
* so any post-submit page paints with the form expanded. The chrome
51+
* mirrors `core/admin_sidebar.tpl`'s mobile `<details open>` pattern
52+
* (chevron + label + `prefers-reduced-motion: reduce` override). The
53+
* count badge ("Filters · N active") rides `$active_filter_count`.
4254
*/
4355
final class AdminAdminsSearchView extends View
4456
{
@@ -74,6 +86,16 @@ final class AdminAdminsSearchView extends View
7486
* to mark the matching `<option selected>` rows.
7587
* @param list<string> $active_filter_admsrvflag Pre-filled
7688
* `admsrvflag[]` values.
89+
* @param int $active_filter_count Number of non-empty filter
90+
* value slots — drives the `<summary>` count badge ("Filters
91+
* · N active") and `$has_active_filters`. Match-mode toggles
92+
* (`name_match` / `steam_match` / `admemail_match`) are NOT
93+
* counted: they always carry a default ('0' or '1') and only
94+
* refine the matching filter, they don't filter on their own.
95+
* @param bool $has_active_filters Convenience boolean derived from
96+
* `$active_filter_count > 0`. The template uses it to decide
97+
* whether the disclosure paints `<details open>` (post-submit
98+
* paint) vs default-collapsed (first-paint).
7799
*/
78100
public function __construct(
79101
public readonly bool $can_editadmin,
@@ -96,6 +118,8 @@ public function __construct(
96118
public readonly string $active_filter_server = '',
97119
public readonly array $active_filter_admwebflag = [],
98120
public readonly array $active_filter_admsrvflag = [],
121+
public readonly int $active_filter_count = 0,
122+
public readonly bool $has_active_filters = false,
99123
) {
100124
}
101125
}

‎web/pages/admin.admins.search.php‎

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,40 @@
178178
}
179179
}
180180

181+
// Match-mode defaults differ per filter (#1231):
182+
// - steam_match defaults to '0' (exact) — typical SteamID
183+
// queries are "find this one admin by their full ID".
184+
// - name_match / admemail_match default to '1' (partial) so
185+
// pre-#1231 URLs (`?name=alice`) keep their substring
186+
// behaviour. Adding the toggle widens the UI without
187+
// regressing the default.
188+
$activeFilterName = is_string($_GET['name'] ?? null) ? (string) $_GET['name'] : '';
189+
$activeFilterSteamid = is_string($_GET['steamid'] ?? null) ? (string) $_GET['steamid'] : '';
190+
$activeFilterAdmemail = is_string($_GET['admemail'] ?? null) ? (string) $_GET['admemail'] : '';
191+
$activeFilterWebgroup = is_scalar($_GET['webgroup'] ?? null) ? (string) $_GET['webgroup'] : '';
192+
$activeFilterSrvadmgroup = is_string($_GET['srvadmgroup'] ?? null) ? (string) $_GET['srvadmgroup'] : '';
193+
$activeFilterSrvgroup = is_scalar($_GET['srvgroup'] ?? null) ? (string) $_GET['srvgroup'] : '';
194+
$activeFilterServer = is_scalar($_GET['server'] ?? null) ? (string) $_GET['server'] : '';
195+
196+
// #1303 — count populated filter slots so the disclosure can paint a
197+
// "Filters · N active" badge on the <summary> and auto-expand on
198+
// post-submit. Match-mode selects (`name_match` / `steam_match` /
199+
// `admemail_match`) deliberately don't count: they always carry a
200+
// default ('0' or '1') and only refine the matching filter, they
201+
// don't filter on their own. Empty multi-select arrays count as zero
202+
// even though the array itself "exists" — the user hasn't picked a
203+
// permission.
204+
$activeFilterCount =
205+
($activeFilterName !== '' ? 1 : 0)
206+
+ ($activeFilterSteamid !== '' ? 1 : 0)
207+
+ ($activeFilterAdmemail !== '' ? 1 : 0)
208+
+ ($activeFilterWebgroup !== '' ? 1 : 0)
209+
+ ($activeFilterSrvadmgroup !== '' ? 1 : 0)
210+
+ ($activeFilterSrvgroup !== '' ? 1 : 0)
211+
+ ($activeFilterServer !== '' ? 1 : 0)
212+
+ (count($activeWebFlags) > 0 ? 1 : 0)
213+
+ (count($activeSrvFlags) > 0 ? 1 : 0);
214+
181215
\Sbpp\View\Renderer::render($theme, new \Sbpp\View\AdminAdminsSearchView(
182216
can_editadmin: $userbank->HasAccess(WebPermission::mask(WebPermission::EditAdmins, WebPermission::Owner)),
183217
server_list: $servers,
@@ -187,23 +221,18 @@
187221
srvgroup_list: $srvgroups,
188222
admwebflag_list: $webflag,
189223
admsrvflag_list: $serverflag,
190-
// Match-mode defaults differ per filter (#1231):
191-
// - steam_match defaults to '0' (exact) — typical SteamID
192-
// queries are "find this one admin by their full ID".
193-
// - name_match / admemail_match default to '1' (partial) so
194-
// pre-#1231 URLs (`?name=alice`) keep their substring
195-
// behaviour. Adding the toggle widens the UI without
196-
// regressing the default.
197-
active_filter_name: is_string($_GET['name'] ?? null) ? (string) $_GET['name'] : '',
224+
active_filter_name: $activeFilterName,
198225
active_filter_name_match: is_scalar($_GET['name_match'] ?? null) ? (string) $_GET['name_match'] : '1',
199-
active_filter_steamid: is_string($_GET['steamid'] ?? null) ? (string) $_GET['steamid'] : '',
226+
active_filter_steamid: $activeFilterSteamid,
200227
active_filter_steam_match: is_scalar($_GET['steam_match'] ?? null) ? (string) $_GET['steam_match'] : '0',
201-
active_filter_admemail: is_string($_GET['admemail'] ?? null) ? (string) $_GET['admemail'] : '',
228+
active_filter_admemail: $activeFilterAdmemail,
202229
active_filter_admemail_match: is_scalar($_GET['admemail_match'] ?? null) ? (string) $_GET['admemail_match'] : '1',
203-
active_filter_webgroup: is_scalar($_GET['webgroup'] ?? null) ? (string) $_GET['webgroup'] : '',
204-
active_filter_srvadmgroup: is_string($_GET['srvadmgroup'] ?? null) ? (string) $_GET['srvadmgroup'] : '',
205-
active_filter_srvgroup: is_scalar($_GET['srvgroup'] ?? null) ? (string) $_GET['srvgroup'] : '',
206-
active_filter_server: is_scalar($_GET['server'] ?? null) ? (string) $_GET['server'] : '',
230+
active_filter_webgroup: $activeFilterWebgroup,
231+
active_filter_srvadmgroup: $activeFilterSrvadmgroup,
232+
active_filter_srvgroup: $activeFilterSrvgroup,
233+
active_filter_server: $activeFilterServer,
207234
active_filter_admwebflag: $activeWebFlags,
208235
active_filter_admsrvflag: $activeSrvFlags,
236+
active_filter_count: $activeFilterCount,
237+
has_active_filters: $activeFilterCount > 0,
209238
));

‎web/tests/e2e/pages/admin/AdminAdmins.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ export class AdminAdminsPage extends BasePage {
7474
return this.page.locator('[data-testid="search-admins-reset"]');
7575
}
7676

77+
/** #1303 — the `<details>` disclosure wrapping the search form. */
78+
get searchDisclosure(): Locator {
79+
return this.page.locator('[data-testid="search-admins-disclosure"]');
80+
}
81+
82+
/** #1303 — the `<summary>` toggle that opens / closes the disclosure. */
83+
get searchToggle(): Locator {
84+
return this.page.locator('[data-testid="search-admins-toggle"]');
85+
}
86+
87+
/** #1303 — the "N active" badge (only present when count > 0). */
88+
get searchActiveCount(): Locator {
89+
return this.page.locator('[data-testid="search-admins-active-count"]');
90+
}
91+
7792
/** ADM-4 — pre-filled inputs by name. */
7893
searchInput(field: 'name' | 'steamid' | 'admemail' | 'webgroup' | 'srvadmgroup' | 'srvgroup' | 'admwebflag' | 'admsrvflag' | 'server'): Locator {
7994
return this.page.locator(`[data-testid="search-admins-${field}"]`);

‎web/tests/e2e/specs/flows/admin-admins-density.spec.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ test.describe('flow: admin/admins density rework (#1207 ADM-3, ADM-4 / #1275)',
102102
const p = new AdminAdminsPage(page);
103103
await p.goto();
104104

105-
// Exactly one Search submit + one reset link, sitting at the
106-
// bottom of the form (collapsing 8 per-row buttons was the
107-
// headline of ADM-4).
105+
// Exactly one Search submit + one reset link in the rendered
106+
// DOM (collapsing 8 per-row buttons was the headline of
107+
// ADM-4). Count assertion works whether the #1303 disclosure
108+
// is open or closed — browsers parse <details> children
109+
// either way.
108110
await expect(p.searchSubmit).toHaveCount(1);
109111
await expect(p.searchReset).toHaveCount(1);
110112

@@ -115,6 +117,12 @@ test.describe('flow: admin/admins density rework (#1207 ADM-3, ADM-4 / #1275)',
115117
const baselineCount = await p.adminRows.count();
116118
expect(baselineCount).toBeGreaterThanOrEqual(1);
117119

120+
// #1303 — the form ships inside a default-collapsed
121+
// `<details>` disclosure, so open it before driving the
122+
// inputs. Native `<details>` makes `[open]` flip
123+
// synchronously on click; no animation-driven sentinel.
124+
await p.searchToggle.click();
125+
118126
// Two filters at once: a deliberately not-matching login
119127
// ("zzznoadminmatchesthis") + a steam_match=1 (partial). The
120128
// login filter narrows the result list to zero — locking the

0 commit comments

Comments
 (0)