feat(kanban): dependency visual on cards with Link2 icon and tooltip
This commit is contained in:
@@ -149,8 +149,8 @@ function DroppableColumn({
|
|||||||
color,
|
color,
|
||||||
tasks,
|
tasks,
|
||||||
domainMap,
|
domainMap,
|
||||||
|
depCounts,
|
||||||
onTaskClick,
|
onTaskClick,
|
||||||
onAddTask,
|
|
||||||
inlineInput,
|
inlineInput,
|
||||||
onInlineInputChange,
|
onInlineInputChange,
|
||||||
onInlineSubmit,
|
onInlineSubmit,
|
||||||
@@ -161,8 +161,8 @@ function DroppableColumn({
|
|||||||
color: string;
|
color: string;
|
||||||
tasks: Task[];
|
tasks: Task[];
|
||||||
domainMap: Map<string, { name: string; color: string | null }>;
|
domainMap: Map<string, { name: string; color: string | null }>;
|
||||||
|
depCounts: Map<string, number>;
|
||||||
onTaskClick: (task: Task) => void;
|
onTaskClick: (task: Task) => void;
|
||||||
onAddTask: () => void;
|
|
||||||
inlineInput: string;
|
inlineInput: string;
|
||||||
onInlineInputChange: (value: string) => void;
|
onInlineInputChange: (value: string) => void;
|
||||||
onInlineSubmit: () => void;
|
onInlineSubmit: () => void;
|
||||||
@@ -176,15 +176,6 @@ function DroppableColumn({
|
|||||||
<h3 className="font-semibold text-sm">{title}</h3>
|
<h3 className="font-semibold text-sm">{title}</h3>
|
||||||
<span className="text-sm text-muted-foreground">({tasks.length})</span>
|
<span className="text-sm text-muted-foreground">({tasks.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7"
|
|
||||||
onClick={onAddTask}
|
|
||||||
aria-label={`Add task to ${title}`}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
role="list"
|
role="list"
|
||||||
@@ -202,7 +193,7 @@ function DroppableColumn({
|
|||||||
task={task}
|
task={task}
|
||||||
domainName={domainMap.get(task.domainId)?.name || ''}
|
domainName={domainMap.get(task.domainId)?.name || ''}
|
||||||
domainColor={domainMap.get(task.domainId)?.color || null}
|
domainColor={domainMap.get(task.domainId)?.color || null}
|
||||||
depCount={0}
|
depCount={depCounts.get(task.id) || 0}
|
||||||
onClick={() => onTaskClick(task)}
|
onClick={() => onTaskClick(task)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -248,6 +239,7 @@ export function TasksKanbanView({
|
|||||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||||
const [inlineInputs, setInlineInputs] = useState<Record<string, string>>({});
|
const [inlineInputs, setInlineInputs] = useState<Record<string, string>>({});
|
||||||
const [inlineLoading, setInlineLoading] = useState<Record<string, boolean>>({});
|
const [inlineLoading, setInlineLoading] = useState<Record<string, boolean>>({});
|
||||||
|
const [depCounts, setDepCounts] = useState<Map<string, number>>(new Map());
|
||||||
const { subscribe } = useRealtimeContext();
|
const { subscribe } = useRealtimeContext();
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
@@ -303,6 +295,37 @@ export function TasksKanbanView({
|
|||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [domainId, subscribe, fetchTasks, onRefresh]);
|
}, [domainId, subscribe, fetchTasks, onRefresh]);
|
||||||
|
|
||||||
|
// Fetch dependency counts for visible tasks
|
||||||
|
useEffect(() => {
|
||||||
|
if (tasks.length === 0 || !domainId) return;
|
||||||
|
const controller = new AbortController();
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
// Fetch task details in parallel to get dependency counts
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
tasks.map((t) =>
|
||||||
|
fetch(`/api/domains/${domainId}/tasks/${t.id}`, { signal: controller.signal })
|
||||||
|
.then((r) => (r.ok ? r.json() : null))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (cancelled) return;
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const r = results[i];
|
||||||
|
if (r.status === 'fulfilled' && r.value?.dependencies?.length > 0) {
|
||||||
|
counts.set(tasks[i].id, r.value.dependencies.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDepCounts(counts);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [tasks, domainId]);
|
||||||
|
|
||||||
async function handleDragEnd(event: DragEndEvent) {
|
async function handleDragEnd(event: DragEndEvent) {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (!over) return;
|
if (!over) return;
|
||||||
@@ -375,8 +398,8 @@ export function TasksKanbanView({
|
|||||||
color={column.color}
|
color={column.color}
|
||||||
tasks={tasks.filter((t) => t.status === column.id)}
|
tasks={tasks.filter((t) => t.status === column.id)}
|
||||||
domainMap={domainMap}
|
domainMap={domainMap}
|
||||||
|
depCounts={depCounts}
|
||||||
onTaskClick={setSelectedTask}
|
onTaskClick={setSelectedTask}
|
||||||
onAddTask={() => {}}
|
|
||||||
inlineInput={inlineInputs[column.id] || ''}
|
inlineInput={inlineInputs[column.id] || ''}
|
||||||
onInlineInputChange={(value) =>
|
onInlineInputChange={(value) =>
|
||||||
setInlineInputs((prev) => ({ ...prev, [column.id]: value }))
|
setInlineInputs((prev) => ({ ...prev, [column.id]: value }))
|
||||||
|
|||||||
Reference in New Issue
Block a user