mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
implement update org name in settings
This commit is contained in:
parent
9fa626cf17
commit
5f937f3217
1 changed files with 189 additions and 67 deletions
|
|
@ -41,10 +41,11 @@ import {
|
|||
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"
|
||||
|
|
@ -141,6 +142,7 @@ export default function Account() {
|
|||
organizations: allOrgs,
|
||||
setActiveOrg,
|
||||
refetchActiveOrg,
|
||||
refetchOrganizations,
|
||||
} = useAuth()
|
||||
const autumn = useCustomer()
|
||||
const [switchingOrgId, setSwitchingOrgId] = useState<string | null>(null)
|
||||
|
|
@ -156,6 +158,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<HTMLInputElement>(null)
|
||||
const tagAnchorRef = useRef<HTMLDivElement>(null)
|
||||
const { allProjects: allContainerTags } = useContainerTags()
|
||||
|
|
@ -191,6 +195,11 @@ export default function Account() {
|
|||
|
||||
const { currentPlan } = useTokenUsage(autumn)
|
||||
|
||||
useEffect(() => {
|
||||
setOrgNameDraft(org?.name ?? "")
|
||||
setIsEditingOrgName(false)
|
||||
}, [org?.id, org?.name])
|
||||
|
||||
const activeMemberRoleQuery = useQuery({
|
||||
queryKey: ["organization", org?.id, "active-member-role"],
|
||||
queryFn: async () => {
|
||||
|
|
@ -346,12 +355,53 @@ 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<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
if (!canManageTeam || inviteMemberMutation.isPending) return
|
||||
inviteMemberMutation.mutate()
|
||||
}
|
||||
|
||||
const handleOrgNameSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
if (!canManageTeam || updateOrgNameMutation.isPending) return
|
||||
const trimmed = orgNameDraft.trim()
|
||||
if (!trimmed || trimmed === org?.name) {
|
||||
setOrgNameDraft(org?.name ?? "")
|
||||
setIsEditingOrgName(false)
|
||||
return
|
||||
}
|
||||
updateOrgNameMutation.mutate(trimmed)
|
||||
}
|
||||
|
||||
const planByOrgId = useMemo(() => {
|
||||
const map = new Map<string, PlanType>()
|
||||
for (const summary of orgSummaries ?? []) {
|
||||
|
|
@ -438,15 +488,130 @@ export default function Account() {
|
|||
>
|
||||
Organization
|
||||
</p>
|
||||
{canSwitchOrg ? (
|
||||
<Popover open={orgMenuOpen} onOpenChange={setOrgMenuOpen}>
|
||||
<PopoverTrigger
|
||||
{isEditingOrgName ? (
|
||||
<form
|
||||
onSubmit={handleOrgNameSubmit}
|
||||
className="flex min-w-0 max-w-full items-center gap-1.5 sm:max-w-[360px]"
|
||||
>
|
||||
<input
|
||||
value={orgNameDraft}
|
||||
onChange={(event) => setOrgNameDraft(event.target.value)}
|
||||
disabled={updateOrgNameMutation.isPending}
|
||||
autoFocus
|
||||
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",
|
||||
)}
|
||||
>
|
||||
placeholder="Organization name"
|
||||
/>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
updateOrgNameMutation.isPending ||
|
||||
!orgNameDraft.trim() ||
|
||||
orgNameDraft.trim() === org?.name
|
||||
}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-8 items-center justify-center gap-1 rounded-full border border-transparent bg-[#0D121A] px-2.5 text-[11px] font-semibold text-[#FAFAFA] shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
{updateOrgNameMutation.isPending ? (
|
||||
<LoaderIcon className="size-3 animate-spin" />
|
||||
) : null}
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={updateOrgNameMutation.isPending}
|
||||
aria-label="Cancel organization name edit"
|
||||
title="Cancel"
|
||||
onClick={() => {
|
||||
setOrgNameDraft(org?.name ?? "")
|
||||
setIsEditingOrgName(false)
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex size-8 items-center justify-center rounded-full bg-[#0D121A] text-[#737373] shadow-inside-out transition-colors hover:text-[#FAFAFA] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex min-w-0 max-w-full items-center gap-2">
|
||||
{canSwitchOrg ? (
|
||||
<Popover open={orgMenuOpen} onOpenChange={setOrgMenuOpen}>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
"flex min-w-0 max-w-full items-center gap-2 transition-opacity",
|
||||
"cursor-pointer hover:opacity-90",
|
||||
dmSans125ClassName(),
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"truncate font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
{org?.name ?? "Personal"}
|
||||
</span>
|
||||
<ChevronDown className="size-4 shrink-0 text-[#737373]" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-80 max-h-80 overflow-y-auto bg-[#1B1F24] rounded-[12px] border-white/10 p-1.5 shadow-[0px_4px_16px_rgba(0,0,0,0.4)]"
|
||||
>
|
||||
{sortedOrgsForMenu.map((organization) => {
|
||||
const isCurrent = organization.id === org?.id
|
||||
const isSwitching = switchingOrgId === organization.id
|
||||
const plan = resolveOrgPlan(
|
||||
organization.id,
|
||||
isCurrent,
|
||||
currentPlan,
|
||||
planByOrgId,
|
||||
)
|
||||
return (
|
||||
<button
|
||||
key={organization.id}
|
||||
type="button"
|
||||
disabled={isCurrent || isSwitching}
|
||||
onClick={() =>
|
||||
handleOrgSwitch(
|
||||
organization.slug,
|
||||
organization.id,
|
||||
)
|
||||
}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-3 px-3 py-2.5 rounded-[8px] text-left transition-colors",
|
||||
isCurrent
|
||||
? "bg-white/5"
|
||||
: "hover:bg-white/5 cursor-pointer",
|
||||
"disabled:opacity-60 disabled:cursor-default",
|
||||
dmSans125ClassName(),
|
||||
)}
|
||||
>
|
||||
<Building2 className="size-4 text-[#737373] shrink-0" />
|
||||
<p className="min-w-0 flex-1 truncate text-[14px] tracking-[-0.14px] text-[#FAFAFA]">
|
||||
{organization.name}
|
||||
</p>
|
||||
{isSwitching ? (
|
||||
<LoaderIcon className="size-4 shrink-0 animate-spin text-[#4BA0FA]" />
|
||||
) : isCurrent ? (
|
||||
<Check className="size-4 shrink-0 text-[#4BA0FA]" />
|
||||
) : (
|
||||
<span className="size-4 shrink-0" aria-hidden />
|
||||
)}
|
||||
<OrgPlanBadge plan={plan} />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
|
|
@ -455,67 +620,24 @@ export default function Account() {
|
|||
>
|
||||
{org?.name ?? "Personal"}
|
||||
</span>
|
||||
<ChevronDown className="size-4 shrink-0 text-[#737373]" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
className="w-80 max-h-80 overflow-y-auto bg-[#1B1F24] rounded-[12px] border-white/10 p-1.5 shadow-[0px_4px_16px_rgba(0,0,0,0.4)]"
|
||||
>
|
||||
{sortedOrgsForMenu.map((organization) => {
|
||||
const isCurrent = organization.id === org?.id
|
||||
const isSwitching = switchingOrgId === organization.id
|
||||
const plan = resolveOrgPlan(
|
||||
organization.id,
|
||||
isCurrent,
|
||||
currentPlan,
|
||||
planByOrgId,
|
||||
)
|
||||
return (
|
||||
<button
|
||||
key={organization.id}
|
||||
type="button"
|
||||
disabled={isCurrent || isSwitching}
|
||||
onClick={() =>
|
||||
handleOrgSwitch(
|
||||
organization.slug,
|
||||
organization.id,
|
||||
)
|
||||
}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-3 px-3 py-2.5 rounded-[8px] text-left transition-colors",
|
||||
isCurrent
|
||||
? "bg-white/5"
|
||||
: "hover:bg-white/5 cursor-pointer",
|
||||
"disabled:opacity-60 disabled:cursor-default",
|
||||
dmSans125ClassName(),
|
||||
)}
|
||||
>
|
||||
<Building2 className="size-4 text-[#737373] shrink-0" />
|
||||
<p className="min-w-0 flex-1 truncate text-[14px] tracking-[-0.14px] text-[#FAFAFA]">
|
||||
{organization.name}
|
||||
</p>
|
||||
{isSwitching ? (
|
||||
<LoaderIcon className="size-4 shrink-0 animate-spin text-[#4BA0FA]" />
|
||||
) : isCurrent ? (
|
||||
<Check className="size-4 shrink-0 text-[#4BA0FA]" />
|
||||
) : (
|
||||
<span className="size-4 shrink-0" aria-hidden />
|
||||
)}
|
||||
<OrgPlanBadge plan={plan} />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"truncate font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
{org?.name ?? "Personal"}
|
||||
</span>
|
||||
{canManageTeam ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Edit organization name"
|
||||
title="Edit organization name"
|
||||
onClick={() => {
|
||||
setOrgNameDraft(org?.name ?? "")
|
||||
setIsEditingOrgName(true)
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex size-7 shrink-0 items-center justify-center rounded-md text-[#FAFAFA] transition-colors hover:bg-white/5",
|
||||
)}
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue