Switch organizations from the Learning space card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-26 09:59:22 +07:00
parent 51700bc0f3
commit 6d4850f4ce
4 changed files with 62 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { useClerk, useOrganization, useUser } from '@clerk/react'
import { useClerk, useOrganization, useOrganizationList, useUser } from '@clerk/react'
import { useQuery } from '@tanstack/react-query'
import { NavLink, Outlet, useLocation } from 'react-router-dom'
import { LANGS, locale, setLang, t, useLang, type Key } from '../i18n'
@@ -70,11 +70,57 @@ function CreditBalance() {
)
}
// The Learning space card switches the active organization. AuthGate keys the pages by orgId and
// clears the query cache, so Brain, schedules, results and credits all reload for the new one.
function OrgSwitcher() {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
const { organization } = useOrganization()
const { setActive, userMemberships } = useOrganizationList({ userMemberships: { infinite: true } })
useEffect(() => {
if (!open) return
const onClick = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false) }
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) }
document.addEventListener('mousedown', onClick)
document.addEventListener('keydown', onKey)
return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey) }
}, [open])
const choose = (id: string) => {
setOpen(false)
if (id !== organization?.id) void setActive?.({ organization: id })
}
return (
<div className="workspace-switch" ref={ref}>
<button type="button" className="workspace" aria-haspopup="listbox" aria-expanded={open}
title={t('layout.switchOrg')} onClick={() => setOpen(o => !o)}>
<span className="workspace-icon">{(organization?.name || 'L').charAt(0).toUpperCase()}</span>
<div>{t('layout.learningSpace')}<small>{organization?.name ?? '-'}</small></div>
<Icon name="chevron-updown" />
</button>
{open && (
<ul className="workspace-menu" role="listbox" aria-label={t('layout.switchOrg')}>
{userMemberships.data?.map(({ organization: org }) => (
<li key={org.id} role="option" aria-selected={org.id === organization?.id}>
<button type="button" onClick={() => choose(org.id)}>
<span className="workspace-icon">{org.name.charAt(0).toUpperCase()}</span>
<span>{org.name}</span>
{org.id === organization?.id && <Icon name="check" />}
</button>
</li>
))}
</ul>
)}
</div>
)
}
export function AppLayout() {
useLang()
const [help, setHelp] = useState(false)
const { user } = useUser()
const { organization } = useOrganization()
const { signOut } = useClerk()
const { pathname } = useLocation()
const current = NAV.find(n => pathname.startsWith(n.to))
@@ -84,10 +130,7 @@ export function AppLayout() {
<div className="app">
<aside className="sidebar">
<NavLink to="/brain"><Brand /></NavLink>
<div className="workspace">
<span className="workspace-icon">{(organization?.name || 'L').charAt(0).toUpperCase()}</span>
<div>{t('layout.learningSpace')}<small>{organization?.name ?? '-'}</small></div>
</div>
<OrgSwitcher />
<div className="nav-label">{t('nav.workspace')}</div>
<nav aria-label={t('layout.mainNav')}>
{NAV.map(n => (