Files
kandev/apps/web/components/kanban-column.tsx
T
José Almeida c64e835612 feat: allow moving tasks across workflows (#822)
* feat: allow moving tasks across workflows

* fix: address PR review feedback

* fix: address remaining PR review feedback

* fix: remove unreachable move state event

* fix: map task move conflicts correctly

* fix: address PR review nitpicks

* fix: use neutral task move error message
2026-05-05 14:13:43 +01:00

119 lines
3.5 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { useDroppable } from "@dnd-kit/core";
import { KanbanCard, resolveTaskRepositoryNames, Task } from "./kanban-card";
import { Badge } from "@kandev/ui/badge";
import { cn } from "@/lib/utils";
import { useAppStore } from "@/components/state-provider";
import type { Repository } from "@/lib/types/http";
export interface WorkflowStep {
id: string;
title: string;
color: string;
events?: {
on_enter?: Array<{ type: string; config?: Record<string, unknown> }>;
on_turn_complete?: Array<{ type: string; config?: Record<string, unknown> }>;
};
}
interface KanbanColumnProps {
step: WorkflowStep;
tasks: Task[];
onPreviewTask: (task: Task) => void;
onOpenTask: (task: Task) => void;
onEditTask: (task: Task) => void;
onDeleteTask: (task: Task) => void;
onArchiveTask?: (task: Task) => void;
onMoveTask?: (task: Task, targetStepId: string) => void;
steps?: WorkflowStep[];
showMaximizeButton?: boolean;
deletingTaskId?: string | null;
archivingTaskId?: string | null;
hideHeader?: boolean;
selectedIds?: Set<string>;
onToggleSelect?: (taskId: string) => void;
isMultiSelectMode?: boolean;
}
export function KanbanColumn({
step,
tasks,
onPreviewTask,
onOpenTask,
onEditTask,
onDeleteTask,
onArchiveTask,
onMoveTask,
steps,
showMaximizeButton,
deletingTaskId,
archivingTaskId,
hideHeader = false,
selectedIds,
onToggleSelect,
isMultiSelectMode,
}: KanbanColumnProps) {
const { setNodeRef, isOver } = useDroppable({
id: step.id,
});
// Access repositories from store to pass repository names to cards
const repositoriesByWorkspace = useAppStore((state) => state.repositories.itemsByWorkspaceId);
const repositories = useMemo(
() => Object.values(repositoriesByWorkspace).flat() as Repository[],
[repositoriesByWorkspace],
);
return (
<div
ref={setNodeRef}
data-testid={`kanban-column-${step.id}`}
className={cn(
"flex flex-col flex-1 h-full min-w-0 px-3 py-2 sm:min-h-[200px]",
"border-r border-dashed border-border/50 last:border-r-0",
isOver && "bg-primary/5",
)}
>
{/* Column Header */}
{!hideHeader && (
<div className="flex items-center justify-between pb-2 mb-3 px-1">
<div className="flex items-center gap-2">
<div className={cn("w-2 h-2 rounded-full", step.color)} />
<h2 className="font-semibold text-sm">{step.title}</h2>
<Badge variant="secondary" className="text-xs">
{tasks.length}
</Badge>
</div>
</div>
)}
{/* Tasks */}
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden space-y-2 px-1 pt-1">
{tasks.map((task) => (
<KanbanCard
key={task.id}
task={task}
repositoryNames={resolveTaskRepositoryNames(task, repositories)}
onClick={onPreviewTask}
onOpenFullPage={onOpenTask}
onEdit={onEditTask}
onDelete={onDeleteTask}
onArchive={onArchiveTask}
onMove={onMoveTask}
steps={steps}
showMaximizeButton={showMaximizeButton}
isDeleting={deletingTaskId === task.id}
isArchiving={archivingTaskId === task.id}
isSelected={selectedIds?.has(task.id)}
selectedIds={selectedIds}
onToggleSelect={onToggleSelect}
isMultiSelectMode={isMultiSelectMode}
/>
))}
</div>
</div>
);
}