Members
See who's on your workspace, invite new teammates, and remove members. Admin actions are gated by the `is_admin` prop.
The Members page lives at /app/members. It shows the current workspace's members and any pending invitations. Admin actions are gated.
Page structure
Top of the page:
-
— the current workspace's name (fromcurrent_account.nameon shared props) - Subtitle: "Manage members and invitations."
Sections that follow (in order):
-
Invite a member card — only if
is_adminis true - A horizontal separator
- Members ({count}) section — always present
- A second horizontal separator + Pending Invitations ({count}) section — only if there is at least one invitation
Invite a member (admin-only)
Shown only when is_admin === true. Card title: Invite a member. Description: "Send an invitation to join this account."
┌─ Invite a member ─────────────────────────────────────────┐
│ Send an invitation to join this account. │
│ │
│ Email Name (optional) │
│ [ colleague@example.com ] [ Jane ] [Invite] │
└───────────────────────────────────────────────────────────┘
Fields:
-
Email —
type="email", required, placeholdercolleague@example.com -
Name (optional) — placeholder
Jane
Submit button: Invite. While the request is in flight: "Sending...". Posts to POST /app/members with an invitation payload.
Server responses (verified from MemberController.create/2):
-
Success: flash "Invitation sent!", redirect to
/app/members -
Already a member of this account: flash "This user is already a member of this account.", redirect to
/app/members -
Any other changeset error: flash "Could not send invitation.", redirect to
/app/members
Members list
Section heading: Members ({count}). One card per member:
-
Primary label:
{first_name} {last_name} -
Below:
{email}muted -
A rounded pill — Admin when
member.roles?.adminis truthy, Member otherwise -
A Remove button (ghost, destructive-colored) — shown only when
is_admin === true
Clicking Remove calls confirm("Remove this member from the account?"). Confirming fires DELETE /app/members/{member.user_id}.
Pending Invitations
Only rendered when invitations.length > 0. Heading: Pending Invitations ({count}). One card per invitation:
-
Primary label:
inv.nameif present, otherwiseinv.email -
Below (only when
inv.nameis present):inv.emailmuted - A muted Pending pill
-
A Cancel button (ghost) — shown only when
is_admin === true
Clicking Cancel calls confirm("Cancel this invitation?"). Confirming fires DELETE /app/members/{inv.id}.
Delete endpoint (shared by members and invitations)
Verified from MemberController.delete/2: there is one DELETE /app/members/:id route. The controller passes the ID straight to Accounts.remove_account_member(account.id, id) — the same function resolves both user IDs and invitation IDs. After removal, the flash is always the same:
Member removed.
followed by a redirect to /app/members. So even when you cancel an invitation, the confirmation flash reads "Member removed.".
Roles
The JSX reads member.roles?.admin. When truthy it shows Admin, otherwise Member. There are no other role names surfaced on this page.
Related
- Invitations — the invited user's side of the flow
- Workspaces — how switching between workspaces works