fix: focus ring clipping in drill-downs + wire up open-task event for task navigation

This commit is contained in:
Brad Groux 2026-01-29 06:59:02 -06:00
parent 5830fd3b7e
commit 5825b56456
3 changed files with 88 additions and 67 deletions

View file

@ -80,6 +80,21 @@ export function KanbanBoard() {
setDetailOpen(true);
}, []);
// Listen for open-task events from dashboard drill-downs
useEffect(() => {
const handler = (e: Event) => {
const taskId = (e as CustomEvent).detail?.taskId;
if (!taskId) return;
const task = tasks?.find((t) => t.id === taskId);
if (task) {
setSelectedTask(task);
setDetailOpen(true);
}
};
window.addEventListener('open-task', handler);
return () => window.removeEventListener('open-task', handler);
}, [tasks]);
const updateTask = useUpdateTask();
const reorderTasks = useReorderTasks();

View file

@ -37,17 +37,16 @@ export function ErrorsDrillDown({ period, project, onTaskClick }: ErrorsDrillDow
{/* Summary */}
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<AlertTriangle className="h-4 w-4 text-red-500" />
<span>{failedRuns.length} failed run(s) in the {period === '24h' ? 'last 24 hours' : 'last 7 days'}</span>
<span>
{failedRuns.length} failed run(s) in the{' '}
{period === '24h' ? 'last 24 hours' : 'last 7 days'}
</span>
</div>
{/* Failed Runs List */}
<div className="space-y-2">
{failedRuns.map((run, index) => (
<FailedRunRow
key={`${run.timestamp}-${index}`}
run={run}
onTaskClick={onTaskClick}
/>
<FailedRunRow key={`${run.timestamp}-${index}`} run={run} onTaskClick={onTaskClick} />
))}
</div>
</div>
@ -71,28 +70,24 @@ function FailedRunRow({ run, onTaskClick }: FailedRunRowProps) {
const canNavigate = run.taskId && onTaskClick;
const content = (
<div className={cn(
'rounded-lg border border-red-500/20 bg-red-500/5 p-3',
canNavigate && 'hover:bg-red-500/10 transition-colors cursor-pointer'
)}>
<div
className={cn(
'rounded-lg border border-red-500/20 bg-red-500/5 p-3',
canNavigate && 'hover:bg-red-500/10 transition-colors cursor-pointer'
)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-red-500 flex-shrink-0" />
<span className="font-medium truncate">
{run.taskId || 'Unknown task'}
</span>
{canNavigate && (
<ExternalLink className="h-3 w-3 text-muted-foreground" />
)}
<span className="font-medium truncate">{run.taskId || 'Unknown task'}</span>
{canNavigate && <ExternalLink className="h-3 w-3 text-muted-foreground" />}
</div>
{run.errorMessage && (
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{run.errorMessage}
</p>
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">{run.errorMessage}</p>
)}
<div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Bot className="h-3 w-3" />
@ -111,7 +106,7 @@ function FailedRunRow({ run, onTaskClick }: FailedRunRowProps) {
)}
</div>
</div>
<div className="text-xs text-muted-foreground text-right flex-shrink-0">
<div>{date.toLocaleDateString()}</div>
<div>{date.toLocaleTimeString()}</div>
@ -124,7 +119,7 @@ function FailedRunRow({ run, onTaskClick }: FailedRunRowProps) {
return (
<button
onClick={() => onTaskClick(run.taskId!)}
className="w-full text-left focus:outline-none focus:ring-2 focus:ring-ring rounded-lg"
className="w-full text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset rounded-lg"
>
{content}
</button>

View file

@ -13,67 +13,74 @@ interface TasksDrillDownProps {
onTaskClick?: (taskId: string) => void;
}
const statusConfig: Record<TaskStatus, {
icon: React.ReactNode;
color: string;
label: string;
}> = {
'todo': {
icon: <ListTodo className="h-4 w-4" />,
const statusConfig: Record<
TaskStatus,
{
icon: React.ReactNode;
color: string;
label: string;
}
> = {
todo: {
icon: <ListTodo className="h-4 w-4" />,
color: 'text-muted-foreground',
label: 'To Do'
label: 'To Do',
},
'in-progress': {
icon: <Play className="h-4 w-4" />,
'in-progress': {
icon: <Play className="h-4 w-4" />,
color: 'text-blue-500',
label: 'In Progress'
label: 'In Progress',
},
'blocked': {
icon: <Ban className="h-4 w-4" />,
blocked: {
icon: <Ban className="h-4 w-4" />,
color: 'text-red-500',
label: 'Blocked'
label: 'Blocked',
},
'done': {
icon: <CheckCircle className="h-4 w-4" />,
done: {
icon: <CheckCircle className="h-4 w-4" />,
color: 'text-green-500',
label: 'Done'
label: 'Done',
},
};
export function TasksDrillDown({ project, statusFilter = 'all', onTaskClick }: TasksDrillDownProps) {
export function TasksDrillDown({
project,
statusFilter = 'all',
onTaskClick,
}: TasksDrillDownProps) {
const { data: tasks, isLoading } = useTasks();
const { data: projects = [] } = useProjects();
const filteredTasks = useMemo(() => {
if (!tasks) return [];
let filtered = tasks;
// Filter by project
if (project) {
filtered = filtered.filter(t => t.project === project);
filtered = filtered.filter((t) => t.project === project);
}
// Filter by status
if (statusFilter !== 'all') {
filtered = filtered.filter(t => t.status === statusFilter);
filtered = filtered.filter((t) => t.status === statusFilter);
}
// Sort by updated date (most recent first)
return [...filtered].sort((a, b) =>
new Date(b.updated).getTime() - new Date(a.updated).getTime()
return [...filtered].sort(
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime()
);
}, [tasks, project, statusFilter]);
// Group by status for summary
const statusCounts = useMemo(() => {
const counts: Record<TaskStatus, number> = {
'todo': 0,
todo: 0,
'in-progress': 0,
'blocked': 0,
'done': 0,
blocked: 0,
done: 0,
};
filteredTasks.forEach(t => counts[t.status]++);
filteredTasks.forEach((t) => counts[t.status]++);
return counts;
}, [filteredTasks]);
@ -109,14 +116,12 @@ export function TasksDrillDown({ project, statusFilter = 'all', onTaskClick }: T
{/* Task List */}
<div className="space-y-2">
{filteredTasks.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No tasks found
</div>
<div className="text-center text-muted-foreground py-8">No tasks found</div>
) : (
filteredTasks.map((task) => (
<TaskRow
key={task.id}
task={task}
<TaskRow
key={task.id}
task={task}
projects={projects}
onClick={() => onTaskClick?.(task.id)}
/>
@ -127,25 +132,31 @@ export function TasksDrillDown({ project, statusFilter = 'all', onTaskClick }: T
);
}
function TaskRow({ task, projects = [], onClick }: { task: Task; projects?: Array<{ id: string; label: string }>; onClick?: () => void }) {
function TaskRow({
task,
projects = [],
onClick,
}: {
task: Task;
projects?: Array<{ id: string; label: string }>;
onClick?: () => void;
}) {
const config = statusConfig[task.status];
const projectLabel = task.project
? projects.find(p => p.id === task.project)?.label || task.project
const projectLabel = task.project
? projects.find((p) => p.id === task.project)?.label || task.project
: null;
return (
<button
onClick={onClick}
className={cn(
'w-full text-left rounded-lg border p-3',
'hover:bg-muted/50 transition-colors',
'focus:outline-none focus:ring-2 focus:ring-ring'
'focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset'
)}
>
<div className="flex items-start gap-3">
<div className={cn('mt-0.5', config.color)}>
{config.icon}
</div>
<div className={cn('mt-0.5', config.color)}>{config.icon}</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{task.title}</div>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">