feat(18-03): create TaskDetailModal component for initiative detail
Controlled dialog showing full task metadata, dependencies, dependents, and action buttons (Queue Task / Stop Task) with proper enable/disable logic based on task status and dependency completion.
This commit is contained in:
170
packages/web/src/components/TaskDetailModal.tsx
Normal file
170
packages/web/src/components/TaskDetailModal.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { StatusBadge } from "@/components/StatusBadge";
|
||||||
|
|
||||||
|
/** Serialized Task shape as returned by tRPC (Date serialized to string over JSON) */
|
||||||
|
export interface SerializedTask {
|
||||||
|
id: string;
|
||||||
|
planId: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
type: string;
|
||||||
|
priority: string;
|
||||||
|
status: string;
|
||||||
|
order: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DependencyInfo {
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskDetailModalProps {
|
||||||
|
task: SerializedTask | null;
|
||||||
|
phaseName: string;
|
||||||
|
agentName: string | null;
|
||||||
|
dependencies: DependencyInfo[];
|
||||||
|
dependents: DependencyInfo[];
|
||||||
|
onClose: () => void;
|
||||||
|
onQueueTask: (taskId: string) => void;
|
||||||
|
onStopTask: (taskId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskDetailModal({
|
||||||
|
task,
|
||||||
|
phaseName,
|
||||||
|
agentName,
|
||||||
|
dependencies,
|
||||||
|
dependents,
|
||||||
|
onClose,
|
||||||
|
onQueueTask,
|
||||||
|
onStopTask,
|
||||||
|
}: TaskDetailModalProps) {
|
||||||
|
const allDependenciesComplete =
|
||||||
|
dependencies.length === 0 ||
|
||||||
|
dependencies.every((d) => d.status === "completed");
|
||||||
|
|
||||||
|
const canQueue = task !== null && task.status === "pending" && allDependenciesComplete;
|
||||||
|
const canStop = task !== null && task.status === "in_progress";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={task !== null} onOpenChange={(open) => { if (!open) onClose(); }}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{task?.name ?? "Task"}</DialogTitle>
|
||||||
|
<DialogDescription>Task details and dependencies</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{task && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Metadata grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Status</span>
|
||||||
|
<div className="mt-1">
|
||||||
|
<StatusBadge status={task.status} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Priority</span>
|
||||||
|
<p className="mt-1 font-medium capitalize">{task.priority}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Phase</span>
|
||||||
|
<p className="mt-1 font-medium">{phaseName}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Type</span>
|
||||||
|
<p className="mt-1 font-medium">{task.type}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span className="text-muted-foreground">Agent</span>
|
||||||
|
<p className="mt-1 font-medium">
|
||||||
|
{agentName ?? "Unassigned"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-1 text-sm font-medium">Description</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{task.description ?? "No description"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dependencies */}
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-1 text-sm font-medium">Dependencies</h4>
|
||||||
|
{dependencies.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No dependencies
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{dependencies.map((dep) => (
|
||||||
|
<li
|
||||||
|
key={dep.name}
|
||||||
|
className="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<span>{dep.name}</span>
|
||||||
|
<StatusBadge status={dep.status} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dependents (Blocks) */}
|
||||||
|
<div>
|
||||||
|
<h4 className="mb-1 text-sm font-medium">Blocks</h4>
|
||||||
|
{dependents.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">None</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{dependents.map((dep) => (
|
||||||
|
<li
|
||||||
|
key={dep.name}
|
||||||
|
className="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<span>{dep.name}</span>
|
||||||
|
<StatusBadge status={dep.status} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!canQueue}
|
||||||
|
onClick={() => task && onQueueTask(task.id)}
|
||||||
|
>
|
||||||
|
Queue Task
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={!canStop}
|
||||||
|
onClick={() => task && onStopTask(task.id)}
|
||||||
|
>
|
||||||
|
Stop Task
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user