import { useNavigate } from "@tanstack/react-router"; import { ChevronDown, ChevronRight, Link2, Plus, Search, X, } from "lucide-react"; import { Fragment, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Command, CommandCollection, CommandDialog, CommandDialogPopup, CommandEmpty, CommandFooter, CommandGroup, CommandGroupLabel, CommandInput, CommandItem, CommandList, CommandPanel, CommandSeparator, } from "@/components/ui/command"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; import useCreateTaskRelation from "@/hooks/mutations/task-relation/use-create-task-relation"; import useDeleteTaskRelation from "@/hooks/mutations/task-relation/use-delete-task-relation"; import useGetProject from "@/hooks/queries/project/use-get-project"; import { useGetTasks } from "@/hooks/queries/task/use-get-tasks"; import useGetTaskRelations from "@/hooks/queries/task-relation/use-get-task-relations"; import useActiveWorkspace from "@/hooks/queries/workspace/use-active-workspace"; import { useGetActiveWorkspaceUsers } from "@/hooks/queries/workspace-users/use-get-active-workspace-users"; import { getColumnIcon } from "@/lib/column"; import { toast } from "@/lib/toast"; import type Task from "@/types/task"; import SubtaskAssigneePopover from "./subtask-assignee-popover"; import SubtaskStatusPopover from "./subtask-status-popover"; type TaskRelationsProps = { taskId: string; projectId: string; workspaceId: string; }; type TaskItem = { id: string; title: string; number: number | null; status: string; }; type TaskGroup = { value: string; label: string; items: TaskItem[]; }; export default function TaskRelations({ taskId, projectId, workspaceId, }: TaskRelationsProps) { const { t } = useTranslation(); const navigate = useNavigate(); const [isOpen, setIsOpen] = useState(true); const [commandOpen, setCommandOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [selectedRelationType, setSelectedRelationType] = useState< "blocks" | "related" >("related"); const { data: relations = [] } = useGetTaskRelations(taskId); const { data: projectData } = useGetTasks(projectId); const { data: project } = useGetProject({ id: projectId, workspaceId }); const { data: workspace } = useActiveWorkspace(); const { data: workspaceUsers } = useGetActiveWorkspaceUsers( workspace?.id ?? "", ); const createRelation = useCreateTaskRelation(); const deleteRelation = useDeleteTaskRelation(taskId); useEffect(() => { if (!commandOpen) { setSearchQuery(""); } }, [commandOpen]); const nonSubtaskRelations = relations.filter( (rel) => rel.relationType !== "subtask", ); const groupedRelations = useMemo(() => { const groups: Record< string, Array<{ id: string; relationType: string; task: NonNullable<(typeof nonSubtaskRelations)[number]["sourceTask"]>; }> > = {}; for (const rel of nonSubtaskRelations) { const isSource = rel.sourceTaskId === taskId; const linkedTask = isSource ? rel.targetTask : rel.sourceTask; if (!linkedTask) continue; const type = rel.relationType; if (!groups[type]) { groups[type] = []; } groups[type].push({ id: rel.id, relationType: type, task: linkedTask, }); } return groups; }, [nonSubtaskRelations, taskId]); const existingRelatedTaskIds = new Set( nonSubtaskRelations.flatMap((rel) => [rel.sourceTaskId, rel.targetTaskId]), ); existingRelatedTaskIds.add(taskId); const allTasks = useMemo(() => { if (!projectData) return []; const tasks: TaskItem[] = []; if ("columns" in projectData && Array.isArray(projectData.columns)) { for (const col of projectData.columns as Array<{ tasks: TaskItem[]; }>) { if (col.tasks) { for (const t of col.tasks) { tasks.push(t); } } } } return tasks; }, [projectData]); const filteredTasks = allTasks.filter( (t) => !existingRelatedTaskIds.has(t.id), ); const commandGroups = useMemo(() => { return [ { value: "tasks", label: t("tasks:relations.tasksInProject"), items: filteredTasks, }, ]; }, [filteredTasks, t]); const handleLinkTask = async (targetTaskId: string) => { try { await createRelation.mutateAsync({ sourceTaskId: taskId, targetTaskId, relationType: selectedRelationType, }); setCommandOpen(false); setSearchQuery(""); } catch { toast.error(t("tasks:relations.linkError")); } }; const handleRemoveRelation = (relationId: string) => { deleteRelation.mutate(relationId); }; const handleNavigateToTask = (linkedTaskId: string) => { navigate({ to: "/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId", params: { workspaceId, projectId, taskId: linkedTaskId }, }); }; const getAssignee = (userId: string | null) => { if (!userId || !workspaceUsers?.members) return null; return workspaceUsers.members.find((member) => member.userId === userId); }; const buildTaskObject = (item: { task: NonNullable<(typeof nonSubtaskRelations)[number]["sourceTask"]>; }): Task => ({ id: item.task.id, title: item.task.title, number: item.task.number, description: null, status: item.task.status, priority: item.task.priority, dueDate: null, position: null, createdAt: "", userId: item.task.userId, assigneeId: item.task.userId, assigneeName: item.task.assigneeName, projectId: item.task.projectId, }); const totalCount = nonSubtaskRelations.length; return ( <>
{totalCount > 0 && ( {totalCount} )}
{Object.entries(groupedRelations).map(([type, items]) => (
{t(`tasks:relations.types.${type}`, { defaultValue: type })}
{items.map((item) => { const assignee = getAssignee(item.task.userId); const taskObj = buildTaskObject(item); return (
handleNavigateToTask(item.task.id)} > {t("tasks:relations.openTask")} handleRemoveRelation(item.id)} > {t("tasks:relations.removeRelation")}
); })}
))} {totalCount === 0 && (

{t("tasks:relations.empty")}

)}
setSearchQuery(e.target.value)} />

{t("tasks:relations.noTasksFound")}

{(group: TaskGroup, groupIndex: number) => ( {group.label} {(item: TaskItem) => ( handleLinkTask(item.id)} className="flex items-center gap-3 py-2" > {getColumnIcon(item.status, false)} {project?.slug}-{item.number} {item.title} )} {groupIndex < commandGroups.length - 1 && ( )} )}
{t("tasks:relations.selectTask")}
); }