refactor: Replace PipelineTaskCard with TaskGraph in pipeline phases
Pipeline phase cards now use the same TaskGraph component from the plan tab's phase detail panel. This gives tasks a two-line layout with rail dot, status badge, category badge, priority, and dependency layer connectors instead of the flat single-line display. Removes PipelineTaskCard (no longer imported anywhere).
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { ChevronDown, Loader2, Play } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { Loader2, Play } from "lucide-react";
|
||||
import { StatusDot, mapEntityStatus } from "@/components/StatusDot";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
topologicalSortPhases,
|
||||
type DependencyEdge,
|
||||
} from "@codewalk-district/shared";
|
||||
import { PipelineTaskCard } from "./PipelineTaskCard";
|
||||
import { TaskGraph } from "@/components/execution/TaskGraph";
|
||||
import { useExecutionContext } from "@/components/execution";
|
||||
import type { SerializedTask } from "@/components/TaskRow";
|
||||
|
||||
export interface DetailAgentInfo {
|
||||
@@ -27,8 +24,6 @@ interface PipelinePhaseGroupProps {
|
||||
detailAgent?: DetailAgentInfo | null;
|
||||
}
|
||||
|
||||
const COLLAPSE_THRESHOLD = 5;
|
||||
|
||||
const statusBorderColor: Record<string, string> = {
|
||||
active: "border-l-status-active-dot",
|
||||
success: "border-l-status-success-dot",
|
||||
@@ -38,22 +33,15 @@ const statusBorderColor: Record<string, string> = {
|
||||
urgent: "border-l-status-urgent-dot",
|
||||
};
|
||||
|
||||
const emptyAgentMap = new Map<string, string>();
|
||||
|
||||
export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detailAgent }: PipelinePhaseGroupProps) {
|
||||
const approvePhase = trpc.approvePhase.useMutation();
|
||||
const queuePhase = trpc.queuePhase.useMutation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const { setSelectedTaskId } = useExecutionContext();
|
||||
|
||||
// Sort tasks topologically by dependency order
|
||||
const { sorted, blockedByCountMap } = useMemo(() => {
|
||||
const edges: DependencyEdge[] = [];
|
||||
for (const raw of taskDepsRaw) {
|
||||
for (const depId of raw.dependsOn) {
|
||||
edges.push({ phaseId: raw.taskId, dependsOnPhaseId: depId });
|
||||
}
|
||||
}
|
||||
const sortedTasks = topologicalSortPhases(tasks, edges);
|
||||
|
||||
// Compute blocked-by counts (incomplete dependencies)
|
||||
// Compute blocked-by counts for TaskGraph
|
||||
const blockedByCountMap = useMemo(() => {
|
||||
const countMap = new Map<string, number>();
|
||||
for (const raw of taskDepsRaw) {
|
||||
const incomplete = raw.dependsOn.filter((depId) => {
|
||||
@@ -62,8 +50,7 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
|
||||
}).length;
|
||||
if (incomplete > 0) countMap.set(raw.taskId, incomplete);
|
||||
}
|
||||
|
||||
return { sorted: sortedTasks, blockedByCountMap: countMap };
|
||||
return countMap;
|
||||
}, [tasks, taskDepsRaw]);
|
||||
|
||||
const completedCount = tasks.filter((t) => t.status === "completed").length;
|
||||
@@ -84,13 +71,6 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
|
||||
const variant = mapEntityStatus(phase.status);
|
||||
const borderClass = statusBorderColor[variant] ?? statusBorderColor.neutral;
|
||||
|
||||
// Collapsible logic
|
||||
const needsCollapse = sorted.length > COLLAPSE_THRESHOLD;
|
||||
const visibleTasks = needsCollapse && !expanded
|
||||
? sorted.slice(0, COLLAPSE_THRESHOLD)
|
||||
: sorted;
|
||||
const hiddenCount = sorted.length - COLLAPSE_THRESHOLD;
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"rounded-lg border border-border border-l-2 bg-card overflow-hidden",
|
||||
@@ -144,38 +124,24 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
|
||||
)}
|
||||
|
||||
{/* Tasks */}
|
||||
<div className="py-1">
|
||||
<div className="px-1 py-1">
|
||||
{isDetailing ? (
|
||||
<p className="flex items-center gap-1.5 px-3 py-1 text-xs text-status-active-fg">
|
||||
<p className="flex items-center gap-1.5 px-2 py-1 text-xs text-status-active-fg">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Detailing…
|
||||
</p>
|
||||
) : detailDone && sorted.length === 0 ? (
|
||||
<p className="px-3 py-1 text-xs text-status-warning-fg">Review changes</p>
|
||||
) : sorted.length === 0 ? (
|
||||
<p className="px-3 py-1 text-xs text-muted-foreground">No tasks</p>
|
||||
) : detailDone && tasks.length === 0 ? (
|
||||
<p className="px-2 py-1 text-xs text-status-warning-fg">Review changes</p>
|
||||
) : tasks.length === 0 ? (
|
||||
<p className="px-2 py-1 text-xs text-muted-foreground">No tasks</p>
|
||||
) : (
|
||||
<>
|
||||
{visibleTasks.map((task) => (
|
||||
<PipelineTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
blockedByCount={blockedByCountMap.get(task.id) ?? 0}
|
||||
/>
|
||||
))}
|
||||
{needsCollapse && (
|
||||
<button
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="flex w-full items-center gap-1 px-3 py-1 text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ChevronDown className={cn(
|
||||
"h-3 w-3 transition-transform",
|
||||
expanded && "rotate-180",
|
||||
)} />
|
||||
{expanded ? "Show less" : `${hiddenCount} more task${hiddenCount === 1 ? "" : "s"}`}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
<TaskGraph
|
||||
tasks={tasks}
|
||||
taskDepsRaw={taskDepsRaw}
|
||||
blockedByCountMap={blockedByCountMap}
|
||||
agentNameMap={emptyAgentMap}
|
||||
onClickTask={setSelectedTaskId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { CheckCircle2, Loader2, Clock, Ban, Play, AlertTriangle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { useExecutionContext } from "@/components/execution";
|
||||
import type { SerializedTask } from "@/components/TaskRow";
|
||||
|
||||
const statusConfig: Record<string, { icon: typeof Clock; color: string; spin?: boolean }> = {
|
||||
pending: { icon: Clock, color: "text-status-neutral-fg" },
|
||||
pending_approval: { icon: AlertTriangle, color: "text-status-warning-dot" },
|
||||
in_progress: { icon: Loader2, color: "text-status-active-dot", spin: true },
|
||||
completed: { icon: CheckCircle2, color: "text-status-success-dot" },
|
||||
blocked: { icon: Ban, color: "text-status-error-dot" },
|
||||
};
|
||||
|
||||
interface PipelineTaskCardProps {
|
||||
task: SerializedTask;
|
||||
blockedByCount?: number;
|
||||
}
|
||||
|
||||
export function PipelineTaskCard({ task, blockedByCount = 0 }: PipelineTaskCardProps) {
|
||||
const { setSelectedTaskId } = useExecutionContext();
|
||||
const queueTask = trpc.queueTask.useMutation();
|
||||
|
||||
const config = statusConfig[task.status] ?? statusConfig.pending;
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded px-2 py-1 cursor-pointer hover:bg-accent/50 group"
|
||||
onClick={() => setSelectedTaskId(task.id)}
|
||||
>
|
||||
<Icon
|
||||
className={cn("h-3.5 w-3.5 shrink-0", config.color, config.spin && "animate-spin")}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-xs">{task.name}</span>
|
||||
{blockedByCount > 0 && (
|
||||
<span className="shrink-0 rounded-full bg-muted px-1.5 py-px text-[10px] text-muted-foreground">
|
||||
{blockedByCount} dep{blockedByCount === 1 ? "" : "s"}
|
||||
</span>
|
||||
)}
|
||||
{task.status === "pending" && (
|
||||
<button
|
||||
className="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
queueTask.mutate({ taskId: task.id });
|
||||
}}
|
||||
title="Queue task"
|
||||
>
|
||||
<Play className="h-3 w-3 text-muted-foreground hover:text-foreground" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user