c64e835612
* 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
119 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|