import { useState } from "react"; import { useParams } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { projectMembersGrouped, projectUpdateMemberRole, projectRemoveMember } from "@/client/api"; import type { GroupedMemberListResponse, MemberInfo, MemberRole } from "@/client/model"; import { useProjectInfo } from "@/hooks/useProjectInfo"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Loader2 } from "lucide-react"; import { isProjectAdminRole, isProjectOwnerRole } from "@/lib/project-permissions"; const NEXT_ROLE: Record = { admin: "member", member: "admin" }; export function MembersSettings() { const { projectName } = useParams<{ projectName: string }>(); const { data: projectInfo } = useProjectInfo(projectName); const queryClient = useQueryClient(); const isCurrentAdmin = isProjectAdminRole(projectInfo?.role); const isCurrentOwner = isProjectOwnerRole(projectInfo?.role); const { data, isLoading, error } = useQuery({ queryKey: ["project-members-grouped", projectName], queryFn: async () => { const res = await projectMembersGrouped(projectName!); return res.data?.data as GroupedMemberListResponse; }, enabled: !!projectName, staleTime: 30_000, }); const [actionLoading, setActionLoading] = useState(null); const [msg, setMsg] = useState<{ type: "success" | "error"; text: string } | null>(null); if (isLoading) return
; if (error || !data) return
Failed to load members
; const invalidate = () => queryClient.invalidateQueries({ queryKey: ["project-members-grouped", projectName] }); const handleRoleToggle = async (member: MemberInfo) => { const nextRole = NEXT_ROLE[member.scope.toLowerCase()] as MemberRole; if (!nextRole) return; try { setActionLoading(member.user_id); setMsg(null); await projectUpdateMemberRole(projectName!, { user_id: member.user_id, scope: nextRole }); setMsg({ type: "success", text: `${member.username} role changed to ${nextRole}` }); invalidate(); } catch { setMsg({ type: "error", text: "Failed to update role" }); } finally { setActionLoading(null); } }; const handleRemove = async (member: MemberInfo) => { if (!confirm(`Remove ${member.username} from this project?`)) return; try { setActionLoading(member.user_id); setMsg(null); await projectRemoveMember(projectName!, member.user_id); setMsg({ type: "success", text: `${member.username} removed` }); invalidate(); } catch { setMsg({ type: "error", text: "Failed to remove member" }); } finally { setActionLoading(null); } }; return (
{msg &&
{msg.text}
} {data.groups.map(group => (
{group.role}s — {group.members.length}
{group.members.map(member => { const isSelf = member.user_id === projectInfo?.created_by; const canManage = isCurrentAdmin && !isProjectOwnerRole(member.scope) && !isSelf; return (
{(member.display_name || member.username)[0]?.toUpperCase()}

{member.display_name || member.username}

@{member.username}

{isSelf && You}
{canManage && ( )} {canManage && isCurrentOwner && ( )}
); })}
))}
); }