diff --git a/src/js/web/src/routes/_app/teams/$teamId.tsx b/src/js/web/src/routes/_app/teams/$teamId.tsx index 38fa94d1f..be83e34b5 100644 --- a/src/js/web/src/routes/_app/teams/$teamId.tsx +++ b/src/js/web/src/routes/_app/teams/$teamId.tsx @@ -1,215 +1,5 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { createFileRoute, Link, useParams } from "@tanstack/react-router" -import { ArrowLeft, Clock, Mail, X } from "lucide-react" -import { useEffect } from "react" -import { InviteMemberDialog } from "@/components/teams/invite-member-dialog" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { PageContainer, PageHeader, PageSection } from "@/components/ui/page-layout" -import { Separator } from "@/components/ui/separator" -import { useAuthStore } from "@/lib/auth" -import { deleteTeamInvitation, getTeam, listTeamInvitations, removeMemberFromTeam, type TeamInvitation, type TeamMember } from "@/lib/generated/api" +import { createFileRoute, Outlet } from "@tanstack/react-router" export const Route = createFileRoute("/_app/teams/$teamId")({ - component: TeamDetail, + component: () => , }) - -function TeamDetail() { - const { teamId } = useParams({ from: "/_app/teams/$teamId" as const }) - const queryClient = useQueryClient() - const { user, currentTeam, setCurrentTeam } = useAuthStore() - - const { - data: team, - isLoading: isTeamLoading, - isError: isTeamError, - } = useQuery({ - queryKey: ["team", teamId], - queryFn: async () => { - const response = await getTeam({ path: { team_id: teamId } }) - return response.data - }, - }) - - const { data: invitationsData } = useQuery({ - queryKey: ["teamInvitations", teamId], - queryFn: async () => { - const response = await listTeamInvitations({ - path: { team_id: teamId }, - }) - return response.data?.items ?? [] - }, - enabled: !!team, - }) - - const pendingInvitations = invitationsData?.filter((inv) => !inv.isAccepted) ?? [] - - useEffect(() => { - if (team && team.id !== currentTeam?.id) { - setCurrentTeam(team) - } - }, [currentTeam?.id, setCurrentTeam, team]) - - const removeMemberMutation = useMutation({ - mutationFn: (memberEmail: string) => - removeMemberFromTeam({ - path: { team_id: teamId }, - body: { userName: memberEmail }, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["team", teamId] }) - }, - }) - - const cancelInvitationMutation = useMutation({ - mutationFn: (invitationId: string) => - deleteTeamInvitation({ - path: { team_id: teamId, invitation_id: invitationId }, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["teamInvitations", teamId] }) - }, - }) - - if (isTeamLoading) { - return ( - -
Loading team...
-
- ) - } - - if (isTeamError || !team) { - return ( - -
{isTeamError ? "We couldn't load this team yet. Try refreshing." : "Team not found"}
-
- ) - } - - const members = team.members ?? [] - const owner = members.find((member) => member.isOwner) - const ownerId = owner?.userId - const isOwner = ownerId === user?.id - const canManageMembers = isOwner || user?.isSuperuser || members.some((member) => member.userId === user?.id && member.role === "ADMIN") - - const canRemoveMember = (member: TeamMember) => { - // Cannot remove the owner (they must transfer ownership first) - if (member.isOwner) return false - // Cannot remove yourself unless you're superuser - if (member.userId === user?.id && !user?.isSuperuser) return false - return canManageMembers - } - - return ( - - - - {canManageMembers && } - - } - /> - - -
- - - Members - - - {members.map((member: TeamMember) => { - const isSelf = member.userId === user?.id - return ( -
-
-
-

{member.name ?? member.email}

- {isSelf && ( - - You - - )} - {member.isOwner && Owner} -
-

{member.email}

-
-
- - {member.role ?? "MEMBER"} - - {canRemoveMember(member) && ( - - )} -
-
- ) - })} - {members.length === 0 &&
No members yet.
} - - {/* Pending Invitations */} - {pendingInvitations.length > 0 && ( - <> - -
-

- - Pending invitations -

- {pendingInvitations.map((invitation: TeamInvitation) => ( -
-
- -
-

{invitation.email}

-

Invited as {invitation.role.toLowerCase()}

-
-
- {canManageMembers && ( - - )} -
- ))} -
- - )} -
-
- - - - Team overview - - -
- Member count - {members.length} -
- -

Use this space to track key links, environments, or runbooks for the team.

-
-
-
-
-
- ) -} - -export default TeamDetail diff --git a/src/js/web/src/routes/_app/teams/$teamId/index.tsx b/src/js/web/src/routes/_app/teams/$teamId/index.tsx new file mode 100644 index 000000000..b1436b6a1 --- /dev/null +++ b/src/js/web/src/routes/_app/teams/$teamId/index.tsx @@ -0,0 +1,210 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { createFileRoute, Link, useParams } from "@tanstack/react-router" +import { ArrowLeft, Clock, Mail, X } from "lucide-react" +import { useEffect } from "react" +import { InviteMemberDialog } from "@/components/teams/invite-member-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { PageContainer, PageHeader, PageSection } from "@/components/ui/page-layout" +import { Separator } from "@/components/ui/separator" +import { useAuthStore } from "@/lib/auth" +import { deleteTeamInvitation, getTeam, listTeamInvitations, removeMemberFromTeam, type TeamInvitation, type TeamMember } from "@/lib/generated/api" + +export const Route = createFileRoute("/_app/teams/$teamId/")({ + component: TeamDetail, +}) + +function TeamDetail() { + const { teamId } = useParams({ from: "/_app/teams/$teamId/" }) + const queryClient = useQueryClient() + const { user, currentTeam, setCurrentTeam } = useAuthStore() + + const { + data: team, + isLoading: isTeamLoading, + isError: isTeamError, + } = useQuery({ + queryKey: ["team", teamId], + queryFn: async () => { + const response = await getTeam({ path: { team_id: teamId } }) + return response.data + }, + }) + + const { data: invitationsData } = useQuery({ + queryKey: ["teamInvitations", teamId], + queryFn: async () => { + const response = await listTeamInvitations({ + path: { team_id: teamId }, + }) + return response.data?.items ?? [] + }, + enabled: !!team, + }) + + const pendingInvitations = invitationsData?.filter((inv) => !inv.isAccepted) ?? [] + + useEffect(() => { + if (team && team.id !== currentTeam?.id) { + setCurrentTeam(team) + } + }, [currentTeam?.id, setCurrentTeam, team]) + + const removeMemberMutation = useMutation({ + mutationFn: (memberEmail: string) => + removeMemberFromTeam({ + path: { team_id: teamId }, + body: { userName: memberEmail }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["team", teamId] }) + }, + }) + + const cancelInvitationMutation = useMutation({ + mutationFn: (invitationId: string) => + deleteTeamInvitation({ + path: { team_id: teamId, invitation_id: invitationId }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["teamInvitations", teamId] }) + }, + }) + + if (isTeamLoading) { + return ( + +
Loading team...
+
+ ) + } + + if (isTeamError || !team) { + return ( + +
{isTeamError ? "We couldn't load this team yet. Try refreshing." : "Team not found"}
+
+ ) + } + + const members = team.members ?? [] + const owner = members.find((member) => member.isOwner) + const ownerId = owner?.userId + const isOwner = ownerId === user?.id + const canManageMembers = isOwner || user?.isSuperuser || members.some((member) => member.userId === user?.id && member.role === "ADMIN") + + const canRemoveMember = (member: TeamMember) => { + if (member.isOwner) return false + if (member.userId === user?.id && !user?.isSuperuser) return false + return canManageMembers + } + + return ( + + + + {canManageMembers && } + + } + /> + + +
+ + + Members + + + {members.map((member: TeamMember) => { + const isSelf = member.userId === user?.id + return ( +
+
+
+

{member.name ?? member.email}

+ {isSelf && ( + + You + + )} + {member.isOwner && Owner} +
+

{member.email}

+
+
+ + {member.role ?? "MEMBER"} + + {canRemoveMember(member) && ( + + )} +
+
+ ) + })} + {members.length === 0 &&
No members yet.
} + + {pendingInvitations.length > 0 && ( + <> + +
+

+ + Pending invitations +

+ {pendingInvitations.map((invitation: TeamInvitation) => ( +
+
+ +
+

{invitation.email}

+

Invited as {invitation.role.toLowerCase()}

+
+
+ {canManageMembers && ( + + )} +
+ ))} +
+ + )} +
+
+ + + + Team overview + + +
+ Member count + {members.length} +
+ +

Use this space to track key links, environments, or runbooks for the team.

+
+
+
+
+
+ ) +} diff --git a/src/js/web/src/routes/_app/teams/$teamId/invitations/$invitationId/accept.tsx b/src/js/web/src/routes/_app/teams/$teamId/invitations/$invitationId/accept.tsx index 404bb5e37..97f2e6e10 100644 --- a/src/js/web/src/routes/_app/teams/$teamId/invitations/$invitationId/accept.tsx +++ b/src/js/web/src/routes/_app/teams/$teamId/invitations/$invitationId/accept.tsx @@ -22,13 +22,13 @@ function AcceptInvitationPage() { const [status, setStatus] = useState<"pending" | "accepting" | "declining" | "accepted" | "declined" | "error">("pending") const [errorMessage, setErrorMessage] = useState(null) - // Optionally fetch team info for display const { data: team, isLoading: isTeamLoading } = useQuery({ queryKey: ["team", teamId], queryFn: async () => { const response = await getTeam({ path: { team_id: teamId } }) return response.data }, + retry: false, }) const acceptMutation = useMutation({