Nasaq
Components

MembersManager

Members and roles of a workspace. A searchable table with an inline role select limited to the roles you can grant, an invite by email dialog, pending invites with resend and revoke, transfer of ownership and leave, with the last owner protected.

PreviewOpen ↗

Code

<MembersManager  members={members}  invites={invites}  roles={[    { id: "owner", label: "Owner" },    { id: "admin", label: "Admin", description: "Manage members and settings" },    { id: "member", label: "Member" },  ]}  currentUserId="u1"  grantableRoles={["admin", "member"]}  onInvite={({ emails, role }) => api.invite(emails, role)}  onChangeRole={(m, role) => api.setRole(m.id, role)}  onRemove={(m) => api.remove(m.id)}  onResendInvite={(i) => api.resend(i.id)}  onRevokeInvite={(i) => api.revoke(i.id)}  onTransferOwnership={(m) => api.transfer(m.id)}  onLeave={() => api.leave()}/>

Collaboration · beta

Live examples and controls: MembersManager in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/members-manager.json

The "Members" page of a workspace or team. People and their roles in a table, an invite dialog that takes several email addresses and one role, the invites still waiting, and the two ownership moves: transfer to someone else, or leave. The rules for the last owner live in pure functions, so the UI and your server agree.

When to use

  • A team or organisation settings page where owners and admins manage who is in.
  • Any product with roles per workspace (owner, admin, member, viewer).

When not to use

  • Managing every user of the whole product as an operator: use AdminUsers.
  • Choosing which workspace you are in: use WorkspaceSwitcher.

Import

import { MembersManager } from "@fadymondy/nasaq/web";

Quick start

<MembersManager
  members={members}
  invites={invites}
  roles={[
    { id: "owner", label: "Owner" },
    { id: "admin", label: "Admin", description: "Manage members and settings" },
    { id: "member", label: "Member" },
  ]}
  currentUserId="u1"
  grantableRoles={["admin", "member"]}
  onInvite={({ emails, role }) => api.invite(emails, role)}
  onChangeRole={(m, role) => api.setRole(m.id, role)}
  onRemove={(m) => api.remove(m.id)}
  onResendInvite={(i) => api.resend(i.id)}
  onRevokeInvite={(i) => api.revoke(i.id)}
  onTransferOwnership={(m) => api.transfer(m.id)}
  onLeave={() => api.leave()}
/>

Anatomy

MembersManager           data-slot="members-manager"
├─ Alert                    outcome of the last action
├─ Tabs                     Members / Pending invites (the tab shows only when `invites` is passed) + Invite button
│  ├─ DataTable             member, role select, last active, joined; row actions transfer / remove
│  └─ list                  pending invites: email, role, expiry, Resend, Revoke
├─ leave row                data-slot="members-leave"
├─ InviteMembersDialog      emails as chips (TagInput) + role
└─ AlertDialog              confirms remove, transfer and leave

API

MembersManager

PropTypeDefaultDescription
membersTeamMember[]required{ id, name, email, avatar?, role, joinedAt, lastActive? }.
invitesPendingInvite[]none{ id, email, role, invitedBy?, sentAt, expiresAt? }. Passing it shows the Pending invites tab.
rolesMemberRoleOption[]required{ id, label, description? } in display order.
currentUserIdstringnoneMarks "You"; that row cannot be removed here.
grantableRolesstring[]every role except the owner roleThe role ids the signed-in person may give. The select shows only these plus the member's current role.
ownerRolestring"owner"The role id that means owner.
canManagebooleantruefalse hides invite and locks every control.
loadingbooleanfalseSkeleton rows.
pageSizenumber8
onInvite(values: { emails, role }) => Promise<void | { error?, emailsError? }>noneShows the Invite button.
onChangeRole(member, role) => Promise<void | { error? }>noneTurns the role cell into a select.
onRemove(member) => Promise<void | { error? }>noneRow action.
onResendInvite / onRevokeInvite(invite) => Promise<void | { error? }>none
onTransferOwnership(member) => Promise<void | { error? }>noneRow action, only for owners.
onLeave() => Promise<void | { error? }>noneShows the leave row.
profile`(member) => PersonProfilenullundefined`
profileActions{ onMessage?, onMention?, onViewProfile?, viewerTimeZone? }noneQuick actions and your time zone for those cards.
labelsMembersManagerLabelsen / ar

Resolve { error } (or throw) to show a failure; the dialog stays open. You send back new members and invites; the component keeps no copy.

Rules (pure, exported)

ownerCount, isLastOwner, roleChoices, roleChangeBlock, removeBlock, canLeave. The last owner cannot be demoted, removed or leave; the owner role is reachable only through transfer.

Examples

  • Read-only member view: canManage={false}.
  • An admin who can only grant member and viewer: grantableRoles={["member", "viewer"]}; owners and admins show a plain badge.

Accessibility

The role select has an accessible name per member. A locked role shows a focusable badge with a tooltip that says why. Destructive actions ask in an alert dialog; outcomes are announced in a status alert.

RTL & i18n

Built-in English and Arabic. Emails stay left-to-right; dates and counts use the locale digits.

Styling & tokens

Built from Nasaq parts (DataTable, Select, Badge, Alert), so it follows the tokens. Extend with className.

Do / Don't

  • Do enforce the same rules on your server; the UI only hides the options.
  • Do not offer the owner role in the invite dialog: ownership moves by transfer.

Lab

https://docs.nasaqui.com/?path=/docs/components-collaboration-members-manager--docs

On this page