fix(webapp): only offer assignable roles in the team role picker - #4888
Draft
claude[bot] wants to merge 2 commits into
Draft
fix(webapp): only offer assignable roles in the team role picker#4888claude[bot] wants to merge 2 commits into
claude[bot] wants to merge 2 commits into
Conversation
The team page's role dropdown listed every role the org's plan allows, including roles above the viewer's own level. Picking one of those was always rejected, so the dropdown advertised a change that could not happen. Narrow the list to the roles the viewer's own role lets them assign, reusing the same ladder the invite flow already applies. Plan-locked roles are kept in the list and still render as "Name (upgrade)" linking to billing: being unable to assign a role and needing a plan upgrade to assign it are different situations, and collapsing them would offer a viewer an upgrade for a role their own role would still not let them hand out. The member's current role stays listed so the dropdown keeps showing what they actually hold. TeamPresenter now returns `offerableRoleIds` (the ladder) alongside the existing `assignableRoleIds` (the plan), and the invite loader reuses both instead of re-querying — its dropdown has no upgrade affordance, so it keeps intersecting the two. Co-Authored-By: Claude <noreply@anthropic.com>
|
The previous commit narrowed the picker with `isAtOrBelow`, which only answers "yes" for roles that have a position on the system-role ladder. Org-defined custom roles have no position, so they were filtered out for everyone, and a viewer holding a custom role — no position either — was offered nothing at all, leaving them with a dropdown containing only the role each member already had. Neither was asked for. Narrow subtractively instead: drop a role only where the ladder places it strictly above the viewer, which is the only case picking it would always be rejected. A new `isAbove` expresses that, and it is false whenever either side is off the ladder, so custom roles stay offerable exactly as before and an off-ladder or roleless viewer is not narrowed at all — being unable to manage members is worse than being offered a role the server may refuse. Plan-locked roles still render as "Name (upgrade)": the offerable set and `assignableRoleIds` remain separate. `offerableRoleIds` is no longer the invite flow's rule, so the invite loader goes back to applying `isAtOrBelow` itself — ladder-only, then intersected with the plan — which is the set it computed before this branch. Its action was already doing its own check and is untouched. Co-Authored-By: Claude <noreply@anthropic.com>
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.
Requested by Matt Aitken · Slack thread
Before / After
Before. The role dropdown next to each member on the Team page listed every role the organization's plan allows — including roles above the viewer's own. A viewer who is not allowed to hand out the Owner role was still shown "Owner", and selecting it came back as a permission error. The dropdown advertised a change that could never happen.
After. Roles above the viewer's own level on the system-role ladder are not in the list at all, so there is nothing to click and nothing to be rejected for. Every other role is listed exactly as before: organization-defined custom roles are untouched, and a role the viewer could assign but the organization's plan does not include still appears as
Name (upgrade), linking to the plan page. Each member's current role also stays in the list, so the dropdown keeps showing the role they hold even when the viewer could not have assigned it themselves.How
Two separate sets now drive the picker, and keeping them separate is the point:
offerableRoleIds— the role catalogue minus the roles the system-role ladder (apps/webapp/app/utils/inviteRoleLadder.ts) places strictly above the viewer. Anything in that set is filtered out of the rendered list.assignableRoleIds— the roles the organization's plan allows, unchanged. Inside the offerable list, a role that is not plan-assignable still renders as the(upgrade)row linking to billing.Merging the two would show a viewer "Owner (upgrade)" — an invitation to pay for something their own role still would not let them do — so
TeamPresenterreturns the offerable set as its own field rather than intersecting it into the plan set.The invite flow keeps its own, stricter rule:
isAtOrBelowin the same file, which requires a ladder position on both sides and refuses anything else, intersected with the plan set. That loader and action are unchanged by this PR.✅ Checklist
Testing
pnpm run format— clean, all 4322 matched files already correctly formatted.pnpm run lint(oxlint) — clean.pnpm run knip— clean (only the pre-existinglefthookconfiguration hint).pnpm run typecheck --filter webapp— 20 tasks successful, 20 total.pnpm exec vitest run test/inviteRoleLadder.test.ts(inapps/webapp) — 12 tests passed. TheofferableRoleIdscases cover: the viewer's own level and below is offered, roles above the viewer are left out entirely, a top-of-ladder viewer gets the whole catalogue, plan-locked roles are still returned so the caller can render the upgrade row, custom roles are kept for viewers at every ladder level, and a viewer holding a custom role — or no role, or an org with no ladder at all — is not narrowed at all. TheisAtOrBelowcases for the invite flow's stricter rule are unchanged.The webapp's vitest setup has no DOM environment configured, so
RolePickeritself is not covered by a rendering test; the filtering rule it applies is unit-tested through the extracted helper instead.Changelog
The Team page's role dropdown no longer lists roles above your own, which were rejected when you picked them. Every other role you could pick before is still there, and roles that need a plan upgrade still appear with a link to upgrade.
Screenshots
Not captured — the change is to which rows the existing dropdown lists; its appearance is unchanged.