Skip to content

feat(web): multi-org membership UI — org switcher, add-to-org form, member list - #1442

Merged
zbigniewsobiecki merged 1 commit into
devfrom
feat/mng-1674-multi-org-ui
Jun 25, 2026
Merged

feat(web): multi-org membership UI — org switcher, add-to-org form, member list#1442
zbigniewsobiecki merged 1 commit into
devfrom
feat/mng-1674-multi-org-ui

Conversation

@aaight

Copy link
Copy Markdown
Collaborator

Multi-org membership · 4/4 UI

Spec 021, plan 4 of 4 — the user-facing layer over the curl-testable backend shipped in plans 2–3 (touches web/ only).

Issue: https://linear.app/mongrel/issue/MNG-1674/multi-org-membership-44-ui-org-switcher-add-to-org-form-member-list (MNG-1674)

What's in this PR

1. Active-org switcher (sidebar/nav)

  • New OrgSwitcher renders for non-superadmin members who belong to more than one org, backed by auth.listMyOrgs (the membership list) + auth.setActiveOrg (writes the session's active_org_id server-side).
  • On switch it calls queryClient.invalidateQueries() so every org-scoped query — and auth.me — refetches against the new active org (which computeEffectiveOrgId now resolves from the session). The selected org is read from auth.me.effectiveOrgId.
  • Single-org users see no switcher — just an inert org-name banner (OrgNameBanner). (spec AC fix: improve PostgreSQL startup reliability and agent documentation #9)
  • The existing superadmin cross-org switcher (x-org-context header, all orgs) is preserved unchanged. (spec AC ci: add GitHub Actions for CI and Fly.io deployment #7) Superadmins never mount the membership switcher (their active_org_id is intentionally ignored by the backend).

2. "Add existing account to this org" form

  • New AddToOrgDialog on Settings → Users (a secondary "Add existing account" button next to "New User") calls users.addExistingUserToOrg.
  • Surfaces the NOT_FOUND envelope inline ("No account exists with this email…") and a success banner distinguishing a fresh grant from an idempotent re-grant (role update). (spec AC build(deps): Bump esbuild, @vitest/coverage-v8 and vitest #1)
  • The complementary CONFLICT envelope (creating a user whose email already exists, which points the admin at add-to-org) is already surfaced inline by the existing create dialog — no change needed there.

3. Membership-based member list

Spec AC mapping

Design notes

  • The genuinely logic-bearing pieces are extracted into pure helpers (shouldShowOrgSwitcher, resolveActiveOrgName, formatAddToOrgSuccess, describeMemberRow, roleVariant) and SSR-safe presentational components (OrgSwitcherView, AddToOrgForm) built on NativeSelect, so they unit-test under renderToStaticMarkup in the node test env. The hook-wired containers (OrgSwitcher, AddToOrgDialog) stay thin.
  • No backend contract changed — this PR only consumes existing tRPC endpoints from plans 2–3.

Testing

  • tests/unit/web/org-switcher.test.ts — switcher decision + view (multi/single/zero org, pending, active-org marking)
  • tests/unit/web/add-to-org-dialog.test.ts — success-message helper + form rendering (NOT_FOUND inline, success banner, pending)
  • tests/unit/web/users-table-membership.test.tsdescribeMemberRow + roleVariant
  • 23 new tests; full web suite (585) green. Root + web typecheck clean; biome check . clean; test:fast (pre-push) green.

🤖 Generated with Claude Code

🕵️ claude-code · claude-opus-4-8 · run details

…ember list
Spec 021 plan 4 (MNG-1674): the user-facing layer over the curl-testable
backend from plans 2-3 (web/ only).
- Active-org switcher (sidebar) for non-superadmin multi-org members, backed
by auth.listMyOrgs + auth.setActiveOrg; invalidates every query on switch so
the dashboard refetches against the new active org. Single-org users get an
inert org-name banner (spec AC #9). Superadmin cross-org switching via
x-org-context is unchanged (spec AC #7).
- Add-existing-account dialog on Settings → Users calling
users.addExistingUserToOrg, surfacing the NOT_FOUND envelope inline; the
complementary CONFLICT from users.create is already shown inline by the
create dialog (spec AC #1).
- Member list now renders per-org role alongside the account role plus a Guest
badge for cross-home members (spec AC #5).
Pure helpers (shouldShowOrgSwitcher, resolveActiveOrgName, formatAddToOrgSuccess,
describeMemberRow) and SSR-safe presentational components (OrgSwitcherView,
AddToOrgForm) are unit-tested; hook-wired containers are thin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@nhopeatallnhopeatall left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

APPROVE — clean, well-scoped web/-only PR that consumes existing plan 2–3 tRPC endpoints. I verified the frontend/backend contracts, the org-switch resolution path, and ran the new tests + typecheck + lint locally (all green).

What I verified

  • Switcher actually works for members.computeEffectiveOrgId (src/api/context.ts) resolves a non-superadmin's effective org from the session's active_org_id and ignores the x-org-context header. The existing OrgProvider still emits x-org-context: <home org> for members, but since the backend ignores it for non-superadmins there is no conflict — auth.setActiveOrg + invalidateQueries() correctly re-scopes the whole dashboard. Confirmed OrgSwitcher is never mounted for superadmins (the isSuperadmin branch in sidebar.tsx always returns first), preserving AC #7.
  • Data shapes match.MyOrg {id,name,role} matches listOrgMembershipsForUser; AddToOrgResult {email,role,alreadyMember} matches addExistingUserToOrg's return; describeMemberRow maps OrgMember {role,globalRole,isGuest} correctly; the NOT_FOUND copy matches the server envelope. colSpan correctly bumped 5→6 for the new "Org role" column.
  • Tests/typecheck/lint. 23 new tests pass; webtsc --noEmit clean; biome check clean on all changed files; CI 7/7 green.

Minor (non-blocking)

  • The hook-wired containers (OrgSwitcher, AddToOrgDialog) are intentionally untested — only the pure helpers and SSR-safe views are covered. Reasonable tradeoff given the node test env, and the container logic (the orgId !== activeOrgId guard, invalidation) is trivial. No action needed.
  • Defensive-only edge: if a multi-org member ever lacked a membership row for their home org (the "pre-backfill safety" path noted in computeEffectiveOrgId), effectiveOrgId would be the home org but listMyOrgs wouldn't include it, so the <select value> wouldn't match an option. Not reachable post plan-1 backfill (and users.create always creates a home membership), so mentioning only for awareness.

LGTM.

🕵️ claude-code · claude-opus-4-8 · run details

@zbigniewsobiecki
zbigniewsobiecki merged commit ecee78a into devJun 25, 2026
9 checks passed
Sign up for freeto 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

@aaight@nhopeatall@zbigniewsobiecki