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 { useTranslation } from 'react-i18next';
|
||||||
import { useUsers, useGroups } from '@/api/hooks';
|
import { useUsers, useGroups } from '@/api/hooks';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
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';
|
import { Bell, Bot, ChevronLeft, ChevronRight, Lightbulb, MessageSquare, Users, X } from 'lucide-react';
|
||||||
|
|
||||||
const robotIcon = (className: string) => (
|
const robotIcon = (className: string) => (
|
||||||
@@ -42,64 +43,6 @@ interface TabDef {
|
|||||||
testId: string;
|
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() {
|
function ChatPanel() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full" data-testid="chat-panel">
|
<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 { useTranslation } from 'react-i18next';
|
||||||
import { useUsers, useGroups } from '@/api/hooks';
|
import { useUsers, useGroups } from '@/api/hooks';
|
||||||
import { Avatar } from '@/components/ui/Avatar';
|
import { Avatar } from '@/components/ui/Avatar';
|
||||||
|
import { SharedTeamPanel } from '@/components/shared/TeamPanel';
|
||||||
import {
|
import {
|
||||||
listConversations,
|
listConversations,
|
||||||
getMessages,
|
getMessages,
|
||||||
@@ -67,66 +68,8 @@ interface QuickAccessDef {
|
|||||||
|
|
||||||
// ─── Team Panel (reused from AISidebar) ───
|
// ─── Team Panel (reused from AISidebar) ───
|
||||||
|
|
||||||
function TeamPanel({ onStartDirectChat }: { onStartDirectChat: (userId: string, userName: string) => void }) {
|
function TeamPanel(props: { onStartDirectChat: (userId: string, userName: string) => void }) {
|
||||||
const { data: usersData, isLoading: usersLoading } = useUsers();
|
return <SharedTeamPanel {...props} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Conversation List Item ───
|
// ─── Conversation List Item ───
|
||||||
|
|||||||
@@ -1,10 +1,61 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { GripVertical, FolderOpen } from 'lucide-react';
|
import {
|
||||||
import * as LucideIcons from 'lucide-react';
|
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';
|
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 {
|
interface SortableMenuItemProps {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -13,8 +64,8 @@ interface SortableMenuItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getIcon(name: string): React.ReactNode {
|
function getIcon(name: string): React.ReactNode {
|
||||||
const Icon = (LucideIcons as any)[name];
|
const Icon = ICON_MAP[name];
|
||||||
return Icon ? <Icon className="h-4 w-4" /> : <LucideIcons.FileText className="h-4 w-4" />;
|
return Icon ? <Icon className="h-4 w-4" /> : <FileText className="h-4 w-4" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SortableMenuItem({ id, label, icon, isGroup }: SortableMenuItemProps) {
|
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