mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
add team management in nova account settings
This commit is contained in:
parent
53ae4af5ab
commit
984eace2f2
2 changed files with 469 additions and 85 deletions
|
|
@ -3,6 +3,7 @@
|
|||
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 { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
|
||||
import {
|
||||
|
|
@ -12,9 +13,35 @@ import {
|
|||
type PlanType,
|
||||
} from "@/hooks/use-token-usage"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/select"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/dropdown-menu"
|
||||
import { useCustomer } from "autumn-js/react"
|
||||
import { Check, LoaderIcon, ChevronDown, Building2, Users } from "lucide-react"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import {
|
||||
Check,
|
||||
LoaderIcon,
|
||||
ChevronDown,
|
||||
Building2,
|
||||
Users,
|
||||
UserPlus,
|
||||
Mail,
|
||||
MoreHorizontal,
|
||||
UserMinus,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { useMemo, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
|
@ -69,6 +96,21 @@ const ROLE_LABELS: Record<string, string> = {
|
|||
member: "Member",
|
||||
}
|
||||
|
||||
type InviteRole = "admin" | "member"
|
||||
|
||||
function getErrorMessage(error: unknown, fallback: string) {
|
||||
if (error instanceof Error && error.message) return error.message
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"message" in error &&
|
||||
typeof error.message === "string"
|
||||
) {
|
||||
return error.message
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
function formatRole(role: string): string {
|
||||
const r = role?.toLowerCase() ?? ""
|
||||
if (ROLE_LABELS[r]) return ROLE_LABELS[r]
|
||||
|
|
@ -93,6 +135,17 @@ function RolePill({ role }: { role: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
function isPendingInvitation(invitation: {
|
||||
status?: string
|
||||
expiresAt?: Date | string
|
||||
}) {
|
||||
if (invitation.status && invitation.status.toLowerCase() !== "pending") {
|
||||
return false
|
||||
}
|
||||
if (!invitation.expiresAt) return true
|
||||
return new Date(invitation.expiresAt).getTime() > Date.now()
|
||||
}
|
||||
|
||||
function resolveOrgPlan(
|
||||
orgId: string,
|
||||
isCurrent: boolean,
|
||||
|
|
@ -106,10 +159,18 @@ function resolveOrgPlan(
|
|||
}
|
||||
|
||||
export default function Account() {
|
||||
const { user, org, organizations: allOrgs, setActiveOrg } = useAuth()
|
||||
const {
|
||||
user,
|
||||
org,
|
||||
organizations: allOrgs,
|
||||
setActiveOrg,
|
||||
refetchActiveOrg,
|
||||
} = useAuth()
|
||||
const autumn = useCustomer()
|
||||
const [switchingOrgId, setSwitchingOrgId] = useState<string | null>(null)
|
||||
const [orgMenuOpen, setOrgMenuOpen] = useState(false)
|
||||
const [inviteEmail, setInviteEmail] = useState("")
|
||||
const [inviteRole, setInviteRole] = useState<InviteRole>("member")
|
||||
const canSwitchOrg = (allOrgs?.length ?? 0) > 1
|
||||
const { data: orgSummaries } = useOrgSummaries()
|
||||
|
||||
|
|
@ -127,6 +188,123 @@ export default function Account() {
|
|||
|
||||
const { currentPlan } = useTokenUsage(autumn)
|
||||
|
||||
const currentMember = useMemo(
|
||||
() => org?.members?.find((member) => member.userId === user?.id) ?? null,
|
||||
[org?.members, user?.id],
|
||||
)
|
||||
const currentRole = currentMember?.role?.toLowerCase() ?? "member"
|
||||
const canManageTeam = currentRole === "owner" || currentRole === "admin"
|
||||
const isOwner = currentRole === "owner"
|
||||
|
||||
const pendingInvitations = useMemo(
|
||||
() => (org?.invitations ?? []).filter(isPendingInvitation),
|
||||
[org?.invitations],
|
||||
)
|
||||
|
||||
const inviteMemberMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!org?.id) throw new Error("No active organization")
|
||||
const email = inviteEmail.trim().toLowerCase()
|
||||
if (!email) throw new Error("Enter an email address")
|
||||
const result = await authClient.organization.inviteMember({
|
||||
email,
|
||||
role: inviteRole,
|
||||
organizationId: org.id,
|
||||
resend: true,
|
||||
})
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message ?? "Failed to invite teammate")
|
||||
}
|
||||
return result.data
|
||||
},
|
||||
onSuccess: async (invitation) => {
|
||||
setInviteEmail("")
|
||||
await refetchActiveOrg()
|
||||
toast.success("Invitation sent", {
|
||||
description: invitation?.email
|
||||
? `${invitation.email} can now join ${org?.name ?? "your organization"}.`
|
||||
: undefined,
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error, "Failed to invite teammate"))
|
||||
},
|
||||
})
|
||||
|
||||
const updateMemberRoleMutation = useMutation({
|
||||
mutationFn: async ({
|
||||
memberId,
|
||||
role,
|
||||
}: {
|
||||
memberId: string
|
||||
role: InviteRole
|
||||
}) => {
|
||||
if (!org?.id) throw new Error("No active organization")
|
||||
const result = await authClient.organization.updateMemberRole({
|
||||
memberId,
|
||||
role,
|
||||
organizationId: org.id,
|
||||
})
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message ?? "Failed to update role")
|
||||
}
|
||||
return result.data
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await refetchActiveOrg()
|
||||
toast.success("Role updated")
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error, "Failed to update role"))
|
||||
},
|
||||
})
|
||||
|
||||
const removeMemberMutation = useMutation({
|
||||
mutationFn: async (memberIdOrEmail: string) => {
|
||||
if (!org?.id) throw new Error("No active organization")
|
||||
const result = await authClient.organization.removeMember({
|
||||
memberIdOrEmail,
|
||||
organizationId: org.id,
|
||||
})
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message ?? "Failed to remove member")
|
||||
}
|
||||
return result.data
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await refetchActiveOrg()
|
||||
toast.success("Member removed")
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error, "Failed to remove member"))
|
||||
},
|
||||
})
|
||||
|
||||
const cancelInvitationMutation = useMutation({
|
||||
mutationFn: async (invitationId: string) => {
|
||||
const result = await authClient.organization.cancelInvitation({
|
||||
invitationId,
|
||||
})
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message ?? "Failed to cancel invitation")
|
||||
}
|
||||
return result.data
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await refetchActiveOrg()
|
||||
toast.success("Invitation canceled")
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error, "Failed to cancel invitation"))
|
||||
},
|
||||
})
|
||||
|
||||
const handleInviteSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
if (!canManageTeam || inviteMemberMutation.isPending) return
|
||||
inviteMemberMutation.mutate()
|
||||
}
|
||||
|
||||
const planByOrgId = useMemo(() => {
|
||||
const map = new Map<string, PlanType>()
|
||||
for (const summary of orgSummaries ?? []) {
|
||||
|
|
@ -318,8 +496,19 @@ export default function Account() {
|
|||
</section>
|
||||
|
||||
<section id="team-members" className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<SectionTitle>Team members</SectionTitle>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 px-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<SectionTitle>Team members</SectionTitle>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] tracking-[-0.13px] text-[#737373] px-2",
|
||||
)}
|
||||
>
|
||||
Invite people into {org?.name ?? "your organization"} and manage
|
||||
their access.
|
||||
</p>
|
||||
</div>
|
||||
{(org?.members?.length ?? 0) > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
|
|
@ -333,101 +522,287 @@ export default function Account() {
|
|||
)}
|
||||
</div>
|
||||
<SettingsCard>
|
||||
{org?.members && org.members.length > 0 ? (
|
||||
<ul className="flex flex-col">
|
||||
{[...org.members]
|
||||
.sort((a, b) => {
|
||||
const rolePriority = (r: string) =>
|
||||
r === "owner" ? 0 : r === "admin" ? 1 : 2
|
||||
const diff =
|
||||
rolePriority(a.role.toLowerCase()) -
|
||||
rolePriority(b.role.toLowerCase())
|
||||
if (diff !== 0) return diff
|
||||
return (a.user?.name ?? "").localeCompare(b.user?.name ?? "")
|
||||
})
|
||||
.map((m, idx) => {
|
||||
const isYou = m.userId === user?.id
|
||||
const name = m.user?.name ?? m.user?.email ?? "Unknown"
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
{canManageTeam ? (
|
||||
<form
|
||||
onSubmit={handleInviteSubmit}
|
||||
className="grid grid-cols-1 gap-3 md:grid-cols-[minmax(0,1fr)_132px_auto]"
|
||||
>
|
||||
<label className="sr-only" htmlFor="team-invite-email">
|
||||
Email address
|
||||
</label>
|
||||
<div className="relative min-w-0">
|
||||
<Mail className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-[#737373]" />
|
||||
<input
|
||||
id="team-invite-email"
|
||||
type="email"
|
||||
value={inviteEmail}
|
||||
onChange={(event) => setInviteEmail(event.target.value)}
|
||||
placeholder="teammate@company.com"
|
||||
autoComplete="email"
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"h-10 w-full rounded-[10px] border border-white/[0.08] bg-[#0D0F14] pl-9 pr-3 text-[14px] text-[#FAFAFA] placeholder:text-[#525D6E] outline-none transition-colors focus:border-[#4BA0FA]/50",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
value={inviteRole}
|
||||
onValueChange={(value) => setInviteRole(value as InviteRole)}
|
||||
>
|
||||
<SelectTrigger className="h-10 rounded-[10px] border-white/[0.08] bg-[#0D0F14] text-[#FAFAFA]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={
|
||||
!inviteEmail.trim() ||
|
||||
!org?.id ||
|
||||
inviteMemberMutation.isPending
|
||||
}
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"inline-flex h-10 items-center justify-center gap-2 rounded-[10px] bg-[#4BA0FA] px-4 text-[14px] font-semibold text-[#00171A] transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-45",
|
||||
)}
|
||||
>
|
||||
{inviteMemberMutation.isPending ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : (
|
||||
<UserPlus className="size-4" />
|
||||
)}
|
||||
Invite
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] p-3">
|
||||
<div className="size-9 rounded-full bg-white/[0.04] flex items-center justify-center shrink-0">
|
||||
<Users className="size-4 text-[#737373]" />
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[13px] tracking-[-0.13px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Only organization owners and admins can invite teammates or
|
||||
change roles.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pendingInvitations.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[11px] uppercase tracking-[0.12em] text-[#737373] font-mono",
|
||||
)}
|
||||
>
|
||||
Pending invitations
|
||||
</p>
|
||||
<ul className="flex flex-col rounded-[12px] border border-white/[0.05] overflow-hidden">
|
||||
{pendingInvitations.map((invitation) => (
|
||||
<li
|
||||
key={m.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 py-2.5",
|
||||
idx > 0 && "border-t border-white/[0.04]",
|
||||
)}
|
||||
key={invitation.id}
|
||||
className="flex items-center gap-3 px-3 py-2.5 border-t border-white/[0.04] first:border-t-0 bg-white/[0.015]"
|
||||
>
|
||||
<Avatar className="size-9 shrink-0 bg-[#0D121A]">
|
||||
<AvatarImage
|
||||
src={m.user?.image ?? ""}
|
||||
alt={name}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="bg-transparent text-white text-[13px] font-medium">
|
||||
{(name.charAt(0) || "U").toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[14px] tracking-[-0.14px] text-[#FAFAFA] truncate",
|
||||
)}
|
||||
<div className="size-9 rounded-full bg-[#0D121A] flex items-center justify-center shrink-0">
|
||||
<Mail className="size-4 text-[#737373]" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"truncate text-[14px] font-medium tracking-[-0.14px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
{invitation.email}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] tracking-[-0.12px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Invited as {formatRole(invitation.role)}
|
||||
</p>
|
||||
</div>
|
||||
{canManageTeam && (
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
disabled={cancelInvitationMutation.isPending}
|
||||
onClick={() =>
|
||||
cancelInvitationMutation.mutate(invitation.id)
|
||||
}
|
||||
className="flex size-8 items-center justify-center rounded-[8px] text-[#8A5247] hover:bg-[#1A0F0C]/60 hover:text-[#C73B1B] disabled:opacity-50"
|
||||
aria-label={`Cancel invitation for ${invitation.email}`}
|
||||
title="Cancel invitation"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{isYou && (
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{org?.members && org.members.length > 0 ? (
|
||||
<ul className="flex flex-col">
|
||||
{[...org.members]
|
||||
.sort((a, b) => {
|
||||
const rolePriority = (r: string) =>
|
||||
r === "owner" ? 0 : r === "admin" ? 1 : 2
|
||||
const diff =
|
||||
rolePriority(a.role.toLowerCase()) -
|
||||
rolePriority(b.role.toLowerCase())
|
||||
if (diff !== 0) return diff
|
||||
return (a.user?.name ?? "").localeCompare(
|
||||
b.user?.name ?? "",
|
||||
)
|
||||
})
|
||||
.map((m, idx) => {
|
||||
const isYou = m.userId === user?.id
|
||||
const memberRole = m.role.toLowerCase()
|
||||
const name = m.user?.name ?? m.user?.email ?? "Unknown"
|
||||
const canEditMember =
|
||||
canManageTeam && !isYou && memberRole !== "owner"
|
||||
return (
|
||||
<li
|
||||
key={m.id}
|
||||
className={cn(
|
||||
"flex items-center gap-3 py-2.5",
|
||||
idx > 0 && "border-t border-white/[0.04]",
|
||||
)}
|
||||
>
|
||||
<Avatar className="size-9 shrink-0 bg-[#0D121A]">
|
||||
<AvatarImage
|
||||
src={m.user?.image ?? ""}
|
||||
alt={name}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="bg-transparent text-white text-[13px] font-medium">
|
||||
{(name.charAt(0) || "U").toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[10.5px] uppercase tracking-[0.1em] text-[#737373] font-mono",
|
||||
"font-medium text-[14px] tracking-[-0.14px] text-[#FAFAFA] truncate",
|
||||
)}
|
||||
>
|
||||
You
|
||||
{name}
|
||||
</span>
|
||||
{isYou && (
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[10.5px] uppercase tracking-[0.1em] text-[#737373] font-mono",
|
||||
)}
|
||||
>
|
||||
You
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{m.user?.email && (
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] tracking-[-0.12px] text-[#737373] truncate",
|
||||
)}
|
||||
>
|
||||
{m.user.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{m.user?.email && (
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] tracking-[-0.12px] text-[#737373] truncate",
|
||||
)}
|
||||
{canEditMember ? (
|
||||
<Select
|
||||
value={memberRole}
|
||||
onValueChange={(value) => {
|
||||
if (value === memberRole) return
|
||||
updateMemberRoleMutation.mutate({
|
||||
memberId: m.id,
|
||||
role: value as InviteRole,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{m.user.email}
|
||||
</span>
|
||||
<SelectTrigger className="h-8 w-[112px] rounded-[8px] border-white/[0.08] bg-[#0D0F14] text-[#FAFAFA]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<RolePill role={m.role} />
|
||||
)}
|
||||
</div>
|
||||
<RolePill role={m.role} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<div className="size-9 rounded-full bg-white/[0.04] flex items-center justify-center shrink-0">
|
||||
<Users className="size-4 text-[#737373]" />
|
||||
{canEditMember && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex size-8 shrink-0 items-center justify-center rounded-[8px] text-[#737373] hover:bg-white/[0.05] hover:text-[#FAFAFA]"
|
||||
aria-label={`Team actions for ${name}`}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<DropdownMenuItem
|
||||
className="text-[#C73B1B] focus:text-[#C73B1B]"
|
||||
onSelect={() =>
|
||||
removeMemberMutation.mutate(m.id)
|
||||
}
|
||||
disabled={
|
||||
removeMemberMutation.isPending || !isOwner
|
||||
}
|
||||
>
|
||||
<UserMinus className="size-4" />
|
||||
Remove member
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<div className="size-9 rounded-full bg-white/[0.04] flex items-center justify-center shrink-0">
|
||||
<Users className="size-4 text-[#737373]" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[14px] tracking-[-0.14px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Just you for now
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] tracking-[-0.12px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Invite teammates to start collaborating.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"font-medium text-[14px] tracking-[-0.14px] text-[#FAFAFA]",
|
||||
)}
|
||||
>
|
||||
Just you for now
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
dmSans125ClassName(),
|
||||
"text-[12px] tracking-[-0.12px] text-[#737373]",
|
||||
)}
|
||||
>
|
||||
Invite teammates from your organization settings.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ interface AuthContextType {
|
|||
setActiveOrg: (orgSlug: string) => Promise<void>
|
||||
clearActiveOrg: () => void
|
||||
updateOrgMetadata: (partial: Record<string, unknown>) => void
|
||||
refetchActiveOrg: () => Promise<Organization | null>
|
||||
refetchOrganizations: () => Promise<unknown>
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +82,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
})
|
||||
}, [])
|
||||
|
||||
const refetchActiveOrg = useCallback(async () => {
|
||||
const full = await authClient.organization.getFullOrganization()
|
||||
const nextOrg = full?.data ?? null
|
||||
setOrg(nextOrg)
|
||||
return nextOrg
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isSessionPending) return
|
||||
|
||||
|
|
@ -198,6 +206,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
setActiveOrg,
|
||||
clearActiveOrg,
|
||||
updateOrgMetadata,
|
||||
refetchActiveOrg,
|
||||
refetchOrganizations,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue