Changeset 0.37.14 (#226)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-23 16:47:48 +00:00
committed by xcaliber
parent fcb998bff9
commit b7746c3004
164 changed files with 6972 additions and 3527 deletions

View File

@@ -13,7 +13,7 @@
* extraItems — Additional menu items inserted after surface links
*/
const { html } = window;
const { useState, useRef, useMemo } = hooks;
const { useState, useRef, useMemo, useEffect } = hooks;
import { Avatar } from '../primitives/avatar.js';
import { Menu } from '../primitives/menu.js';
@@ -27,12 +27,23 @@ function _currentSurface() {
export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
const [open, setOpen] = useState(false);
const [extSurfaces, setExtSurfaces] = useState([]);
const btnRef = useRef(null);
const sw = window.sw;
const user = sw?.auth?.user;
const authenticated = sw?.auth?.isAuthenticated;
// Fetch extension surfaces once on mount
useEffect(() => {
if (!sw?.api?.surfaces?.list) return;
sw.api.surfaces.list().then(data => {
const all = data || [];
const CORE = new Set(['chat', 'admin', 'notes', 'settings', 'team-admin', 'workflow', 'workflow-landing']);
setExtSurfaces(all.filter(s => !CORE.has(s.id)));
}).catch(() => {});
}, [authenticated]);
const items = useMemo(() => {
const current = _currentSurface();
const list = [];
@@ -44,10 +55,9 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
{ key: 'notes', label: 'Notes', icon: '\ud83d\udcdd' },
];
// Add extension surfaces from page data if available
const extSurfaces = window.__EXTENSION_SURFACES__ || [];
// Add extension surfaces fetched from API
for (const ext of extSurfaces) {
surfaces.push({ key: ext.id || ext.route, label: ext.title, icon: '\ud83e\udde9' });
surfaces.push({ key: ext.id, label: ext.title, icon: '\ud83e\udde9', route: ext.route });
}
const surfaceItems = surfaces
@@ -72,18 +82,18 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
}
// Admin — requires admin role
if ((!authenticated || sw?.isAdmin) && current !== 'admin') {
if (authenticated && sw?.isAdmin && current !== 'admin') {
list.push({ label: 'Admin', action: 'admin', icon: '\ud83d\udee1\ufe0f' });
}
// Team Admin — requires admin role on at least one team
const hasTeamAdmin = sw?.auth?.teams?.some(t => t.my_role === 'admin');
if ((!authenticated || hasTeamAdmin) && current !== 'team-admin') {
if (authenticated && hasTeamAdmin && current !== 'team-admin') {
list.push({ label: 'Team Admin', action: 'team-admin', icon: '\ud83d\udc65' });
}
// Debug — admin only
if (!authenticated || sw?.isAdmin) {
if (authenticated && sw?.isAdmin) {
list.push({ label: 'Debug', action: 'debug', icon: '\ud83d\udc1b' });
}
@@ -91,7 +101,7 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
list.push({ label: 'Sign Out', action: 'sign-out' });
return list;
}, [user, authenticated, extraItems]);
}, [user, authenticated, extraItems, extSurfaces]);
function handleSelect(action) {
setOpen(false);
@@ -101,7 +111,9 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
const BASE = window.__BASE__ || '';
switch (action) {
case 'sign-out':
window.sw?.auth?.logout();
window.sw?.auth?.logout().then(() => {
location.href = BASE + '/login';
});
break;
case 'debug':
const dbg = document.getElementById('debugModal');
@@ -110,8 +122,12 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
case 'chat':
location.href = BASE + '/';
break;
default:
location.href = BASE + '/' + action;
default: {
// Extension surfaces use /s/{id} routes
const ext = extSurfaces.find(s => s.id === action);
location.href = BASE + (ext?.route || '/' + action);
break;
}
}
}
@@ -119,17 +135,20 @@ export function UserMenu({ placement = 'down-right', onAction, extraItems }) {
const avatar = user?.avatar || null;
return html`
<button class="sw-user-menu__trigger" ref=${btnRef}
onClick=${() => setOpen(!open)} aria-label="User menu">
<${Avatar} src=${avatar} name=${displayName} size="sm" />
</button>
<${Menu}
open=${open}
anchor=${btnRef.current}
direction=${placement}
items=${items}
onSelect=${handleSelect}
onClose=${() => setOpen(false)}
/>
<div class="user-menu-wrap">
<button class="user-btn" ref=${btnRef}
onClick=${() => setOpen(!open)} aria-label="User menu">
<${Avatar} src=${avatar} name=${displayName} size="sm" />
<span class="sb-label">${displayName}</span>
</button>
<${Menu}
open=${open}
anchor=${btnRef.current}
direction=${placement}
items=${items}
onSelect=${handleSelect}
onClose=${() => setOpen(false)}
/>
</div>
`;
}