kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1import { useNavigate } from "@tanstack/react-router";
2import { ArrowUpRight } from "lucide-react";
3import { useTranslation } from "react-i18next";
4import Activity from "@/components/activity";
5import CommentInput from "@/components/activity/comment-input";
6import { isCommentActivity } from "@/components/activity/utils";
7import { ExternalLinksAccordion } from "@/components/external-links/external-links-accordion";
8import useAuth from "@/components/providers/auth-provider/hooks/use-auth";
9import { Timeline } from "@/components/ui/timeline";
10import useGetActivitiesByTaskId from "@/hooks/queries/activity/use-get-activities-by-task-id";
11import useExternalLinks from "@/hooks/queries/external-link/use-external-links";
12import useGetProject from "@/hooks/queries/project/use-get-project";
13import useGetTask from "@/hooks/queries/task/use-get-task";
14import useGetTaskRelations from "@/hooks/queries/task-relation/use-get-task-relations";
15import type { ExternalLink } from "@/types/external-link";
16import TaskDescription from "./task-description";
17import TaskRelations from "./task-relations";
18import TaskSubtasks from "./task-subtasks";
19import TaskTitle from "./task-title";
20
21type TaskDetailsContentProps = {
22 taskId: string | undefined;
23 projectId: string;
24 workspaceId: string;
25 className?: string;
26};
27
28export default function TaskDetailsContent({
29 taskId,
30 projectId,
31 workspaceId,
32 className,
33}: TaskDetailsContentProps) {
34 const { t } = useTranslation();
35 const navigate = useNavigate();
36 const { data: task } = useGetTask(taskId ?? "");
37 const { data: project } = useGetProject({ id: projectId, workspaceId });
38 const { data: activities = [] } = useGetActivitiesByTaskId(taskId ?? "");
39 const { data: externalLinks = [], isLoading: isLoadingExternalLinks } =
40 useExternalLinks(taskId ?? "");
41 const { data: relations = [] } = useGetTaskRelations(taskId ?? "");
42 const { user } = useAuth();
43
44 const parentRelation = relations.find(
45 (rel) => rel.relationType === "subtask" && rel.targetTaskId === taskId,
46 );
47 const parentTask = parentRelation?.sourceTask;
48
49 if (!taskId) return null;
50
51 return (
52 <div className={`${className} gap-4`}>
53 <div className="flex flex-col gap-2.5">
54 {parentTask && (
55 <button
56 type="button"
57 className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors w-fit"
58 onClick={() =>
59 navigate({
60 to: "/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId",
61 params: {
62 workspaceId,
63 projectId,
64 taskId: parentTask.id,
65 },
66 })
67 }
68 >
69 <ArrowUpRight className="size-3" />
70 <span>
71 {t("tasks:detail.subtaskOf")}{" "}
72 <span className="font-medium">{parentTask.title}</span>
73 </span>
74 </button>
75 )}
76 <p className="text-xs font-semibold text-foreground/70">
77 {project?.slug}-{task?.number}
78 </p>
79 <TaskTitle taskId={taskId} />
80 <TaskDescription taskId={taskId} />
81 </div>
82 {!isLoadingExternalLinks && externalLinks.length > 0 && (
83 <div className="mt-4">
84 <ExternalLinksAccordion
85 externalLinks={externalLinks as ExternalLink[]}
86 isLoading={isLoadingExternalLinks}
87 />
88 </div>
89 )}
90 <div className="mt-4">
91 <TaskSubtasks
92 taskId={taskId}
93 projectId={projectId}
94 workspaceId={workspaceId}
95 />
96 </div>
97 <div className="mt-2">
98 <TaskRelations
99 taskId={taskId}
100 projectId={projectId}
101 workspaceId={workspaceId}
102 />
103 </div>
104 <span className="text-sm font-medium text-muted-foreground h-[1px] bg-border w-full block shrink-0" />
105 <div className="flex flex-col gap-4">
106 <h1 className="text-md font-semibold">{t("tasks:detail.activity")}</h1>
107 {user?.id && taskId && <CommentInput taskId={taskId} />}
108 {activities.length > 0 ? (
109 <Timeline>
110 {activities.map((activity, index) => {
111 const nextActivity = activities[index + 1];
112 const showConnector =
113 !isCommentActivity(activity) &&
114 Boolean(nextActivity) &&
115 !isCommentActivity(nextActivity);
116
117 return (
118 <Activity
119 key={activity.id}
120 activity={activity}
121 step={activities.length - index}
122 showConnector={showConnector}
123 />
124 );
125 })}
126 </Timeline>
127 ) : (
128 <p className="text-sm font-medium text-muted-foreground">
129 {t("tasks:detail.noActivity")}
130 </p>
131 )}
132 </div>
133 </div>
134 );
135}