Files
kandev/apps/web/app/tasks/columns.tsx
T
2026-02-01 19:07:52 +00:00

146 lines
4.2 KiB
TypeScript

"use client"
import type { ColumnDef } from "@tanstack/react-table"
import type { Task, Board, WorkflowStep, Repository } from "@/lib/types/http"
import Link from "next/link"
import { IconTrash, IconLoader } from "@tabler/icons-react"
import { Button } from "@kandev/ui/button"
import { Badge } from "@kandev/ui/badge"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip"
import { formatDistanceToNow } from "date-fns"
type TaskWithResolution = Task & {
boardName?: string
stepName?: string
repositoryNames?: string[]
}
interface ColumnsConfig {
boards: Board[]
steps: WorkflowStep[]
repositories: Repository[]
onDelete: (taskId: string) => void
deletingTaskId: string | null
}
export function getColumns({
boards,
steps,
repositories,
onDelete,
deletingTaskId,
}: ColumnsConfig): ColumnDef<TaskWithResolution>[] {
const boardMap = new Map(boards.map((b) => [b.id, b.name]))
const stepMap = new Map(steps.map((s) => [s.id, s.name]))
const repoMap = new Map(repositories.map((r) => [r.id, { name: r.name, path: r.local_path }]))
return [
{
accessorKey: "title",
header: "Title",
cell: ({ row }) => {
const task = row.original
const sessionId = task.primary_session_id
if (sessionId) {
return (
<Link
href={`/s/${sessionId}`}
className="text-primary hover:underline font-medium"
>
{task.title}
</Link>
)
}
return <span className="font-medium">{task.title}</span>
},
},
{
accessorKey: "board_id",
header: "Board",
cell: ({ row }) => {
const boardName = boardMap.get(row.original.board_id)
return boardName || "-"
},
},
{
accessorKey: "workflow_step_id",
header: "Column",
cell: ({ row }) => {
const stepName = stepMap.get(row.original.workflow_step_id)
return stepName || "-"
},
},
{
accessorKey: "repositories",
header: "Repository",
cell: ({ row }) => {
const taskRepos = row.original.repositories || []
if (taskRepos.length === 0) return "-"
const repoInfos = taskRepos
.map((tr) => repoMap.get(tr.repository_id))
.filter((info): info is { name: string; path: string } => info !== undefined)
if (repoInfos.length === 0) return "-"
return (
<TooltipProvider>
<div className="flex flex-wrap gap-1">
{repoInfos.map((info, idx) => (
<Tooltip key={idx}>
<TooltipTrigger asChild>
<span className="cursor-default hover:underline">{info.name}</span>
</TooltipTrigger>
<TooltipContent>
<p className="font-mono text-xs">{info.path || 'No path'}</p>
</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
)
},
},
{
accessorKey: "state",
header: "State",
cell: ({ row }) => {
const state = row.original.state
const variant = state === "COMPLETED" ? "default" : state === "IN_PROGRESS" ? "secondary" : "outline"
return <Badge variant={variant}>{state}</Badge>
},
},
{
accessorKey: "updated_at",
header: "Updated",
cell: ({ row }) => {
const date = new Date(row.original.updated_at)
return formatDistanceToNow(date, { addSuffix: true })
},
},
{
id: "actions",
header: "Actions",
cell: ({ row }) => {
const isDeleting = deletingTaskId === row.original.id
return (
<Button
variant="ghost"
size="sm"
className="cursor-pointer"
disabled={isDeleting}
onClick={(e) => {
e.stopPropagation()
onDelete(row.original.id)
}}
>
{isDeleting ? (
<IconLoader className="h-4 w-4 animate-spin" />
) : (
<IconTrash className="h-4 w-4 text-destructive" />
)}
</Button>
)
},
},
]
}