Skip to main content

Team Members Interface

The Members UI provides work-level team management, allowing work owners and managers to invite collaborators, assign roles, update permissions, and remove members. Access control is enforced through a role-based permission system.

Component Hierarchy

MembersPage
|
+-- Header section
| +-- Title & subtitle
| +-- "Invite Member" button (if canManageMembers)
|
+-- MembersList
| +-- Owner row (always first, with "Creator" badge)
| +-- MemberRow (for each member)
| +-- Avatar (first letter)
| +-- Username & email
| +-- Role selector (Select) or role badge
| +-- Remove button (trash icon)
| +-- Confirm Remove Dialog
|
+-- InviteMemberDialog
+-- Email input
+-- Role selector (Select)
+-- Role description panel
+-- Submit / Cancel buttons

Key Components

MembersPage

File: apps/web/src/components/works/detail/members/MembersPage.tsx

The top-level container that manages the members list state and coordinates the invite dialog.

interface MembersPageProps {
work: Work;
members: WorkMember[];
owner: WorkOwner;
}

State Management:

  • members state is initialized from initialMembers prop and updated locally on add/remove/update
  • inviteDialogOpen controls the invite modal visibility
  • canInvite is derived from canManageMembers(work.userRole)

Callback Handlers:

  • handleMemberAdded(member) -- appends the new member to the list
  • handleMemberRemoved(memberId) -- filters the member from the list
  • handleMemberUpdated(updated) -- replaces the member entry in the list

MembersList

File: apps/web/src/components/works/detail/members/MembersList.tsx

Renders the owner row followed by all member rows in a bordered card with dividers.

interface MembersListProps {
work: Work;
members: WorkMember[];
owner: WorkOwner;
onMemberRemoved: (memberId: string) => void;
onMemberUpdated: (member: WorkMember) => void;
}

The owner is always displayed at the top with a highlighted background and a "Creator" badge pill. If there are no members, a centered empty state message is shown.

MemberRow

File: apps/web/src/components/works/detail/members/MemberRow.tsx

Renders a single member with inline role management and removal capabilities.

interface MemberRowProps {
workId: string;
member: WorkMember;
canManage: boolean;
onRemoved: () => void;
onUpdated: (member: WorkMember) => void;
}

Role Change Flow:

  1. Manager selects a new role from the inline Select dropdown
  2. updateMemberRole(workId, memberId, newRole) server action is called
  3. On success, onUpdated callback propagates the change to MembersPage
  4. Toast notification confirms the update

Member Removal Flow:

  1. Manager clicks the trash icon button
  2. A confirmation Dialog opens with the member's username
  3. On confirm, removeMember(workId, memberId) server action is called
  4. On success, onRemoved callback removes the member from the list
  5. Toast notification confirms removal

InviteMemberDialog

File: apps/web/src/components/works/detail/members/InviteMemberDialog.tsx

A modal form for inviting new members by email with a role assignment.

interface InviteMemberDialogProps {
workId: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onMemberAdded: (member: WorkMember) => void;
}

Form Fields:

FieldTypeValidation
EmailInputRequired, email format
RoleSelectDefault: VIEWER

Available Roles:

RoleLabelDescription
VIEWERViewerCan view work content and settings (read-only)
EDITOREditorCan edit items, categories, and content
MANAGERManagerCan manage members, settings, and all work features

The dialog displays a contextual description panel below the role selector that updates dynamically based on the selected role.

Submission Flow:

  1. User enters email and selects role
  2. Form validation checks for non-empty email
  3. inviteMember(workId, email, role) server action is called
  4. On success: toast notification, form reset, dialog closes, onMemberAdded callback fires
  5. On failure: inline error message displayed below the email field

Role-Based Permissions

The canManageMembers utility from @/lib/permissions determines whether the current user can manage team members:

User RoleCan InviteCan Change RolesCan Remove MembersSees Role Selector
OwnerYesYesYesYes (Select)
ManagerYesYesYesYes (Select)
EditorNoNoNoNo (badge only)
ViewerNoNoNoNo (badge only)

When canManage is false, the role is displayed as a static badge rather than an interactive selector, and the remove button is hidden.

State Management Patterns

MembersPage (state owner)
|
|-- members: WorkMember[] // local state, initialized from server
|-- inviteDialogOpen: boolean // dialog visibility
|
+-- MembersList (stateless display)
| |
| +-- MemberRow (local state for isUpdating, isRemoving, confirmRemoveOpen)
|
+-- InviteMemberDialog (local form state: email, role, isSubmitting, error)

All server mutations go through Next.js server actions. Optimistic updates are applied locally to avoid full-page refreshes, maintaining a responsive feel.

ActionServer Action FunctionHTTP Method
Invite memberinviteMember(workId, email, role)POST
Update roleupdateMemberRole(workId, memberId, role)PATCH
Remove memberremoveMember(workId, memberId)DELETE

Internationalization

All strings use next-intl with the namespace dashboard.workDetail.members:

  • title, subtitle -- page header
  • inviteMember -- invite button label
  • roles.creator, roles.viewer, roles.editor, roles.manager -- role labels
  • roleDescriptions.viewer, roleDescriptions.editor, roleDescriptions.manager -- role descriptions
  • invite.* -- invite dialog labels and messages
  • confirmRemove.* -- removal confirmation dialog
  • noMembers -- empty state message

Cross-References