fix(graph): Profile opens member dialog (not message) + Add Task button

- Profile button dispatches 'graph:open-profile' → MemberDetailDialog
  (was incorrectly mapped to 'graph:send-message')
- Double-click on member → opens profile (was: send message)
- New "Add Task" button on member popover → CreateTaskDialog with owner pre-filled
- New events: graph:open-profile, graph:create-task
- TeamDetailView listens for both new events
This commit is contained in:
iliya 2026-03-28 15:08:32 +02:00
parent 26fe42739b
commit add9df2006
3 changed files with 50 additions and 6 deletions

View file

@ -246,11 +246,26 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
setSendDialogDefaultChip(undefined); setSendDialogDefaultChip(undefined);
setSendDialogOpen(true); setSendDialogOpen(true);
}; };
const onOpenProfile = (e: Event) => {
const { teamName: tn, memberName } = (e as CustomEvent).detail ?? {};
if (tn !== teamName || !data) return;
const member = data.members.find((m: { name: string }) => m.name === memberName);
if (member) setSelectedMember(member);
};
const onCreateTask = (e: Event) => {
const { teamName: tn, owner } = (e as CustomEvent).detail ?? {};
if (tn !== teamName) return;
openCreateTaskDialog('', '', owner ?? '');
};
window.addEventListener('graph:open-task', onOpenTask); window.addEventListener('graph:open-task', onOpenTask);
window.addEventListener('graph:send-message', onSendMsg); window.addEventListener('graph:send-message', onSendMsg);
window.addEventListener('graph:open-profile', onOpenProfile);
window.addEventListener('graph:create-task', onCreateTask);
return () => { return () => {
window.removeEventListener('graph:open-task', onOpenTask); window.removeEventListener('graph:open-task', onOpenTask);
window.removeEventListener('graph:send-message', onSendMsg); window.removeEventListener('graph:send-message', onSendMsg);
window.removeEventListener('graph:open-profile', onOpenProfile);
window.removeEventListener('graph:create-task', onCreateTask);
}; };
}); });

View file

@ -7,7 +7,7 @@
import { Badge } from '@renderer/components/ui/badge'; import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button'; import { Button } from '@renderer/components/ui/button';
import { agentAvatarUrl } from '@renderer/utils/memberHelpers'; import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
import { MessageSquare, ExternalLink, User } from 'lucide-react'; import { MessageSquare, ExternalLink, User, Plus } from 'lucide-react';
import type { GraphNode } from '@claude-teams/agent-graph'; import type { GraphNode } from '@claude-teams/agent-graph';
@ -17,6 +17,7 @@ interface GraphNodePopoverProps {
onSendMessage?: (memberName: string) => void; onSendMessage?: (memberName: string) => void;
onOpenTaskDetail?: (taskId: string) => void; onOpenTaskDetail?: (taskId: string) => void;
onOpenMemberProfile?: (memberName: string) => void; onOpenMemberProfile?: (memberName: string) => void;
onCreateTask?: (owner: string) => void;
} }
export function GraphNodePopover({ export function GraphNodePopover({
@ -25,6 +26,7 @@ export function GraphNodePopover({
onSendMessage, onSendMessage,
onOpenTaskDetail, onOpenTaskDetail,
onOpenMemberProfile, onOpenMemberProfile,
onCreateTask,
}: GraphNodePopoverProps): React.JSX.Element { }: GraphNodePopoverProps): React.JSX.Element {
if (node.kind === 'member' || node.kind === 'lead') { if (node.kind === 'member' || node.kind === 'lead') {
return ( return (
@ -33,6 +35,7 @@ export function GraphNodePopover({
onClose={onClose} onClose={onClose}
onSendMessage={onSendMessage} onSendMessage={onSendMessage}
onOpenProfile={onOpenMemberProfile} onOpenProfile={onOpenMemberProfile}
onCreateTask={onCreateTask}
/> />
); );
} }
@ -66,11 +69,13 @@ function MemberPopoverContent({
onClose, onClose,
onSendMessage, onSendMessage,
onOpenProfile, onOpenProfile,
onCreateTask,
}: { }: {
node: GraphNode; node: GraphNode;
onClose: () => void; onClose: () => void;
onSendMessage?: (name: string) => void; onSendMessage?: (name: string) => void;
onOpenProfile?: (name: string) => void; onOpenProfile?: (name: string) => void;
onCreateTask?: (owner: string) => void;
}): React.JSX.Element { }): React.JSX.Element {
const memberName = node.domainRef.kind === 'member' ? node.domainRef.memberName : 'team-lead'; const memberName = node.domainRef.kind === 'member' ? node.domainRef.memberName : 'team-lead';
const avatarSrc = node.avatarUrl ?? agentAvatarUrl(memberName, 64); const avatarSrc = node.avatarUrl ?? agentAvatarUrl(memberName, 64);
@ -167,7 +172,7 @@ function MemberPopoverContent({
)} )}
{/* Actions */} {/* Actions */}
<div className="mt-3 flex gap-1.5"> <div className="mt-3 flex flex-wrap gap-1.5">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@ -190,6 +195,17 @@ function MemberPopoverContent({
> >
<User size={12} /> Profile <User size={12} /> Profile
</Button> </Button>
<Button
variant="outline"
size="sm"
className="h-7 gap-1 px-2 text-xs"
onClick={() => {
onCreateTask?.(memberName);
onClose();
}}
>
<Plus size={12} /> Task
</Button>
</div> </div>
</div> </div>
); );

View file

@ -37,18 +37,30 @@ export const TeamGraphTab = ({ teamName }: TeamGraphTabProps): React.JSX.Element
), ),
[teamName] [teamName]
); );
const dispatchOpenProfile = useCallback(
(memberName: string) =>
window.dispatchEvent(
new CustomEvent('graph:open-profile', { detail: { teamName, memberName } })
),
[teamName]
);
const dispatchCreateTask = useCallback(
(owner: string) =>
window.dispatchEvent(new CustomEvent('graph:create-task', { detail: { teamName, owner } })),
[teamName]
);
const events: GraphEventPort = { const events: GraphEventPort = {
onNodeDoubleClick: useCallback( onNodeDoubleClick: useCallback(
(ref: GraphDomainRef) => { (ref: GraphDomainRef) => {
if (ref.kind === 'task') dispatchOpenTask(ref.taskId); if (ref.kind === 'task') dispatchOpenTask(ref.taskId);
else if (ref.kind === 'member') dispatchSendMessage(ref.memberName); else if (ref.kind === 'member') dispatchOpenProfile(ref.memberName);
}, },
[dispatchOpenTask, dispatchSendMessage] [dispatchOpenTask, dispatchOpenProfile]
), ),
onSendMessage: dispatchSendMessage, onSendMessage: dispatchSendMessage,
onOpenTaskDetail: dispatchOpenTask, onOpenTaskDetail: dispatchOpenTask,
onOpenMemberProfile: dispatchSendMessage, onOpenMemberProfile: dispatchOpenProfile,
}; };
return ( return (
@ -64,7 +76,8 @@ export const TeamGraphTab = ({ teamName }: TeamGraphTabProps): React.JSX.Element
onClose={onClose} onClose={onClose}
onSendMessage={dispatchSendMessage} onSendMessage={dispatchSendMessage}
onOpenTaskDetail={dispatchOpenTask} onOpenTaskDetail={dispatchOpenTask}
onOpenMemberProfile={dispatchSendMessage} onOpenMemberProfile={dispatchOpenProfile}
onCreateTask={dispatchCreateTask}
/> />
)} )}
/> />