mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-18 10:36:22 +08:00
feat: improve admin panel design (#14464)
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { buttonVariants } from '../../components/ui/button';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
export const NormalSubItem = ({
|
||||
module,
|
||||
title,
|
||||
changeModule,
|
||||
indent = 'normal',
|
||||
}: {
|
||||
module: string;
|
||||
title: string;
|
||||
changeModule?: (module: string) => void;
|
||||
indent?: 'normal' | 'nested';
|
||||
}) => {
|
||||
const handleClick = useCallback(() => {
|
||||
changeModule?.(module);
|
||||
}, [changeModule, module]);
|
||||
const indentClassName = indent === 'nested' ? 'ml-12' : 'ml-8';
|
||||
return (
|
||||
<div className="w-full flex">
|
||||
<NavLink
|
||||
to={`/admin/settings/${module}`}
|
||||
onClick={handleClick}
|
||||
className={({ isActive }) => {
|
||||
return cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
className: cn(
|
||||
indentClassName,
|
||||
'px-2 w-full justify-start',
|
||||
isActive && 'bg-zinc-100'
|
||||
),
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface NavContextType {
|
||||
activeTab: string;
|
||||
activeSubTab: string;
|
||||
currentModule: string;
|
||||
setActiveTab: (tab: string) => void;
|
||||
setActiveSubTab: (tab: string) => void;
|
||||
setCurrentModule: (module: string) => void;
|
||||
}
|
||||
|
||||
export const NavContext = createContext<NavContextType | undefined>(undefined);
|
||||
export const useNav = () => {
|
||||
const context = useContext(NavContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useNav must be used within a NavProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { afterEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { NavItem } from './nav-item';
|
||||
|
||||
describe('NavItem', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('applies selected style when route matches', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/admin/accounts']}>
|
||||
<NavItem to="/admin/accounts" label="Accounts" icon={<span>i</span>} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
expect(link.className).toContain('bg-sidebar-active');
|
||||
expect(link.className).toContain('text-sidebar-foreground');
|
||||
});
|
||||
|
||||
test('keeps label hidden in collapsed mode', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/admin/accounts']}>
|
||||
<NavItem
|
||||
to="/admin/dashboard"
|
||||
label="Dashboard"
|
||||
isCollapsed={true}
|
||||
icon={<span>i</span>}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Dashboard')).toBeNull();
|
||||
expect(screen.getByRole('link').className).toContain('w-9');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,4 @@
|
||||
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||
import { cn } from '@affine/admin/utils';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
interface NavItemProps {
|
||||
@@ -11,52 +9,34 @@ interface NavItemProps {
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
const navItemBaseClass =
|
||||
'group inline-flex h-9 items-center gap-2 rounded-lg text-sm font-medium transition-all duration-150';
|
||||
const navItemStateClass =
|
||||
'text-sidebar-foreground-secondary hover:bg-sidebar-hover hover:text-sidebar-foreground';
|
||||
const navItemActiveClass =
|
||||
'bg-sidebar-active text-sidebar-foreground shadow-sm';
|
||||
|
||||
export const NavItem = ({ icon, label, to, isCollapsed }: NavItemProps) => {
|
||||
const className = ({ isActive }: { isActive: boolean }) =>
|
||||
cn(
|
||||
navItemBaseClass,
|
||||
navItemStateClass,
|
||||
isCollapsed ? 'w-9 justify-center px-0' : 'w-full justify-start px-2',
|
||||
isActive && navItemActiveClass
|
||||
);
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
className: 'w-10 h-10',
|
||||
size: 'icon',
|
||||
})
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
'&:hover': {
|
||||
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||
},
|
||||
})}
|
||||
>
|
||||
<NavLink to={to} className={className}>
|
||||
{icon}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
}),
|
||||
'justify-start flex-none text-sm font-medium px-2'
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
'&:hover': {
|
||||
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||
},
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
<NavLink to={to} className={className}>
|
||||
<span className="flex items-center p-0.5">{icon}</span>
|
||||
<span className="truncate">{label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,77 +1,17 @@
|
||||
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||
import { cn } from '@affine/admin/utils';
|
||||
import { ROUTES } from '@affine/routes';
|
||||
import { AccountIcon, SelfhostIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import {
|
||||
BarChart3Icon,
|
||||
LayoutDashboardIcon,
|
||||
ListChecksIcon,
|
||||
} from 'lucide-react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { NavItem } from './nav-item';
|
||||
import { ServerVersion } from './server-version';
|
||||
import { SettingsItem } from './settings-item';
|
||||
import { UserDropdown } from './user-dropdown';
|
||||
|
||||
interface NavItemProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
to: string;
|
||||
isActive?: boolean;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
const NavItem = ({ icon, label, to, isCollapsed }: NavItemProps) => {
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
className: 'w-10 h-10',
|
||||
size: 'icon',
|
||||
})
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
'&:hover': {
|
||||
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||
},
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
}),
|
||||
'justify-start flex-none text-sm font-medium px-2'
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
'&:hover': {
|
||||
backgroundColor: cssVarV2('selfhost/button/sidebarButton/bg/hover'),
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className="flex items-center p-0.5 mr-2">{icon}</span>
|
||||
{label}
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
interface NavProps {
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
@@ -80,13 +20,13 @@ export function Nav({ isCollapsed = false }: NavProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-4 py-2 justify-between flex-grow h-full overflow-hidden',
|
||||
'flex h-full flex-grow flex-col justify-between gap-4 py-2',
|
||||
isCollapsed && 'overflow-visible'
|
||||
)}
|
||||
>
|
||||
<nav
|
||||
className={cn(
|
||||
'flex flex-1 flex-col gap-1 px-2 flex-grow overflow-y-auto overflow-x-hidden',
|
||||
'flex flex-1 flex-col gap-1 overflow-x-hidden overflow-y-auto px-2',
|
||||
isCollapsed && 'items-center px-0 gap-1 overflow-visible'
|
||||
)}
|
||||
>
|
||||
@@ -134,7 +74,7 @@ export function Nav({ isCollapsed = false }: NavProps) {
|
||||
</nav>
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-2 px-2 flex-col overflow-hidden',
|
||||
'flex flex-col gap-2 overflow-hidden px-2',
|
||||
isCollapsed && 'items-center px-0 gap-1'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Button } from '../../components/ui/button';
|
||||
@@ -19,7 +18,7 @@ export const ServerVersion = () => {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center justify-center gap-1 text-xs p-2 font-medium w-full overflow-hidden"
|
||||
className="flex w-full items-center justify-center gap-1 overflow-hidden px-2 py-1.5 text-xs font-medium"
|
||||
onClick={handleClick}
|
||||
title={`New Version ${availableUpgrade.version} Available`}
|
||||
>
|
||||
@@ -32,12 +31,7 @@ export const ServerVersion = () => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="inline-flex items-center justify-between pt-2 border-t px-2 text-xs flex-nowrap gap-1"
|
||||
style={{
|
||||
color: cssVarV2('text/tertiary'),
|
||||
}}
|
||||
>
|
||||
<div className="inline-flex flex-nowrap items-center justify-between gap-1 border-t border-border px-2 pt-2 text-xs text-muted-foreground">
|
||||
<span>ServerVersion</span>
|
||||
<span
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
|
||||
@@ -1,198 +1,15 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@affine/admin/components/ui/accordion';
|
||||
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||
import { cn } from '@affine/admin/utils';
|
||||
import { ROUTES } from '@affine/routes';
|
||||
import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { KNOWN_CONFIG_GROUPS, UNKNOWN_CONFIG_GROUPS } from '../settings/config';
|
||||
import { NormalSubItem } from './collapsible-item';
|
||||
import { useNav } from './context';
|
||||
import { NavItem } from './nav-item';
|
||||
|
||||
export const SettingsItem = ({ isCollapsed }: { isCollapsed: boolean }) => {
|
||||
const { setCurrentModule } = useNav();
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
className="flex-none relative"
|
||||
orientation="vertical"
|
||||
>
|
||||
<NavigationMenuPrimitive.List>
|
||||
<NavigationMenuPrimitive.Item>
|
||||
<NavigationMenuPrimitive.Trigger className="[&>svg]:hidden m-0 p-0">
|
||||
<NavLink
|
||||
to={'/admin/settings'}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
className: 'w-10 h-10',
|
||||
size: 'icon',
|
||||
})
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
})}
|
||||
>
|
||||
<SettingsIcon fontSize={20} />
|
||||
</NavLink>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
<NavigationMenuPrimitive.Content>
|
||||
<ul
|
||||
className="border rounded-lg w-full flex flex-col p-1 min-w-[160px] max-h-[200px] overflow-y-auto"
|
||||
style={{
|
||||
backgroundColor: cssVarV2('layer/background/overlayPanel'),
|
||||
borderColor: cssVarV2('layer/insideBorder/blackBorder'),
|
||||
}}
|
||||
>
|
||||
{KNOWN_CONFIG_GROUPS.map(group => (
|
||||
<li key={group.module} className="flex">
|
||||
<NavLink
|
||||
to={`/admin/settings/${group.module}`}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
className:
|
||||
'p-2 rounded-[6px] text-[14px] w-full justify-start font-normal',
|
||||
})
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
})}
|
||||
onClick={() => setCurrentModule?.(group.module)}
|
||||
>
|
||||
{group.name}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
{UNKNOWN_CONFIG_GROUPS.length ? (
|
||||
<li className="flex px-2 pt-1 pb-0.5 text-xs font-medium opacity-70">
|
||||
Experimental
|
||||
</li>
|
||||
) : null}
|
||||
{UNKNOWN_CONFIG_GROUPS.map(group => (
|
||||
<li key={group.module} className="flex">
|
||||
<NavLink
|
||||
to={`/admin/settings/${group.module}`}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
className:
|
||||
'p-2 pl-6 rounded-[6px] text-[14px] w-full justify-start font-normal',
|
||||
})
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
})}
|
||||
onClick={() => setCurrentModule?.(group.module)}
|
||||
>
|
||||
{group.name}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuPrimitive.Content>
|
||||
</NavigationMenuPrimitive.Item>
|
||||
</NavigationMenuPrimitive.List>
|
||||
<NavigationMenuPrimitive.Viewport className="absolute z-10 left-11 top-0" />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion type="multiple" className="w-full overflow-hidden">
|
||||
<AccordionItem
|
||||
value="item-1"
|
||||
className="border-b-0 h-full flex flex-col gap-1 w-full"
|
||||
>
|
||||
<NavLink
|
||||
to={'/admin/settings'}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
}),
|
||||
'justify-start flex-none w-full px-2'
|
||||
)}
|
||||
style={({ isActive }) => ({
|
||||
backgroundColor: isActive
|
||||
? cssVarV2('selfhost/button/sidebarButton/bg/select')
|
||||
: undefined,
|
||||
})}
|
||||
>
|
||||
<AccordionTrigger
|
||||
className={
|
||||
'flex items-center justify-between w-full [&[data-state=closed]>svg]:rotate-270 [&[data-state=open]>svg]:rotate-360'
|
||||
}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="flex items-center p-0.5 mr-2">
|
||||
<SettingsIcon fontSize={20} />
|
||||
</span>
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
</NavLink>
|
||||
|
||||
<AccordionContent className="h-full overflow-hidden w-full pb-0">
|
||||
<ScrollAreaPrimitive.Root
|
||||
className={cn('relative overflow-hidden w-full h-full')}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] [&>div]:!block">
|
||||
{KNOWN_CONFIG_GROUPS.map(group => (
|
||||
<NormalSubItem
|
||||
key={group.module}
|
||||
module={group.module}
|
||||
title={group.name}
|
||||
changeModule={setCurrentModule}
|
||||
/>
|
||||
))}
|
||||
{UNKNOWN_CONFIG_GROUPS.length ? (
|
||||
<Accordion type="multiple" className="w-full">
|
||||
<AccordionItem value="item-1" className="border-b-0">
|
||||
<AccordionTrigger className="ml-8 py-2 px-2 rounded [&[data-state=closed]>svg]:rotate-270 [&[data-state=open]>svg]:rotate-360">
|
||||
Experimental
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-1 py-1">
|
||||
{UNKNOWN_CONFIG_GROUPS.map(group => (
|
||||
<NormalSubItem
|
||||
key={group.module}
|
||||
module={group.module}
|
||||
title={group.name}
|
||||
changeModule={setCurrentModule}
|
||||
indent="nested"
|
||||
/>
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
) : null}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
className={cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
|
||||
'h-full w-2.5 border-l border-l-transparent p-[1px]'
|
||||
)}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<NavItem
|
||||
to={ROUTES.admin.settings.index}
|
||||
icon={<SettingsIcon fontSize={20} />}
|
||||
label="Settings"
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@affine/admin/components/ui/dropdown-menu';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { CircleUser } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
@@ -25,6 +24,9 @@ interface UserDropdownProps {
|
||||
isCollapsed: boolean;
|
||||
}
|
||||
|
||||
const adminBadgeClass =
|
||||
'inline-flex h-5 items-center rounded-md border border-border/60 bg-chip-blue px-2 py-0.5 text-xxs font-medium text-chip-text';
|
||||
|
||||
const UserInfo = ({
|
||||
name,
|
||||
email,
|
||||
@@ -44,21 +46,41 @@ const UserInfo = ({
|
||||
</Avatar>
|
||||
<div className="flex flex-col font-medium gap-1">
|
||||
{name ?? email.split('@')[0]}
|
||||
<span
|
||||
className="w-fit rounded px-2 py-0.5 text-xs h-5 border text-center inline-flex items-center font-normal"
|
||||
style={{
|
||||
borderRadius: '4px',
|
||||
backgroundColor: cssVarV2('chip/label/blue'),
|
||||
borderColor: cssVarV2('layer/insideBorder/border'),
|
||||
}}
|
||||
>
|
||||
Admin
|
||||
</span>
|
||||
<span className={adminBadgeClass}>Admin</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UserName = ({
|
||||
name,
|
||||
email,
|
||||
}: {
|
||||
name?: string | null;
|
||||
email?: string;
|
||||
}) => {
|
||||
if (name) {
|
||||
return (
|
||||
<span
|
||||
className="max-w-[120px] overflow-hidden text-ellipsis text-sm whitespace-nowrap"
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const fallback = email?.split('@')[0] ?? '';
|
||||
return (
|
||||
<span
|
||||
className="max-w-[120px] overflow-hidden text-ellipsis text-sm whitespace-nowrap"
|
||||
title={fallback}
|
||||
>
|
||||
{fallback}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export function UserDropdown({ isCollapsed }: UserDropdownProps) {
|
||||
const currentUser = useCurrentUser();
|
||||
const relative = useRevalidateCurrentUser();
|
||||
@@ -78,8 +100,8 @@ export function UserDropdown({ isCollapsed }: UserDropdownProps) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="w-10 h-10" size="icon">
|
||||
<Avatar className="w-5 h-5">
|
||||
<Button variant="ghost" className="h-9 w-9 rounded-lg" size="icon">
|
||||
<Avatar className="h-5 w-5">
|
||||
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
|
||||
<AvatarFallback>
|
||||
<CircleUser size={24} />
|
||||
@@ -105,43 +127,24 @@ export function UserDropdown({ isCollapsed }: UserDropdownProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-none items-center ${isCollapsed ? 'justify-center' : 'justify-between'} px-1 py-3 flex-nowrap`}
|
||||
>
|
||||
<div className="flex items-center gap-2 font-medium text-ellipsis break-words overflow-hidden">
|
||||
<Avatar className="w-5 h-5">
|
||||
<div className="flex items-center justify-between px-1 py-3">
|
||||
<div className="flex min-w-0 items-center gap-2 font-medium">
|
||||
<Avatar className="h-5 w-5">
|
||||
<AvatarImage src={currentUser?.avatarUrl ?? undefined} />
|
||||
<AvatarFallback>
|
||||
<CircleUser size={24} />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{currentUser?.name ? (
|
||||
<span
|
||||
className="text-sm text-nowrap text-ellipsis break-words overflow-hidden"
|
||||
title={currentUser?.name}
|
||||
>
|
||||
{currentUser?.name}
|
||||
</span>
|
||||
) : (
|
||||
// Fallback to email prefix if name is not available
|
||||
<span className="text-sm" title={currentUser?.email.split('@')[0]}>
|
||||
{currentUser?.email.split('@')[0]}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="ml-2 rounded px-2 py-0.5 text-xs h-5 border text-center inline-flex items-center font-normal"
|
||||
style={{
|
||||
borderRadius: '4px',
|
||||
backgroundColor: cssVarV2('chip/label/blue'),
|
||||
borderColor: cssVarV2('layer/insideBorder/border'),
|
||||
}}
|
||||
>
|
||||
Admin
|
||||
</span>
|
||||
<UserName name={currentUser?.name} email={currentUser?.email} />
|
||||
<span className={adminBadgeClass}>Admin</span>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="ml-2 p-1 h-6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="ml-2 h-7 w-7 rounded-lg p-0"
|
||||
size="icon"
|
||||
>
|
||||
<MoreVerticalIcon fontSize={20} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
Reference in New Issue
Block a user