Skip to content

fix(admin): backport upstream mods-list + groups select-all polish - #41

Merged
Rushaway merged 4 commits into
mainfrom
fix/upstream-admin-ui-polish
Sep 26, 2026
Merged

Rushaway merged 4 commits into
mainfrom
fix/upstream-admin-ui-polish

Conversation

@Rushaway

Copy link
Copy Markdown
Member

Summary

Backports the non-dependabot admin-UI fixes from upstream sbpp/sourcebans-pp that were still missing after comparing main against upstream — this covers items 5 and 4 from that audit (item 6, the Add Admin server-access grid CSS fix, turned out not applicable to this fork; see "Not included" below).

fix(admin-mods): decode entity-encoded mod metadata and add proper empty state

Port of upstream sbpp#1552, continuing our earlier feat/mods-list-compact-actions (#14) port before upstream's own follow-up review passes landed.

  • Mod name/folder/icon path are stored entity-encoded, but the template rendered them with Smarty's default auto-escape on top, so a mod whose name contains e.g. an ampersand painted the literal & in the admin list instead of &. Added |unescape:'html' at every text/attribute sink (desktop table, mobile cards, edit/delete aria-labels and data-name attributes).
  • Replaced the ad-hoc "No mods configured yet." paragraph with the shared .empty-state pattern (icon + title + body + permission-gated CTA) per the "Empty states" convention in AGENTS.md, adding the missing $permission_addmods property.
  • Real bug, not just cosmetic: $mod_count counted mid=0 (the reserved "Web" pseudo-mod every install carries) even though $mod_list always excludes it, so a fresh install with zero real mods configured showed an empty table instead of the empty state. Scoped the COUNT query to mid > 0 to match.
  • Mobile cards: truncate + title on the modfolder line so a long folder name can't push the SU badge off narrow screens.

feat(admin-groups): reflect exhausted select-all/select-none state

Port of upstream sbpp#1573's tri-state "select all" checkbox for the web-group permission flag grid — adapted to this fork's existing two-button shape (sbpp#1436) instead of swapping in a single <input type="checkbox">.

A straight swap to upstream's checkbox would have thrown away the explicit Select-all/Select-none affordance and forced re-proving two things the existing admin-groups-select-all-flags.spec.ts spec already locks in against the button pair: the unsigned bit-31 OR-fold guard (sbpp#1272) and the dirty-tracker-arming contract in SbppGroupsToggleAllFlags. Neither comes for free from a native checkbox's indeterminate state.

Instead, both buttons now reflect the grid's current state via aria-disabled + a dimmed style once nothing is left to do — not the native disabled attribute, because SbppGroupsToggleAllFlags is already a safe no-op on a fully-selected grid and the existing spec deliberately re-clicks "Select all" in that state as an idempotency check; a truly disabled button can't be clicked at all (Playwright would hang on actionability). Wired into the three places the grid's checked state can change without a manual checkbox click: the change listener, the master-detail paintGroup() repaint, and a bootstrap call for the SSR-rendered initial state.

Extended the existing E2E spec with aria-disabled assertions at each state transition, including after a page reload to prove the state re-derives from the SSR-checked grid rather than JS memory.

Not included: item 6 (Add Admin server-access grid, upstream sbpp#1491)

Upstream's fix caps a checkbox-tile grid (.admin-server-access-grid + [data-testid="server-tile"] cards) at two columns so long hostnames wrap instead of being clipped. This fork's Add Admin "Individual servers" picker was already migrated off that pattern onto a themed data-multiselect <select multiple> (hostname hydration via option[data-server-host], same shape as the admin-search box) — there is zero trace of .admin-server-access-grid / [data-testid="server-tile"] left in page_admin_admins_add.tpl or theme.css. The column-width problem the upstream CSS fixes doesn't exist in a scrollable dropdown, so there's nothing to backport here.

Verification

  • phpstan analyse (DBA plugin disabled, matching the documented offline recipe): clean — the only reported item is the known unrelated install/pages/page.6.php baseline artifact from running without DBA.
  • phpunit --filter "ModsDeleteDialogTest|RoutingTest|AdminPagesQueryCountTest|WebGroupsCatalogTest|ButtonClassChainTest|DeadJsCallSitesTest": all green.
  • New inline JS in page_admin_groups_list.tpl syntax-checked with node -c after extracting the {literal} block.
  • E2E (admin-groups-select-all-flags.spec.ts, mods-add-form.spec.ts, mod-delete-confirm.spec.ts) not run in this environment (no Playwright browser install here) — CI's e2e.yml gate covers it; the modified spec only adds assertions to an existing passing flow, no existing assertions were changed.

Test plan

  • CI: PHPStan / PHPUnit / ts-check / api-contract / E2E all green.
  • Manually load ?p=admin&c=mods&section=list with zero configured mods on a fresh-ish install and confirm the new empty-state card (icon + "Add mod" CTA) renders instead of an empty table.
  • Add a mod whose name contains & or < and confirm it renders correctly (not as &amp;) in both the desktop table and the mobile card.
  • On ?p=admin&c=groups&section=list, select a group, click "Select all", confirm the button visibly dims and a second click is still possible (idempotent) but a screen reader announces it as unavailable (aria-disabled="true").

🤖 Generated with Claude Code

Rushaway and others added 2 commits September 22, 2026 10:53
…pty state

Port of upstream sbpp#1552, continuing our earlier
feat/mods-list-compact-actions (#14) port before upstream's own
follow-up review passes landed.

- Mod name/folder/icon path are stored entity-encoded, but the
  template rendered them with Smarty's default auto-escape on top,
  so a mod whose name contains e.g. an ampersand painted the literal
  `&amp;` in the admin list instead of `&`. Add `|unescape:'html'`
  at every text and attribute sink (desktop table, mobile cards,
  edit/delete aria-labels and data-name attributes) to decode once
  before Smarty's own escape reapplies it safely.
- Replace the ad-hoc "No mods configured yet." paragraph with the
  shared `.empty-state` pattern (icon + title + body + permission-
  gated CTA) per the "Empty states" convention in AGENTS.md, and add
  the missing `$permission_addmods` property so the CTA can gate on
  it.
- `$mod_count` counted mid=0 (the reserved "Web" pseudo-mod every
  install carries) even though `$mod_list` always excludes it, so a
  fresh install with zero real mods configured showed an empty table
  instead of the empty state. Scope the COUNT query to `mid > 0` to
  match.
- Mobile cards: give the modfolder line `truncate` + a `title`
  attribute and move the SU badge to a `flex-shrink:0` sibling
  instead of letting a long folder name push it off narrow screens.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Port of upstream sbpp#1573's tri-state "select all"
checkbox for the web-group permission flag grid, adapted to this
fork's existing two-button shape (sbpp#1436) instead of swapping in a
single <input type="checkbox">.

A straight swap to upstream's checkbox would have thrown away the
explicit Select-all/Select-none affordance and forced re-proving two
things the sbpp#1436 spec (admin-groups-select-all-flags.spec.ts) already
locks in against the button pair: the unsigned bit-31 OR-fold guard
(sbpp#1272, SbppFoldFlags' `>>> 0`) and the dirty-tracker-arming contract
in SbppGroupsToggleAllFlags. Neither is exercised for free by a native
checkbox's indeterminate state.

Instead, both buttons now reflect the grid's current state via
aria-disabled + a dimmed style once nothing is left to do — not the
native `disabled` attribute, because SbppGroupsToggleAllFlags is
already a safe no-op on a fully-selected grid and the existing spec
deliberately re-clicks "Select all" in that state as an idempotency
check; a truly disabled button can't be clicked at all.
SbppGroupsRefreshSelectAllButtons() is wired into the three places the
grid's checked state can change: the existing `change` listener
(manual clicks and the bulk toggle's own dispatched event), the
master-detail paintGroup() repaint (which sets .checked directly with
no change event), and a bootstrap call for the SSR-rendered initial
state.

Extends the existing E2E spec (rather than adding a new file) with
aria-disabled assertions at each state transition, including after a
fresh page reload to prove the state is re-derived from the SSR-
checked grid rather than carried in JS memory.

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

Copy link
Copy Markdown
Collaborator

Let's fix tests before merging

SbppGroupsToggleAllFlags dispatches its synthetic change event on the
flag grid itself, which the grid listener filters out (it only reacts to
input[name="flags[]"] targets). The Select all / Select none
aria-disabled state was therefore never refreshed after a bulk toggle,
failing admin-groups-select-all-flags.spec.ts. Refresh explicitly.
Once every flag is checked the button carries aria-disabled="true",
which Playwright's actionability check treats as disabled, so the plain
click() hung until the 30s timeout. Force the click (a mouse user can
still press it, it has no native disabled) and correct the template
docblock that claimed aria-disabled kept it clickable for Playwright.
@Rushaway
Rushaway merged commit d7d2456 into main Sep 26, 2026
6 checks passed
@Rushaway
Rushaway deleted the fix/upstream-admin-ui-polish branch September 26, 2026 13:20
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.

3 participants