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