import { useMemo, useState } from "react" import { withRefresh } from "@/api/with-refresh.ts" import { orgStore } from "@/auth/org.ts" import { makeOrgsApi } from "@/sdkClient.ts" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Loader2 } from "lucide-react" import { useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" import { Button } from "@/components/ui/button.tsx" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form.tsx" import { Input } from "@/components/ui/input.tsx" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select.tsx" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table.tsx" const addSchema = z.object({ user_id: z.uuid("Invalid UUID"), role: z.enum(["owner", "admin", "member"]), }) type AddValues = z.infer export const OrgMembers = () => { const api = makeOrgsApi() const qc = useQueryClient() const orgId = orgStore.get() const [updatingId, setUpdatingId] = useState(null) const q = useQuery({ enabled: !!orgId, queryKey: ["org:members", orgId], queryFn: () => withRefresh(() => api.listMembers({ id: orgId! })), }) const ownersCount = useMemo( () => (q.data ?? []).filter((m) => m.role === "owner").length, [q.data] ) const form = useForm({ resolver: zodResolver(addSchema), defaultValues: { user_id: "", role: "member", }, }) const addMut = useMutation({ mutationFn: (v: AddValues) => api.addOrUpdateMember({ id: orgId!, handlersMemberUpsertReq: v }), onSuccess: () => { toast.success("Member added/updated") void qc.invalidateQueries({ queryKey: ["org:members", orgId] }) form.reset({ user_id: "", role: "member" }) }, onError: (e: any) => toast.error(e?.message ?? "Failed"), }) const removeMut = useMutation({ mutationFn: (userId: string) => api.removeMember({ id: orgId!, userId }), onSuccess: () => { toast.success("Member removed") void qc.invalidateQueries({ queryKey: ["org:members", orgId] }) }, onError: (e: any) => toast.error(e?.message ?? "Failed"), }) const roleMut = useMutation({ mutationFn: ({ userId, role }: { userId: string; role: "owner" | "admin" | "member" }) => api.addOrUpdateMember({ id: orgId!, handlersMemberUpsertReq: { user_id: userId, role } }), onMutate: async ({ userId, role }) => { setUpdatingId(userId) // cancel queries and snapshot previous await qc.cancelQueries({ queryKey: ["org:members", orgId] }) const prev = qc.getQueryData(["org:members", orgId]) // optimistic update qc.setQueryData(["org:members", orgId], (old: any[] = []) => old.map((m) => (m.user_id === userId ? { ...m, role } : m)) ) return { prev } }, onError: (e, _vars, ctx) => { if (ctx?.prev) qc.setQueryData(["org:members", orgId], ctx.prev) toast.error((e as any)?.message ?? "Failed to update role") }, onSuccess: () => { toast.success("Role updated") }, onSettled: () => { setUpdatingId(null) void qc.invalidateQueries({ queryKey: ["org:members", orgId] }) }, }) const canDowngrade = (m: any) => !(m.role === "owner" && ownersCount <= 1) if (!orgId) return

Pick an organization.

if (q.isLoading) return

Loading...

if (q.error) return

Failed to load members.

return ( Members {/* Add/Update */}
addMut.mutate(v))} >
( User ID )} />
( Role )} />
Id User Role {q.data?.map((m) => { const isRowPending = updatingId === m.user_id return ( {m.user_id} {m.email} {/* Inline role select */}
{isRowPending && }
) })} {q.data?.length === 0 && ( No members. )}
) }