feat(privileges): add "All Categories" aggregate view to admin privileges page#14303
Open
palmoni5 wants to merge 3 commits into
Open
feat(privileges): add "All Categories" aggregate view to admin privileges page#14303palmoni5 wants to merge 3 commits into
palmoni5 wants to merge 3 commits into
Conversation
Add an "All Categories" entry at the bottom of the admin privileges
category selector that shows an aggregated view of category-scoped
privileges across every category. Checkboxes display the combined
state per privilege/member:
- checked granted in every category
- empty granted in no category
- indeterminate granted in some categories only ("mixed")
Toggling a checkbox in this view applies the change to every
category at once. Rows that inherit from a parent group in the mixed
state are also marked indeterminate so the effective inheritance is
visible.
Implementation
- privileges.categories.listAll() aggregates membership across all
cids; new helpers (getGroupPrivilegesAll / getUserPrivilegesAll)
batch the underlying set reads in privileges/helpers.js.
- API getPrivileges/setPrivilege handle the cid="all" sentinel and
fan the give/rescind out over every cid.
- spawnPrivilegeStates only marks a checkbox 'checked' when the
state is literally true, leaving "mixed" rendered as
indeterminate via exposeMixedPrivileges().
- categorySearch supports an appendCategories option so the
"All Categories" entry sits after the real category list.
Safety
- copyPrivilegesFrom now rejects a non-numeric source/target cid,
preventing the bulk-revoke that would otherwise occur when copying
from the "all" pseudo-cid (fromChecks would be all-false).
- The per-row copy dropdown and the footer copy buttons are hidden
in the "All Categories" view; the client also guards the copy
actions defensively.
Contributor
💡 Friendly NoteThis pull request was made against the
Thanks! |
Contributor
Author
|
@barisusakli I would be happy to receive a response from you. |
Member
|
I will have to review this but I like the idea of having a single page to apply the change to all categories. |
barisusakli
reviewed
Jun 3, 2026
barisusakli
reviewed
Jun 3, 2026
barisusakli
reviewed
Jun 3, 2026
Address review feedback on NodeBB#14303: benchpress supports != so the empty if-branch + else pattern can be collapsed into a single if. No behavior change.
Per review on NodeBB#14303: placing the aggregate entry at the bottom hides it when the forum has many categories. Move it back to the head of the local-prepended list and drop the now-unused appendCategories plumbing in categorySearch.js (no diff vs upstream there now).
Contributor
Author
|
All are corrected. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an All Categories entry at the bottom of the category selector on the admin privileges page (
/admin/manage/privileges). When selected it shows an aggregated view of category-scoped privileges across every category, with a third "mixed" state for privileges granted in some categories but not others. Toggling a checkbox in this view applies the change to every category at once.Motivation
Administering privileges across many categories today means either picking categories one by one or using the per-row Copy to all categories action — which silently overwrites every existing per-category difference. There is no way to see, at a glance, which privileges are uniformly granted/denied and which are inconsistent, and no way to make a coarse change ("give group X this privilege everywhere") without losing per-category nuance elsewhere.
Behavior
For each privilege × member (group or user), the aggregated state is one of:
truecheckedfalsemixedindeterminateClicking a
mixedcheckbox once turns it on for every category (or off, depending on the resulting state) — it is always a real change, never a no-op. Rows that inherit from a parent group in themixedstate are themselves markedindeterminate, so the effective inheritance is visible rather than rendering as plainfalse.Implementation notes
privileges.categories.listAll()(new) aggregates membership across all cids. Two new helpers insrc/privileges/helpers.js(getGroupPrivilegesAll/getUserPrivilegesAll) batch the underlyinggetMembersOfGroupsreads — one call per privilege × cid, then in-memory aggregation — instead of N round-trips per category.getPrivileges/setPrivilegehandle thecid === 'all'sentinel; the latter fansgive/rescindout over every cid via the existing array-cid path ofhelpers.giveOrRescind.spawnPrivilegeStatesmarks a checkboxcheckedonly when the state is literallytrue, somixedrenders unchecked and is then promoted toindeterminateclient-side by a newPrivileges.exposeMixedPrivileges(). The mixed pass runs beforeexposeAssumedPrivileges()so inheritance can see indeterminate parents as granted;getPrivilegesFromRownow also picks upindeterminatecheckboxes for this reason.categorySearchgains anappendCategoriesoption (mirroringlocalCategories) that places entries after the API search results, so the All Categories pseudo-entry sits at the bottom of the dropdown rather than at the top.Safety
categories.copyPrivilegesFromnow rejects a non-numericfromCid/toCid. With the previous code, copying from an invalid source producedfromChecks = [false, false, …], which madecopyPrivilegesByGroupinterpret every privilege the target currently held as "in target but not in source" — i.e. every such privilege would be rescinded. The guard turns that into[[error:invalid-data]]. (This also closes the same hazard against a direct socket call, not just the UI button.)Files changed
src/privileges/helpers.js,src/privileges/categories.js,src/api/categories.js,src/controllers/admin/privileges.js,src/categories/create.jssrc/views/admin/partials/privileges/category.tplpublic/src/admin/manage/privileges.js,public/src/modules/categorySearch.js,public/src/modules/helpers.common.jspublic/language/en-GB/admin/manage/privileges.json(all-categories,alert.copy-disabled-all)Out of scope / non-goals
cid > 0) privileges, mirroring the scope of the existing per-category view.Manual verification
listAll()returns groups withstate ∈ {true, false, 'mixed'}once at least one category has a differing privilege.PUT/DELETE /api/v3/categories/all/privileges/:privilegecall which fans out across cids.cid === 'all'is blocked by both the UI (buttons hidden) and the server-side guard incopyPrivilegesFrom.