Switch organizations from the Learning space card
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 => (
|
||||
|
||||
@@ -16,6 +16,7 @@ const paths: Record<string, string> = {
|
||||
search: '<circle cx="10" cy="10" r="6"/><path d="m15 15 5 5"/>',
|
||||
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7v.2"/>',
|
||||
check: '<path d="m5 12 4 4L19 6"/>',
|
||||
'chevron-updown': '<path d="m8 9 4-4 4 4M8 15l4 4 4-4"/>',
|
||||
logout:'<path d="M10 4H4v16h6M10 12h11m-4-4 4 4-4 4"/>',
|
||||
plus: '<path d="M12 4v16M4 12h16"/>',
|
||||
trash: '<path d="M3 6h18M9 3h6l1 3M6 6l1 15h10l1-15M10 10v7M14 10v7"/>',
|
||||
|
||||
@@ -13,6 +13,7 @@ const en = {
|
||||
'nav.results': 'Results',
|
||||
'nav.workspace': 'WORKSPACE',
|
||||
'layout.learningSpace': 'Learning space',
|
||||
'layout.switchOrg': 'Switch organization',
|
||||
'layout.breadcrumb': 'Workspace',
|
||||
'layout.signOut': 'Sign out',
|
||||
'layout.engine': 'learning-lab engine',
|
||||
@@ -395,6 +396,7 @@ const id: Record<Key, string> = {
|
||||
'nav.results': 'Hasil',
|
||||
'nav.workspace': 'RUANG KERJA',
|
||||
'layout.learningSpace': 'Ruang belajar',
|
||||
'layout.switchOrg': 'Ganti organisasi',
|
||||
'layout.breadcrumb': 'Ruang kerja',
|
||||
'layout.signOut': 'Keluar',
|
||||
'layout.engine': 'Engine learning-lab',
|
||||
|
||||
@@ -54,6 +54,15 @@ input::placeholder,textarea::placeholder{color:#98a2b3}
|
||||
.sidebar .brand-mark{width:38px;height:38px;border-radius:12px}
|
||||
.workspace{border:1px solid var(--line);background:#f8faff;border-radius:14px;padding:10px;margin-bottom:26px;font-size:13.5px}
|
||||
.workspace small{font-size:12px}
|
||||
.workspace-switch{position:relative;margin-bottom:26px}
|
||||
button.workspace{width:100%;margin:0;text-align:left;font:inherit;font-size:13.5px;color:inherit;cursor:pointer;transition:border-color .15s,background .15s}
|
||||
button.workspace:hover,button.workspace[aria-expanded=true]{border-color:var(--soft);background:#f2f7ff}
|
||||
.workspace-menu{position:absolute;z-index:6;top:calc(100% + 6px);left:0;right:0;max-height:280px;overflow:auto;margin:0;padding:6px;list-style:none;background:#fff;border:1px solid var(--line);border-radius:14px;box-shadow:0 16px 32px -12px rgba(16,24,40,.22)}
|
||||
.workspace-menu button{display:flex;align-items:center;gap:10px;width:100%;padding:8px;border:0;border-radius:10px;background:none;font:inherit;font-size:13.5px;font-weight:550;color:var(--ink);text-align:left;cursor:pointer}
|
||||
.workspace-menu button:hover{background:#f2f7ff}
|
||||
.workspace-menu button>span:nth-child(2){flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.workspace-menu .workspace-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.workspace-menu svg{width:16px;height:16px;color:var(--blue)}
|
||||
.workspace-icon{background:var(--light);color:var(--blue-dark);border-radius:9px;width:30px;height:30px;font-weight:800}
|
||||
.nav-label{color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase;margin:0 12px 10px}
|
||||
.sidebar nav{gap:4px}
|
||||
|
||||
Reference in New Issue
Block a user