agent-ecosystem/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx
777genius 03dda6b486 refactor(agent-graph): replace store usage with context hooks for team data retrieval
- Updated components in the agent-graph renderer to utilize context hooks instead of the store for accessing team data.
- Introduced `useGraphActivityContext` and `useGraphMemberPopoverContext` hooks to streamline data management.
- Refactored `GraphBlockingEdgePopover`, `GraphNodePopover`, and `GraphTaskCard` components for improved performance and readability.
- Enhanced imports in `MemberDetailDialog` for better organization.
2026-04-15 16:42:05 +03:00

207 lines
6.4 KiB
TypeScript

import { useMemo } from 'react';
import { Badge } from '@renderer/components/ui/badge';
import { Button } from '@renderer/components/ui/button';
import { useGraphActivityContext } from '../hooks/useGraphActivityContext';
import type { GraphEdge, GraphNode } from '@claude-teams/agent-graph';
import type { TeamTaskWithKanban } from '@shared/types';
function isTaskNode(node: GraphNode | undefined): node is GraphNode & {
domainRef: Extract<GraphNode['domainRef'], { kind: 'task' }>;
} {
return node?.kind === 'task' && node.domainRef.kind === 'task';
}
function isOverflowNode(
node: GraphNode | undefined
): node is GraphNode & { isOverflowStack: true } {
return Boolean(node?.kind === 'task' && node.isOverflowStack);
}
function describeNode(node: GraphNode | undefined, fallback: string): string {
if (!node) return fallback;
if (isOverflowNode(node)) {
return node.overflowCount && node.overflowCount > 1
? `${node.overflowCount} hidden tasks`
: 'Hidden task stack';
}
if (isTaskNode(node)) {
return `${node.displayId ?? node.label} - ${node.sublabel ?? 'Task'}`;
}
return node.label;
}
function getActionLabel(node: GraphNode | undefined, role: 'blocker' | 'blocked'): string | null {
if (!node) return null;
if (isOverflowNode(node)) {
return role === 'blocker' ? 'Open blocker stack' : 'Open blocked stack';
}
if (isTaskNode(node)) {
return role === 'blocker' ? 'Open blocker task' : 'Open blocked task';
}
return null;
}
export interface GraphBlockingEdgePopoverProps {
teamName: string;
edge: GraphEdge;
sourceNode: GraphNode | undefined;
targetNode: GraphNode | undefined;
onClose: () => void;
onSelectNode: (nodeId: string) => void;
onOpenTaskDetail?: (taskId: string) => void;
}
export const GraphBlockingEdgePopover = ({
teamName,
edge,
sourceNode,
targetNode,
onClose,
onSelectNode,
onOpenTaskDetail,
}: GraphBlockingEdgePopoverProps): React.JSX.Element => {
const { teamData } = useGraphActivityContext(teamName);
const tasksById = useMemo(
() => new Map((teamData?.tasks ?? []).map((task) => [task.id, task] as const)),
[teamData?.tasks]
);
const relationCount = edge.aggregateCount ?? 1;
const sourceLabel = describeNode(sourceNode, edge.source);
const targetLabel = describeNode(targetNode, edge.target);
const sourceActionLabel = getActionLabel(sourceNode, 'blocker');
const targetActionLabel = getActionLabel(targetNode, 'blocked');
const sourceHiddenTasks = resolveEdgeTaskPreview(sourceNode, edge.sourceTaskIds, tasksById);
const targetHiddenTasks = resolveEdgeTaskPreview(targetNode, edge.targetTaskIds, tasksById);
const openSource = (): void => {
if (isTaskNode(sourceNode)) {
onOpenTaskDetail?.(sourceNode.domainRef.taskId);
onClose();
return;
}
if (sourceNode) {
onSelectNode(sourceNode.id);
}
};
const openTarget = (): void => {
if (isTaskNode(targetNode)) {
onOpenTaskDetail?.(targetNode.domainRef.taskId);
onClose();
return;
}
if (targetNode) {
onSelectNode(targetNode.id);
}
};
return (
<div className="min-w-[260px] max-w-[340px] rounded-lg border border-red-500/20 bg-[var(--color-surface-raised)] p-3 shadow-xl">
<div className="flex items-center justify-between gap-2">
<div className="font-mono text-[10px] uppercase tracking-[0.14em] text-red-400/90">
Blocking Dependency
</div>
{relationCount > 1 && (
<Badge
variant="outline"
className="border-red-500/30 px-1.5 py-0 text-[10px] text-red-300"
>
{relationCount} links
</Badge>
)}
</div>
<div className="mt-2 text-xs leading-relaxed text-[var(--color-text)]">
<div className="font-medium text-red-100">{sourceLabel}</div>
{sourceHiddenTasks.length > 0 && (
<HiddenTaskPreview
title="Blocking hidden tasks"
tasks={sourceHiddenTasks}
onOpenTaskDetail={onOpenTaskDetail}
onClose={onClose}
/>
)}
<div className="mt-1 text-[11px] text-red-300/85">blocks</div>
<div className="mt-1 font-medium text-red-100">{targetLabel}</div>
{targetHiddenTasks.length > 0 && (
<HiddenTaskPreview
title="Blocked hidden tasks"
tasks={targetHiddenTasks}
onOpenTaskDetail={onOpenTaskDetail}
onClose={onClose}
/>
)}
</div>
<div className="mt-3 flex flex-wrap gap-2">
{sourceActionLabel && (
<Button type="button" size="sm" variant="outline" onClick={openSource}>
{sourceActionLabel}
</Button>
)}
{targetActionLabel && (
<Button type="button" size="sm" variant="outline" onClick={openTarget}>
{targetActionLabel}
</Button>
)}
<Button type="button" size="sm" variant="ghost" onClick={onClose}>
Close
</Button>
</div>
</div>
);
};
function resolveEdgeTaskPreview(
node: GraphNode | undefined,
edgeTaskIds: string[] | undefined,
tasksById: ReadonlyMap<string, TeamTaskWithKanban>
): TeamTaskWithKanban[] {
if (!node || !isOverflowNode(node)) {
return [];
}
const candidateIds =
edgeTaskIds && edgeTaskIds.length > 0 ? edgeTaskIds : (node.overflowTaskIds ?? []);
return candidateIds
.map((taskId) => tasksById.get(taskId) ?? null)
.filter((task): task is TeamTaskWithKanban => task != null)
.slice(0, 4);
}
const HiddenTaskPreview = ({
title,
tasks,
onOpenTaskDetail,
onClose,
}: {
title: string;
tasks: TeamTaskWithKanban[];
onOpenTaskDetail?: (taskId: string) => void;
onClose: () => void;
}): React.JSX.Element => {
return (
<div className="mt-2 rounded border border-red-500/15 bg-red-500/5 p-2">
<div className="text-[10px] uppercase tracking-widest text-red-300/80">{title}</div>
<div className="mt-1 space-y-1">
{tasks.map((task) => (
<button
key={task.id}
type="button"
className="block w-full truncate text-left text-[11px] text-red-100/95 transition-opacity hover:opacity-80"
onClick={() => {
onOpenTaskDetail?.(task.id);
onClose();
}}
>
{task.displayId ?? `#${task.id.slice(0, 6)}`} - {task.subject}
</button>
))}
</div>
</div>
);
};