Skip to content

feat(groups): add 'Select all' to web group permission flags - #24

Open
Rushaway wants to merge 2 commits into
mainfrom
fix/issue-1436
Open

feat(groups): add 'Select all' to web group permission flags#24
Rushaway wants to merge 2 commits into
mainfrom
fix/issue-1436

Conversation

@Rushaway

@Rushaway Rushaway commented Sep 3, 2026

Copy link
Copy Markdown
Member

What

Ports upstream issue sbpp#1436 to this fork.

On Group Management -> Web admin groups, the permission flag grid
had no bulk control, so operators had to tick every flag individually.

This adds two ghost buttons next to the Permission flags label in
the groups-list master-detail editor:

  • Select all — checks every enabled flags[] checkbox
  • Select none — clears them

Both refresh the live bitmask preview (SbppFoldFlags), and the folded
OR-sum round-trips through Save exactly like a manual toggle. The
buttons are gated on permission_editgroup, matching the checkboxes
themselves, and disabled checkboxes are left untouched.

Changes

  • web/themes/default/page_admin_groups_list.tpl — buttons + a small
    SbppGroupsToggleAllFlags(checked) helper.
  • web/tests/e2e/specs/flows/admin-groups-select-all-flags.spec.ts
    new flow spec covering select-all, save round-trip, and select-none.

Notes

Fork's current code already handled the unsigned-bitmask concern
(sbpp#1272); this change is purely additive UI. No SourcePawn changes; no
local SP compiler, relying on CI.

🤖 Generated with Claude Code

Rushaway and others added 2 commits September 3, 2026 10:54
Porting upstream issue sbpp#1436: operators had to tick
every web-admin-group permission flag individually. Adds two ghost
buttons next to the "Permission flags" label on the groups list
master-detail editor that bulk-toggle every enabled checkbox in the
flag grid and refresh the live bitmask preview. The folded OR-sum
round-trips through Save unchanged. Buttons are gated on
permission_editgroup, matching the checkboxes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bulk-toggle review follow-ups for the sbpp#1436 Select all / Select none
buttons on the web-group permission flag grid.

- Arm the master-detail dirty tracker. `SbppGroupsToggleAllFlags` set
  `input.checked` from script, which fires no `change` event, so the
  form-level `markDirty` listener never ran. A left-rail click after a
  bulk toggle therefore repainted the pane and silently dropped the
  operator's change instead of raising the "Unsaved changes" confirm --
  a data-loss path a manual checkbox click does not have. The helper now
  dispatches one bubbling `change` from the grid, and short-circuits when
  nothing actually changed so a no-op press does not arm the guard.
- Give both buttons an `aria-label` so the accessible name says which
  list "Select all" applies to.
- Harden the spec: the expected bitmask is now recomputed in-spec from
  `data-flag-value` with non-bitwise arithmetic (an independent oracle
  instead of the page's own fold), asserted exactly rather than as
  "not 0", and re-asserted after the Save + reload round-trip with every
  checkbox verified -- select-all sets bit 31, so this is the sbpp#1272
  unsigned-fold path. Adds `type="button"` assertions, an idempotency
  press, and a second test covering the unsaved-changes guard.

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

Rushaway commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Review — approve with fixes pushed (7ffb12f)

Reviewed the feature against upstream sbpp#1436, the whole page_admin_groups_list.tpl, SbppFoldFlags / SbppGroupsSave, web/pages/admin.groups.php, and api_groups_edit. The approach is right and the gating is correct; I found one real bug and pushed a fix plus a hardened spec.

Bug found: a bulk toggle did not arm the unsaved-changes guard

SbppGroupsToggleAllFlags set input.checked from script. A programmatic .checked = fires no change event, so the master-detail markDirty listener (form.addEventListener('change', markDirty), further down the same page tail) never ran. Consequence: press Select all, then click another group in the left rail — the selection handler saw dirty === false, called paintGroup() straight away, and the operator's bulk change vanished with no "Unsaved changes" confirm. Ticking the same 30 boxes by hand does raise the prompt, so this was a behavioural asymmetry the button introduced.

Fixed by dispatching one bubbling change from the grid after the loop (the grid's own preview listener ignores non-checkbox targets by design, so the preview is still repainted explicitly through SbppFoldFlags). The helper now also skips checkboxes already in the target state and returns early when nothing changed, so a no-op press doesn't arm the guard spuriously.

Also added aria-label="Select all permission flags" / "Select no permission flags" — "Select all" alone doesn't say which list it applies to out of context, and the label is a superset of the visible text so label-in-name still holds.

What checked out as correct in the original patch

  • Gate. {if $permission_editgroup} is the same flag the checkboxes use for disabled (line 179) and the Save block uses (line 188), and AdminGroupsListView already carries it — no view change needed. Belt-and-braces input.disabled skip in the helper is the right defensive layer.
  • Real buttons. <button type="button"> inside <form data-testid="group-detail"> — no accidental submit, no <a href="#">.
  • Unsigned 32-bit. $all_flags strips ALL_WEB and ADMIN_OWNER (admin.groups.php L214-236), but the remainder still includes ADMIN_UNBAN_GROUP_BANS (bit 31), so select-all folds to 4278189567 — squarely the Web admin groups bitmask preview goes negative when high-bit flags toggled — JS |= returns Int32 sbpp/sourcebans-pp#1272 case. Routing the repaint through the shared SbppFoldFlags (>>> 0) is exactly right; no sign or precision bug, and SbppGroupsSave folds the same helper over the form so Save round-trips the same value.
  • Escaping / Smarty. No new interpolation, no new nofilter, {if}/{/if} balanced, and the JS lives inside the existing {literal} block so the { bubbles: true } object literal is safe.
  • No-JS. Not a regression: admin.groups.php never reads $_POST, so the whole master-detail editor (Save included) is already JS-only. The SSR bitmask preview is untouched.
  • Scope. [data-testid="flag-grid"] is unique — page_admin_groups_add.tpl deliberately has no flag grid (it points at the List tab), and page_admin_overrides.tpl's override_flags[] is a text input, so there's no parity gap and no selector collision.

Spec hardening

The original spec asserted the badge was "not 0 bitmask" and, after the Save reload, only that the first checkbox was checked — weak enough that a partial fold or a wrong-but-nonzero value would pass. Rewritten to:

  • recompute the expected bitmask in-spec from each data-flag-value using non-bitwise arithmetic (an independent oracle rather than re-running the page's own fold), assert the badge equals it exactly, and re-assert it after the Save + reload round-trip with every checkbox verified;
  • guard the guard: expect(expected).toBeGreaterThanOrEqual(2 ** 31) fails loudly if a future web.json edit quietly drops the bit-31 flag and stops exercising the Web admin groups bitmask preview goes negative when high-bit flags toggled — JS |= returns Int32 sbpp/sourcebans-pp#1272 path;
  • assert type="button" on both controls, and that a second Select all press is idempotent;
  • add a second test seeding two groups that presses Select all, clicks the other group, and asserts the shared [data-testid="sbpp-confirm-dialog"] opens with "Unsaved changes" and that cancelling keeps the selection — the regression test for the bug above.

Both tests stay desktop-chromium-only with truncateE2eDb() per test, matching admin-groups-bitmask.spec.ts.

CI

All green on 7ffb12f: PHPUnit, Playwright + axe, Static analysis, tsc --checkJs.

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