import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@agent-native/toolkit/ui/alert-dialog"; import { Button as ToolkitButton } from "@agent-native/toolkit/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@agent-native/toolkit/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@agent-native/toolkit/ui/table"; import { IconUserPlus, IconTrash, IconCrown, IconShieldCheck, IconLoader2, IconCheck, IconPencil, IconAt, IconX, IconKey, IconCopy, IconRefresh, IconEye, IconEyeOff, IconCloudUpload, IconFileImport, IconPlus, IconAlertTriangle, IconUsersGroup, IconHelpCircle, } from "@tabler/icons-react"; import { forwardRef, useMemo, useRef, useState, type ComponentPropsWithoutRef, type ReactNode, } from "react"; import type { DomainMatchOrg } from "../../org/types.js"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "../components/ui/tooltip.js"; import { useT } from "../i18n.js"; import { cn } from "../utils.js"; import { useOrg, useOrgMembers, useOrgInvitations, useCreateOrg, useUpdateOrg, useBulkInviteMembers, useChangeMemberRole, useAcceptInvitation, useRemoveMember, useDeleteOrg, useSwitchOrg, useSetOrgDomain, useRevealA2ASecret, useSetA2ASecret, useSyncA2ASecret, useJoinByDomain, type InviteRole, type SyncA2ASecretResult, } from "./hooks.js"; const Button = forwardRef< HTMLButtonElement, ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )); Button.displayName = "TeamPrimitiveButton"; export interface TeamPageProps { /** * Optional wrapper around the page contents. Templates pass their own Layout * component so the Team page renders inside the template's chrome. */ layout?: (children: ReactNode) => ReactNode; /** * Title shown at the top of the page. Defaults to "Team". */ title?: string; /** * Hide the page title when this is rendered inside another titled surface, * such as the Settings > Team tab. */ showTitle?: boolean; /** * Description shown on the "Create an Organization" card. Defaults to * "Set up a team to collaborate with your colleagues." */ createOrgDescription?: string; /** * Class applied to the outer max-width container. Templates can use this to * tweak page width. */ className?: string; } function RoleIcon({ role }: { role: string }) { if (role === "owner") return ; if (role === "admin") return ; return null; } function ErrorText({ error }: { error: unknown }) { if (!error) return null; return (

{error instanceof Error ? error.message : String(error)}

); } function PendingInvitationsCard() { const t = useT(); const { data: org } = useOrg(); const acceptInvitation = useAcceptInvitation(); if (!org?.pendingInvitations?.length) return null; return (

{t("org.pendingInvitations")}

{org.pendingInvitations.map((inv) => (
{inv.orgName}
{t("org.invitedByLabel", { name: inv.invitedBy })}
))}
); } function JoinByDomainCard({ matches }: { matches: DomainMatchOrg[] }) { const t = useT(); const joinByDomain = useJoinByDomain(); const [pendingId, setPendingId] = useState(null); return (

{t("org.joinYourTeam")}

{matches.length === 1 ? t("org.joinDomainOne") : t("org.joinDomainMany")}

{matches.map((m) => (
{m.orgName}
))}
); } function CreateOrgCard({ description }: { description?: string }) { const t = useT(); const createOrg = useCreateOrg(); const [name, setName] = useState(""); const [showForm, setShowForm] = useState(false); return (

{t("org.createOrgCardTitle")}

{description || t("org.createOrgCardDescription")}

{t("org.createOrgVaultNotice")}

{!showForm ? ( ) : (
setName(e.target.value)} placeholder="e.g. Acme Inc." className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-foreground" autoFocus />
)}
); } function OrgNameDisplay({ name, canEdit }: { name: string; canEdit: boolean }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(name); const updateOrg = useUpdateOrg(); if (!canEdit) return
{name}
; if (!editing) { return ( ); } function save() { const trimmed = draft.trim(); if (!trimmed || trimmed === name) { setEditing(false); return; } updateOrg.mutate(trimmed, { onSuccess: () => setEditing(false) }); } return (
setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") save(); if (e.key === "Escape") setEditing(false); }} onBlur={save} className="rounded border border-border bg-background px-1.5 py-0.5 text-sm font-medium focus:outline-none focus:ring-1 focus:ring-foreground" autoFocus />
); } interface MemberListItem { email: string; role: string; } interface PendingInviteListItem { id: string; email: string; role: string; } function MembersCard() { const t = useT(); const { data: org } = useOrg(); const { data: membersData, isLoading: isLoadingMembers } = useOrgMembers(); const { data: invitationsData } = useOrgInvitations(); const switchOrg = useSwitchOrg(); if (!org?.orgId) return null; const isOwner = org.role === "owner"; const isOwnerOrAdmin = isOwner || org.role === "admin"; const members = membersData?.members ?? []; const pendingInvites = invitationsData?.invitations ?? []; const hasMultipleOrgs = (org.orgs?.length ?? 0) > 1; return (
{t("org.memberCount", { count: members.length })} ·{" "} {t("org.youAreRole", { role: org.role })}
{hasMultipleOrgs && ( )}
{isOwnerOrAdmin && (
{isOwner && }
)}
{isOwner && }
); } function MembersTableCard({ members, pendingInvites, isLoadingMembers, currentUserEmail, currentUserRole, }: { members: MemberListItem[]; pendingInvites: PendingInviteListItem[]; isLoadingMembers: boolean; currentUserEmail: string; currentUserRole: string | null; }) { const t = useT(); const [showInviteForm, setShowInviteForm] = useState(false); const canInvite = currentUserRole === "owner" || currentUserRole === "admin"; return (

{t("org.members")}

{t("org.memberCount", { count: members.length })}

{canInvite && !showInviteForm && ( )}
{canInvite && showInviteForm && (
setShowInviteForm(false)} />
)} {t("org.member")} {t("org.role")} {t("org.status")} {t("org.actions")} {isLoadingMembers && members.length === 0 ? ( [0, 1, 2].map((i) => (
)) ) : members.length === 0 && pendingInvites.length === 0 ? ( {t("org.noMembers")} ) : ( <> {members.map((m) => ( ))} {pendingInvites.map((inv) => ( ))} )}
); } function DangerZoneCard({ orgName }: { orgName: string }) { const t = useT(); const deleteOrg = useDeleteOrg(); const [open, setOpen] = useState(false); const [confirmText, setConfirmText] = useState(""); const canConfirm = confirmText.trim().toLowerCase() === orgName.trim().toLowerCase(); function handleConfirm(e: { preventDefault: () => void }) { e.preventDefault(); if (!canConfirm || deleteOrg.isPending) return; deleteOrg.mutate(orgName, { onSuccess: () => setOpen(false) }); } return (

{t("org.dangerZone")}

{t("org.deleteOrgDescription")}

{ setOpen(next); if (!next) setConfirmText(""); }} > {t("org.deleteOrg")} {t("org.deleteOrgConfirmPrompt", { name: orgName })} setConfirmText(e.target.value)} placeholder={t("org.deleteOrgConfirmPlaceholder")} className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-destructive" autoFocus /> {t("org.cancel")} {deleteOrg.isPending ? ( {t("org.deleteOrgPending")} ) : ( t("org.deleteOrgConfirmCta") )}
); } function roleLabel(role: string, t: ReturnType) { if (role === "owner") return t("org.owner"); if (role === "admin") return t("org.admin"); return t("org.member"); } function RoleBadge({ role }: { role: string }) { const t = useT(); return ( {roleLabel(role, t)} ); } function PendingInviteRow({ invite }: { invite: PendingInviteListItem }) { const t = useT(); return ( {invite.email} {t("org.invited")} - ); } function MemberRow({ email, role, isCurrentUser, currentUserRole, }: { email: string; role: string; isCurrentUser: boolean; currentUserRole: string | null; }) { const t = useT(); const removeMember = useRemoveMember(); const changeRole = useChangeMemberRole(); const [editing, setEditing] = useState(false); const [confirmingRemove, setConfirmingRemove] = useState(false); // Owners can manage admins + members. Admins can only manage members. // Owners themselves are immutable through this UI; current user can't // edit their own role here. const canManage = role !== "owner" && !isCurrentUser && (currentUserRole === "owner" || (currentUserRole === "admin" && role === "member")); return ( {email} {isCurrentUser ? ( {t("org.you")} ) : ( - )} {canManage ? (
{editing ? ( ) : ( {t("org.changeRole")} )} {confirmingRemove ? (
) : ( {t("org.removeMember")} )}
) : (
-
)}
); } interface DraftInvite { email: string; role: InviteRole; } const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; function parseEmailList(input: string): string[] { return Array.from( new Set( input .split(/[\s,;]+/) .map((s) => s.trim().toLowerCase()) .filter(Boolean), ), ); } function parseCsvEmails(text: string): string[] { // Tolerant CSV parse — split on lines, then on commas, take any cell // that looks like an email. Handles "name,email,role" rows or just // "email" per line. A robust full CSV parser would be overkill here. const cells: string[] = []; for (const line of text.split(/\r?\n/)) { for (const cell of line.split(",")) { const trimmed = cell.trim().replace(/^"|"$/g, ""); if (trimmed) cells.push(trimmed); } } return Array.from( new Set(cells.filter((c) => EMAIL_RE.test(c)).map((c) => c.toLowerCase())), ); } function BulkInviteForm({ currentUserRole, onClose, }: { currentUserRole: string | null; onClose: () => void; }) { const bulkInvite = useBulkInviteMembers(); const fileRef = useRef(null); const [drafts, setDrafts] = useState([ { email: "", role: "member" }, ]); const [pasteOpen, setPasteOpen] = useState(false); const [pasteValue, setPasteValue] = useState(""); const [pasteRole, setPasteRole] = useState("member"); const [resultBanner, setResultBanner] = useState<{ succeeded: number; failed: { email: string; error: string }[]; } | null>(null); const canSetAdmin = currentUserRole === "owner"; const validDrafts = useMemo( () => drafts .map((d) => ({ ...d, email: d.email.trim().toLowerCase() })) .filter((d) => EMAIL_RE.test(d.email)), [drafts], ); function setDraft(index: number, patch: Partial) { setDrafts((prev) => prev.map((d, i) => (i === index ? { ...d, ...patch } : d)), ); } function appendEmails(emails: string[], role: InviteRole) { if (!emails.length) return; setDrafts((prev) => { const existing = new Set( prev.map((d) => d.email.trim().toLowerCase()).filter(Boolean), ); const fresh: DraftInvite[] = []; for (const e of emails) { if (!existing.has(e)) { fresh.push({ email: e, role }); existing.add(e); } } // If the only existing row is an empty placeholder, drop it. const cleaned = prev.filter( (d, i) => !(i === 0 && !d.email.trim() && prev.length === 1), ); return [...cleaned, ...fresh]; }); } function handleFile(file: File) { file.text().then((text) => { const emails = parseCsvEmails(text); if (emails.length) { appendEmails(emails, "member"); } else { setResultBanner({ succeeded: 0, failed: [{ email: file.name, error: "No valid emails found in CSV" }], }); } }); } async function submit() { setResultBanner(null); const dedup = new Map(); for (const d of validDrafts) { // canSetAdmin guard mirrors server-side enforcement so an admin-only // user editing the form can't even attempt to grant admin (they'd // get a 403 anyway). const role = canSetAdmin ? d.role : "member"; dedup.set(d.email, { ...d, role }); } const invites = Array.from(dedup.values()); if (invites.length === 0) return; const result = await bulkInvite.mutateAsync(invites); setResultBanner({ succeeded: result.succeeded.length, failed: result.failed, }); // Wipe drafts that succeeded; leave failed ones so the user can fix // and retry. If everything succeeded, reset to a single blank row. const failedEmails = new Set(result.failed.map((f) => f.email)); setDrafts((prev) => { const remaining = prev.filter((d) => failedEmails.has(d.email.trim().toLowerCase()), ); return remaining.length > 0 ? remaining : [{ email: "", role: "member" }]; }); // Auto-close on full success. if (result.failed.length === 0 && result.succeeded.length > 0) { setTimeout(() => onClose(), 1200); } } return (
{drafts.map((draft, i) => (
setDraft(i, { email: e.target.value })} placeholder="colleague@company.com" className="flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-foreground" autoFocus={i === drafts.length - 1} /> {drafts.length > 1 && ( )}
))}
{ const file = e.target.files?.[0]; if (file) handleFile(file); // reset so re-uploading the same file re-fires onChange e.target.value = ""; }} />
{pasteOpen && (
Paste emails (comma, space, or newline separated)