fix(c8): shared TeamPanel component (arch-062) + curated icon map in SortableMenuItem (arch-063 OOM fix)
This commit is contained in:
@@ -9,6 +9,7 @@ import { getContributedTabs } from './sidebarTabs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUsers, useGroups } from '@/api/hooks';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { SharedTeamPanel as TeamPanel } from '@/components/shared/TeamPanel';
|
||||
import { Bell, Bot, ChevronLeft, ChevronRight, Lightbulb, MessageSquare, Users, X } from 'lucide-react';
|
||||
|
||||
const robotIcon = (className: string) => (
|
||||
@@ -42,64 +43,6 @@ interface TabDef {
|
||||
testId: string;
|
||||
}
|
||||
|
||||
function TeamPanel() {
|
||||
const { data: usersData, isLoading: usersLoading } = useUsers();
|
||||
const { data: groupsData, isLoading: groupsLoading } = useGroups();
|
||||
const users: any[] = usersData?.items || [];
|
||||
const groups: any[] = groupsData?.items || [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-auto p-3 gap-3" data-testid="team-panel">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-secondary-500 uppercase tracking-wide mb-2">Mitarbeiter</h3>
|
||||
{usersLoading ? (
|
||||
<p className="text-sm text-secondary-400">Laden...</p>
|
||||
) : users.length === 0 ? (
|
||||
<p className="text-sm text-secondary-400">Keine Mitarbeiter</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{users.map((u) => (
|
||||
<div key={u.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors">
|
||||
<div className="relative flex-shrink-0">
|
||||
<Avatar name={u.name} size="sm" />
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border-2 border-white bg-secondary-300" aria-label="offline" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{u.name}</p>
|
||||
<p className="text-xs text-secondary-400 truncate">{u.email}</p>
|
||||
</div>
|
||||
<span className="text-xs text-secondary-400">{u.role}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-secondary-500 uppercase tracking-wide mb-2">Gruppen</h3>
|
||||
{groupsLoading ? (
|
||||
<p className="text-sm text-secondary-400">Laden...</p>
|
||||
) : groups.length === 0 ? (
|
||||
<p className="text-sm text-secondary-400">Keine Gruppen</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{groups.map((g) => (
|
||||
<div key={g.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full bg-secondary-200 flex items-center justify-center flex-shrink-0">
|
||||
<Users className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p>
|
||||
{g.description && <p className="text-xs text-secondary-400 truncate">{g.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatPanel() {
|
||||
return (
|
||||
<div className="flex flex-col h-full" data-testid="chat-panel">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCommStore } from '@/store/commStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUsers, useGroups } from '@/api/hooks';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { SharedTeamPanel } from '@/components/shared/TeamPanel';
|
||||
import {
|
||||
listConversations,
|
||||
getMessages,
|
||||
@@ -67,66 +68,8 @@ interface QuickAccessDef {
|
||||
|
||||
// ─── Team Panel (reused from AISidebar) ───
|
||||
|
||||
function TeamPanel({ onStartDirectChat }: { onStartDirectChat: (userId: string, userName: string) => void }) {
|
||||
const { data: usersData, isLoading: usersLoading } = useUsers();
|
||||
const { data: groupsData, isLoading: groupsLoading } = useGroups();
|
||||
const users: any[] = usersData?.items || [];
|
||||
const groups: any[] = groupsData?.items || [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-auto p-3 gap-3" data-testid="team-panel">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-secondary-500 uppercase tracking-wide mb-2">Mitarbeiter</h3>
|
||||
{usersLoading ? (
|
||||
<p className="text-sm text-secondary-400">Laden...</p>
|
||||
) : users.length === 0 ? (
|
||||
<p className="text-sm text-secondary-400">Keine Mitarbeiter</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{users.map((u) => (
|
||||
<button
|
||||
key={u.id}
|
||||
onClick={() => onStartDirectChat(u.id, u.name)}
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors w-full text-left"
|
||||
>
|
||||
<div className="relative flex-shrink-0">
|
||||
<Avatar name={u.name} size="sm" />
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border-2 border-white bg-secondary-300" aria-label="offline" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{u.name}</p>
|
||||
<p className="text-xs text-secondary-400 truncate">{u.email}</p>
|
||||
</div>
|
||||
<span className="text-xs text-secondary-400">{u.role}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-secondary-500 uppercase tracking-wide mb-2">Gruppen</h3>
|
||||
{groupsLoading ? (
|
||||
<p className="text-sm text-secondary-400">Laden...</p>
|
||||
) : groups.length === 0 ? (
|
||||
<p className="text-sm text-secondary-400">Keine Gruppen</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{groups.map((g) => (
|
||||
<div key={g.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full bg-secondary-200 flex items-center justify-center flex-shrink-0">
|
||||
<Users className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p>
|
||||
{g.description && <p className="text-xs text-secondary-400 truncate">{g.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
function TeamPanel(props: { onStartDirectChat: (userId: string, userName: string) => void }) {
|
||||
return <SharedTeamPanel {...props} />;
|
||||
}
|
||||
|
||||
// ─── Conversation List Item ───
|
||||
|
||||
@@ -1,10 +1,61 @@
|
||||
import React from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { GripVertical, FolderOpen } from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpDown,
|
||||
BarChart3,
|
||||
Bell,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Calendar,
|
||||
CheckSquare,
|
||||
Copy,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Link as LinkIcon,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Tag,
|
||||
GripVertical,
|
||||
Trash2,
|
||||
Users,
|
||||
UsersRound,
|
||||
Workflow,
|
||||
} from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
// Curated icon map — avoids `import * as LucideIcons` which loads ALL icons
|
||||
// and causes OOM in tests (ARCH-063).
|
||||
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Activity,
|
||||
ArrowUpDown,
|
||||
BarChart: BarChart3,
|
||||
BarChart3,
|
||||
Bell,
|
||||
BookOpen,
|
||||
Bot,
|
||||
Calendar,
|
||||
CheckSquare,
|
||||
Copy,
|
||||
FolderOpen,
|
||||
Link: LinkIcon,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Tag,
|
||||
Trash: Trash2,
|
||||
Trash2,
|
||||
Users,
|
||||
UsersRound,
|
||||
Workflow,
|
||||
};
|
||||
|
||||
interface SortableMenuItemProps {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -13,8 +64,8 @@ interface SortableMenuItemProps {
|
||||
}
|
||||
|
||||
function getIcon(name: string): React.ReactNode {
|
||||
const Icon = (LucideIcons as any)[name];
|
||||
return Icon ? <Icon className="h-4 w-4" /> : <LucideIcons.FileText className="h-4 w-4" />;
|
||||
const Icon = ICON_MAP[name];
|
||||
return Icon ? <Icon className="h-4 w-4" /> : <FileText className="h-4 w-4" />;
|
||||
}
|
||||
|
||||
export function SortableMenuItem({ id, label, icon, isGroup }: SortableMenuItemProps) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* SharedTeamPanel — single source of truth for the team list panel used by
|
||||
* AISidebar and MessageSidebar (ARCH-062).
|
||||
*
|
||||
* Pass `onStartDirectChat` to make user rows clickable (MessageSidebar);
|
||||
* omit it for a read-only list (AISidebar).
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Users } from 'lucide-react';
|
||||
import { useUsers, useGroups } from '@/api/hooks';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
|
||||
interface SharedTeamPanelProps {
|
||||
onStartDirectChat?: (userId: string, userName: string) => void;
|
||||
}
|
||||
|
||||
export function SharedTeamPanel({ onStartDirectChat }: SharedTeamPanelProps) {
|
||||
const { data: usersData, isLoading: usersLoading } = useUsers();
|
||||
const { data: groupsData, isLoading: groupsLoading } = useGroups();
|
||||
const users: any[] = usersData?.items || [];
|
||||
const groups: any[] = groupsData?.items || [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-auto p-3 gap-3" data-testid="team-panel">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-secondary-500 uppercase tracking-wide mb-2">Mitarbeiter</h3>
|
||||
{usersLoading ? (
|
||||
<p className="text-sm text-secondary-400">Laden...</p>
|
||||
) : users.length === 0 ? (
|
||||
<p className="text-sm text-secondary-400">Keine Mitarbeiter</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{users.map((u) =>
|
||||
onStartDirectChat ? (
|
||||
<button
|
||||
key={u.id}
|
||||
onClick={() => onStartDirectChat(u.id, u.name)}
|
||||
className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors w-full text-left"
|
||||
>
|
||||
<div className="relative flex-shrink-0">
|
||||
<Avatar name={u.name} size="sm" />
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border-2 border-white bg-secondary-300" aria-label="offline" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{u.name}</p>
|
||||
<p className="text-xs text-secondary-400 truncate">{u.email}</p>
|
||||
</div>
|
||||
<span className="text-xs text-secondary-400">{u.role}</span>
|
||||
</button>
|
||||
) : (
|
||||
<div key={u.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors">
|
||||
<div className="relative flex-shrink-0">
|
||||
<Avatar name={u.name} size="sm" />
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border-2 border-white bg-secondary-300" aria-label="offline" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{u.name}</p>
|
||||
<p className="text-xs text-secondary-400 truncate">{u.email}</p>
|
||||
</div>
|
||||
<span className="text-xs text-secondary-400">{u.role}</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-secondary-500 uppercase tracking-wide mb-2">Gruppen</h3>
|
||||
{groupsLoading ? (
|
||||
<p className="text-sm text-secondary-400">Laden...</p>
|
||||
) : groups.length === 0 ? (
|
||||
<p className="text-sm text-secondary-400">Keine Gruppen</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{groups.map((g) => (
|
||||
<div key={g.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full bg-secondary-200 flex items-center justify-center flex-shrink-0">
|
||||
<Users className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p>
|
||||
{g.description && <p className="text-xs text-secondary-400 truncate">{g.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user