diff --git a/apps/web/components/settings/account.tsx b/apps/web/components/settings/account.tsx index 155659b58..fa3d7dc9d 100644 --- a/apps/web/components/settings/account.tsx +++ b/apps/web/components/settings/account.tsx @@ -4,15 +4,8 @@ import { dmSans125ClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { useAuth } from "@lib/auth-context" import { authClient } from "@lib/auth" -import { useOrgSummaries } from "@/hooks/use-org-summaries" -import { OrgPlanBadge, resolveOrgPlan } from "@/components/org-plan-badge" import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar" -import { - PLAN_RANK, - useTokenUsage, - type PlanType, -} from "@/hooks/use-token-usage" -import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" +import { Popover, PopoverContent } from "@ui/components/popover" import { Select, SelectContent, @@ -28,23 +21,21 @@ import { } from "@ui/components/dropdown-menu" import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" import * as DialogPrimitive from "@radix-ui/react-dialog" -import { useCustomer } from "autumn-js/react" import { useMutation, useQuery } from "@tanstack/react-query" import { - Check, LoaderIcon, ChevronDown, - Building2, Users, UserPlus, Mail, MoreHorizontal, UserMinus, X, + Pencil, Tag, Plus, } from "lucide-react" -import { useMemo, useRef, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import { toast } from "sonner" import { useContainerTags } from "@/hooks/use-container-tags" import { PopoverAnchor } from "@ui/components/popover" @@ -135,16 +126,7 @@ function isPendingInvitation(invitation: { } export default function Account() { - const { - user, - org, - organizations: allOrgs, - setActiveOrg, - refetchActiveOrg, - } = useAuth() - const autumn = useCustomer() - const [switchingOrgId, setSwitchingOrgId] = useState(null) - const [orgMenuOpen, setOrgMenuOpen] = useState(false) + const { user, org, refetchActiveOrg, refetchOrganizations } = useAuth() const [inviteDialogOpen, setInviteDialogOpen] = useState(false) const [inviteEmail, setInviteEmail] = useState("") const [inviteRole, setInviteRole] = useState("member") @@ -156,6 +138,8 @@ export default function Account() { >([]) const [tagQuery, setTagQuery] = useState("") const [tagDropdownOpen, setTagDropdownOpen] = useState(false) + const [isEditingOrgName, setIsEditingOrgName] = useState(false) + const [orgNameDraft, setOrgNameDraft] = useState("") const tagInputRef = useRef(null) const tagAnchorRef = useRef(null) const { allProjects: allContainerTags } = useContainerTags() @@ -174,28 +158,17 @@ export default function Account() { const showAccessType = inviteRole === "member" const showTagPicker = inviteRole === "member" && inviteAccessType === "restricted" - const canSwitchOrg = (allOrgs?.length ?? 0) > 1 - const { data: orgSummaries } = useOrgSummaries() - - const handleOrgSwitch = async (orgSlug: string, orgId: string) => { - if (orgId === org?.id) return - setSwitchingOrgId(orgId) - try { - await setActiveOrg(orgSlug) - window.location.reload() - } catch (error) { - console.error("Failed to switch organization:", error) - setSwitchingOrgId(null) - } - } - const { currentPlan } = useTokenUsage(autumn) + useEffect(() => { + setOrgNameDraft(org?.name ?? "") + setIsEditingOrgName(false) + }, [org?.name]) const activeMemberRoleQuery = useQuery({ queryKey: ["organization", org?.id, "active-member-role"], queryFn: async () => { if (!org?.id) return null - const result = await authClient.organization.getActiveMemberRole({ + const result = await authClient.organization.getActiveMember({ query: { organizationId: org.id }, }) if (result.error) { @@ -346,40 +319,50 @@ export default function Account() { }, }) + const updateOrgNameMutation = useMutation({ + mutationFn: async (name: string) => { + if (!org?.id) throw new Error("No active organization") + const trimmed = name.trim() + if (!trimmed) throw new Error("Enter an organization name") + const result = await authClient.organization.update({ + organizationId: org.id, + data: { name: trimmed }, + }) + if (result.error) { + throw new Error( + result.error.message ?? "Failed to update organization name", + ) + } + return trimmed + }, + onSuccess: async (name) => { + setOrgNameDraft(name) + setIsEditingOrgName(false) + await Promise.all([refetchActiveOrg(), refetchOrganizations()]) + toast.success("Organization name updated") + }, + onError: (error) => { + toast.error(getErrorMessage(error, "Failed to update organization name")) + }, + }) + const handleInviteSubmit = (event: React.FormEvent) => { event.preventDefault() if (!canManageTeam || inviteMemberMutation.isPending) return inviteMemberMutation.mutate() } - const planByOrgId = useMemo(() => { - const map = new Map() - for (const summary of orgSummaries ?? []) { - map.set(summary.orgId, summary.plan) + const handleOrgNameSubmit = (event: React.FormEvent) => { + event.preventDefault() + if (!canManageTeam || updateOrgNameMutation.isPending) return + const trimmed = orgNameDraft.trim() + if (!trimmed || trimmed === org?.name) { + setOrgNameDraft(org?.name ?? "") + setIsEditingOrgName(false) + return } - return map - }, [orgSummaries]) - - const sortedOrgsForMenu = useMemo(() => { - if (!allOrgs?.length) return [] - return [...allOrgs].sort((a, b) => { - const planA = resolveOrgPlan( - a.id, - a.id === org?.id, - currentPlan, - planByOrgId, - ) - const planB = resolveOrgPlan( - b.id, - b.id === org?.id, - currentPlan, - planByOrgId, - ) - const rankDiff = PLAN_RANK[planB] - PLAN_RANK[planA] - if (rankDiff !== 0) return rankDiff - return a.name.localeCompare(b.name) - }) - }, [allOrgs, org?.id, currentPlan, planByOrgId]) + updateOrgNameMutation.mutate(trimmed) + } const memberSince = user?.createdAt ? new Date(user.createdAt).toLocaleDateString("en-US", { @@ -438,84 +421,84 @@ export default function Account() { > Organization

- {canSwitchOrg ? ( - - + setOrgNameDraft(event.target.value)} + disabled={updateOrgNameMutation.isPending} + maxLength={80} className={cn( - "flex min-w-0 max-w-full items-center gap-2 transition-opacity", - "cursor-pointer hover:opacity-90", dmSans125ClassName(), + "h-9 min-w-0 flex-1 rounded-[9px] border border-white/10 bg-black/30 px-3 text-[14px] font-medium tracking-[-0.14px] text-[#FAFAFA] outline-none transition-colors placeholder:text-[#525252] focus:border-[#4BA0FA]/60", )} - > - +
+ - ) - })} - - + {updateOrgNameMutation.isPending ? ( + + ) : null} + Save + + +
+ ) : ( - - {org?.name ?? "Personal"} - +
+ + {org?.name ?? "Personal"} + + {canManageTeam ? ( + + ) : null} +
)}