Skip to content

Commit bee8b12

Browse files
committed
feat: allow api orgs to nova app (supermemoryai#728)
1 parent 5dd358a commit bee8b12

2 files changed

Lines changed: 106 additions & 31 deletions

File tree

apps/web/components/new/settings/account.tsx

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ import {
1111
DialogTrigger,
1212
DialogClose,
1313
} from "@ui/components/dialog"
14+
import { authClient } from "@lib/auth"
15+
import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover"
1416
import { useCustomer } from "autumn-js/react"
15-
import { Check, X, Trash2, LoaderIcon, Settings } from "lucide-react"
17+
import {
18+
Check,
19+
X,
20+
Trash2,
21+
LoaderIcon,
22+
Settings,
23+
ChevronDown,
24+
Building2,
25+
} from "lucide-react"
1626
import { useState } from "react"
1727

1828
function SectionTitle({ children }: { children: React.ReactNode }) {
@@ -76,11 +86,25 @@ function PlanFeatureRow({
7686
}
7787

7888
export default function Account() {
79-
const { user, org } = useAuth()
89+
const { user, org, setActiveOrg } = useAuth()
8090
const autumn = useCustomer()
8191
const [isUpgrading, setIsUpgrading] = useState(false)
8292
const [deleteConfirmText, setDeleteConfirmText] = useState("")
8393
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
94+
const [switchingOrgId, setSwitchingOrgId] = useState<string | null>(null)
95+
const { data: allOrgs } = authClient.useListOrganizations()
96+
97+
const handleOrgSwitch = async (orgSlug: string, orgId: string) => {
98+
if (orgId === org?.id) return
99+
setSwitchingOrgId(orgId)
100+
try {
101+
await setActiveOrg(orgSlug)
102+
window.location.reload()
103+
} catch (error) {
104+
console.error("Failed to switch organization:", error)
105+
setSwitchingOrgId(null)
106+
}
107+
}
84108

85109
const {
86110
memoriesUsed,
@@ -163,7 +187,6 @@ export default function Account() {
163187
</div>
164188
</div>
165189

166-
{/* Organization + Member since */}
167190
<div className="flex gap-4">
168191
<div className="flex-1 flex flex-col gap-2">
169192
<p
@@ -174,14 +197,76 @@ export default function Account() {
174197
>
175198
Organization
176199
</p>
177-
<p
178-
className={cn(
179-
dmSans125ClassName(),
180-
"font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
200+
<Popover>
201+
<PopoverTrigger
202+
className={cn(
203+
"flex items-center gap-2 cursor-pointer transition-opacity hover:opacity-90",
204+
dmSans125ClassName(),
205+
)}
206+
>
207+
<span
208+
className={cn(
209+
dmSans125ClassName(),
210+
"font-medium text-[16px] tracking-[-0.16px] text-[#FAFAFA]",
211+
)}
212+
>
213+
{org?.name ?? "Personal"}
214+
</span>
215+
<ChevronDown className="size-4 text-[#737373]" />
216+
</PopoverTrigger>
217+
{allOrgs && allOrgs.length > 1 && (
218+
<PopoverContent
219+
align="start"
220+
className="w-72 bg-[#1B1F24] rounded-[12px] border-white/10 p-1.5 shadow-[0px_4px_16px_rgba(0,0,0,0.4)]"
221+
>
222+
{allOrgs.map((organization) => {
223+
const isCurrent = organization.id === org?.id
224+
const isSwitching = switchingOrgId === organization.id
225+
const isConsumer =
226+
organization.metadata?.isConsumer === true
227+
return (
228+
<button
229+
key={organization.id}
230+
type="button"
231+
disabled={isCurrent || isSwitching}
232+
onClick={() =>
233+
handleOrgSwitch(
234+
organization.slug,
235+
organization.id,
236+
)
237+
}
238+
className={cn(
239+
"w-full flex items-center gap-3 px-3 py-2.5 rounded-[8px] text-left transition-colors",
240+
isCurrent
241+
? "bg-white/5"
242+
: "hover:bg-white/5 cursor-pointer",
243+
"disabled:opacity-60 disabled:cursor-default",
244+
dmSans125ClassName(),
245+
)}
246+
>
247+
<Building2 className="size-4 text-[#737373] shrink-0" />
248+
<div className="flex-1 min-w-0 flex items-center gap-2">
249+
<p className="text-[14px] tracking-[-0.14px] text-[#FAFAFA] truncate">
250+
{organization.name}
251+
</p>
252+
{isCurrent && (
253+
<Check className="size-4 text-[#4BA0FA] shrink-0" />
254+
)}
255+
{isSwitching && (
256+
<LoaderIcon className="size-4 text-[#4BA0FA] shrink-0 animate-spin" />
257+
)}
258+
</div>
259+
{!isConsumer && (
260+
<span className="text-[11px] font-medium tracking-[0.3px] px-1.5 py-0.5 rounded-[4px] shrink-0 bg-[#737373]/15 text-[#737373]">
261+
API
262+
</span>
263+
)}
264+
</button>
265+
)
266+
})}
267+
</PopoverContent>
181268
)}
182-
>
183-
{org?.name ?? "Personal"}
184-
</p>
269+
</Popover>
185270
</div>
186271
<div className="flex-1 flex flex-col gap-2">
187272
<p

packages/lib/auth-context.tsx

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
3535
organizationSlug: slug,
3636
})
3737
setOrg(activeOrg)
38+
localStorage.setItem("supermemory-consumer-last-org-slug", slug)
3839
}
3940

4041
const updateOrgMetadata = useCallback((partial: Record<string, unknown>) => {
@@ -52,28 +53,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
5253

5354
// biome-ignore lint/correctness/useExhaustiveDependencies: ignoring the setActiveOrg dependency
5455
useEffect(() => {
55-
if (session?.session.activeOrganizationId) {
56-
authClient.organization
57-
.getFullOrganization()
58-
.then((org) => {
59-
if (org.metadata?.isConsumer === true) {
60-
console.log("Consumer organization:", org)
61-
setOrg(org)
62-
} else {
63-
console.log("ALl orgs:", orgs)
64-
const consumerOrg = orgs?.find(
65-
(o) => o.metadata?.isConsumer === true,
66-
)
67-
if (consumerOrg) {
68-
setActiveOrg(consumerOrg.slug)
69-
}
70-
}
71-
})
72-
.catch((error) => {
73-
// Silently handle organization fetch failures to prevent unhandled rejections
74-
console.error("Failed to fetch organization:", error)
75-
})
56+
if (!session?.session.activeOrganizationId || !orgs) return
57+
58+
const savedSlug = localStorage.getItem("supermemory-consumer-last-org-slug")
59+
60+
if (savedSlug && orgs.find((o) => o.slug === savedSlug)) {
61+
setActiveOrg(savedSlug)
62+
return
7663
}
64+
65+
if (savedSlug) localStorage.removeItem("supermemory-consumer-last-org-slug")
66+
authClient.organization.getFullOrganization().then(setOrg)
7767
}, [session?.session.activeOrganizationId, orgs])
7868

7969
// When a session exists and there is a pending login method recorded,

0 commit comments

Comments
 (0)