"use client"; import { useState } from "react"; import { Crown, Shield, User, Plus, MoreHorizontal, UserMinus, Users, Clock, X, Mail } from "lucide-react"; import { ActorAvatar } from "../../common/actor-avatar"; import type { MemberWithUser, MemberRole, Invitation } from "@multica/core/types"; import { Input } from "@multica/ui/components/ui/input"; import { Button } from "@multica/ui/components/ui/button"; import { Card, CardContent } from "@multica/ui/components/ui/card"; import { Badge } from "@multica/ui/components/ui/badge"; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from "@multica/ui/components/ui/alert-dialog"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from "@multica/ui/components/ui/select"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, } from "@multica/ui/components/ui/dropdown-menu"; import { toast } from "sonner"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAuthStore } from "@multica/core/auth"; import { useWorkspaceId } from "@multica/core/hooks"; import { useCurrentWorkspace } from "@multica/core/paths"; import { memberListOptions, invitationListOptions, workspaceKeys } from "@multica/core/workspace/queries"; import { api } from "@multica/core/api"; const roleConfig: Record = { owner: { label: "Owner", icon: Crown, description: "Full access, manage all settings" }, admin: { label: "Admin", icon: Shield, description: "Manage members and settings" }, member: { label: "Member", icon: User, description: "Create and work on issues" }, }; function MemberRow({ member, canManage, canManageOwners, isSelf, busy, onRoleChange, onRemove, }: { member: MemberWithUser; canManage: boolean; canManageOwners: boolean; isSelf: boolean; busy: boolean; onRoleChange: (role: MemberRole) => void; onRemove: () => void; }) { const rc = roleConfig[member.role]; const RoleIcon = rc.icon; const canEditRole = canManage && !isSelf && (member.role !== "owner" || canManageOwners); const canRemove = canManage && !isSelf && (member.role !== "owner" || canManageOwners); const showMenu = canEditRole || canRemove; return (
{member.name}
{member.email}
{showMenu && ( } /> {canEditRole && ( Change role {(Object.entries(roleConfig) as [MemberRole, (typeof roleConfig)[MemberRole]][]).map( ([role, config]) => { if (role === "owner" && !canManageOwners) return null; const Icon = config.icon; return ( onRoleChange(role)} >
{config.label} {config.description}
{member.role === role && ( )}
); } )}
)} {canEditRole && canRemove && } {canRemove && ( Remove from workspace )}
)} {rc.label}
); } function InvitationRow({ invitation, canManage, onRevoke, busy, }: { invitation: Invitation; canManage: boolean; onRevoke: () => void; busy: boolean; }) { const rc = roleConfig[invitation.role]; return (
{invitation.invitee_email}
Pending
{canManage && ( )} {rc.label}
); } export function MembersTab() { const user = useAuthStore((s) => s.user); const workspace = useCurrentWorkspace(); const qc = useQueryClient(); const wsId = useWorkspaceId(); const { data: members = [] } = useQuery(memberListOptions(wsId)); const { data: invitations = [] } = useQuery(invitationListOptions(wsId)); const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("member"); const [inviteLoading, setInviteLoading] = useState(false); const [memberActionId, setMemberActionId] = useState(null); const [invitationActionId, setInvitationActionId] = useState(null); const [confirmAction, setConfirmAction] = useState<{ title: string; description: string; variant?: "destructive"; onConfirm: () => Promise; } | null>(null); const currentMember = members.find((m) => m.user_id === user?.id) ?? null; const canManageWorkspace = currentMember?.role === "owner" || currentMember?.role === "admin"; const isOwner = currentMember?.role === "owner"; const handleInviteMember = async () => { if (!workspace) return; setInviteLoading(true); try { await api.createMember(workspace.id, { email: inviteEmail, role: inviteRole, }); setInviteEmail(""); setInviteRole("member"); qc.invalidateQueries({ queryKey: workspaceKeys.invitations(wsId) }); toast.success("Invitation sent"); } catch (e) { toast.error(e instanceof Error ? e.message : "Failed to send invitation"); } finally { setInviteLoading(false); } }; const handleRevokeInvitation = (invitation: Invitation) => { if (!workspace) return; setConfirmAction({ title: "Revoke invitation", description: `Revoke the invitation to ${invitation.invitee_email}? They will no longer be able to join this workspace.`, variant: "destructive", onConfirm: async () => { setInvitationActionId(invitation.id); try { await api.revokeInvitation(workspace.id, invitation.id); qc.invalidateQueries({ queryKey: workspaceKeys.invitations(wsId) }); toast.success("Invitation revoked"); } catch (e) { toast.error(e instanceof Error ? e.message : "Failed to revoke invitation"); } finally { setInvitationActionId(null); } }, }); }; const handleRoleChange = async (memberId: string, role: MemberRole) => { if (!workspace) return; setMemberActionId(memberId); try { await api.updateMember(workspace.id, memberId, { role }); qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) }); toast.success("Role updated"); } catch (e) { toast.error(e instanceof Error ? e.message : "Failed to update member"); } finally { setMemberActionId(null); } }; const handleRemoveMember = (member: MemberWithUser) => { if (!workspace) return; setConfirmAction({ title: `Remove ${member.name}`, description: `Remove ${member.name} from ${workspace.name}? They will lose access to this workspace.`, variant: "destructive", onConfirm: async () => { setMemberActionId(member.id); try { await api.deleteMember(workspace.id, member.id); qc.invalidateQueries({ queryKey: workspaceKeys.members(wsId) }); toast.success("Member removed"); } catch (e) { toast.error(e instanceof Error ? e.message : "Failed to remove member"); } finally { setMemberActionId(null); } }, }); }; if (!workspace) return null; return (

Members ({members.length})

{canManageWorkspace && (

Invite member

setInviteEmail(e.target.value)} placeholder="user@company.com" onKeyDown={(e) => { if (e.key === "Enter" && inviteEmail.trim()) handleInviteMember(); }} />
)} {members.length > 0 ? (
{members.map((m, i) => (
0 ? "border-t border-border/50" : ""}> handleRoleChange(m.id, role)} onRemove={() => handleRemoveMember(m)} />
))}
) : (

No members found.

)}
{invitations.length > 0 && (

Pending invitations ({invitations.length})

{invitations.map((inv, i) => (
0 ? "border-t border-border/50" : ""}> handleRevokeInvitation(inv)} busy={invitationActionId === inv.id} />
))}
)} { if (!v) setConfirmAction(null); }}> {confirmAction?.title} {confirmAction?.description} Cancel { await confirmAction?.onConfirm(); setConfirmAction(null); }} > Confirm
); }