mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-10 05:37:53 +00:00
fix: focus ring clipping in drill-downs + wire up open-task event for task navigation
This commit is contained in:
parent
5830fd3b7e
commit
5825b56456
3 changed files with 88 additions and 67 deletions
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue