Task dependencies UI in TaskModal + API
- Dependencies section shows links with colored labels - Add dependency form (type + task ID) - Delete link button - API: getTaskLinks, createTaskLink, deleteTaskLink
This commit is contained in:
parent
a9da3672b9
commit
be3007f40a
@ -1,10 +1,11 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Task, Member, Step, Message } from "@/lib/api";
|
||||
import type { Task, Member, Step, Message, TaskLink } from "@/lib/api";
|
||||
import {
|
||||
updateTask, deleteTask, getMembers,
|
||||
getSteps, createStep, updateStep, deleteStep as _deleteStepApi,
|
||||
getMessages, sendMessage,
|
||||
getTaskLinks, createTaskLink, deleteTaskLink,
|
||||
} from "@/lib/api";
|
||||
|
||||
const STATUSES = [
|
||||
@ -45,6 +46,10 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
|
||||
const [assigneeId, setAssigneeId] = useState(task.assignee_id || "");
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [steps, setSteps] = useState<Step[]>(task.steps || []);
|
||||
const [links, setLinks] = useState<TaskLink[]>([]);
|
||||
const [showAddLink, setShowAddLink] = useState(false);
|
||||
const [linkTargetId, setLinkTargetId] = useState("");
|
||||
const [linkType, setLinkType] = useState("depends_on");
|
||||
const [comments, setComments] = useState<Message[]>([]);
|
||||
const [_saving, setSaving] = useState(false);
|
||||
const [editingDesc, setEditingDesc] = useState(false);
|
||||
@ -56,6 +61,7 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
|
||||
useEffect(() => {
|
||||
getMembers().then(setMembers).catch(() => {});
|
||||
getSteps(task.id).then(setSteps).catch(() => {});
|
||||
getTaskLinks(task.id).then(setLinks).catch(() => {});
|
||||
getMessages({ task_id: task.id }).then(setComments).catch(() => {});
|
||||
}, [task.id]);
|
||||
|
||||
@ -233,6 +239,68 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dependencies */}
|
||||
{(links.length > 0 || showAddLink) && (
|
||||
<div>
|
||||
<div className="text-sm text-[var(--muted)] mb-2">Зависимости</div>
|
||||
<div className="space-y-1 mb-2">
|
||||
{links.map((link) => {
|
||||
const label = link.link_type === "blocks" ? "🔴 Блокирует" :
|
||||
link.link_type === "blocked_by" ? "🟡 Заблокировано" :
|
||||
link.link_type === "depends_on" ? "🟡 Зависит от" :
|
||||
link.link_type === "required_by" ? "🔵 Требуется для" :
|
||||
"🔗 Связано с";
|
||||
const title = link.target_title || link.source_title || "...";
|
||||
return (
|
||||
<div key={link.id} className="flex items-center justify-between text-xs p-1.5 bg-[var(--bg)] border border-[var(--border)] rounded">
|
||||
<span>{label}: <span className="font-medium">{title}</span></span>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await deleteTaskLink(task.id, link.id);
|
||||
setLinks(links.filter(l => l.id !== link.id));
|
||||
}}
|
||||
className="text-red-400 hover:text-red-300 ml-2"
|
||||
>×</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{showAddLink && (
|
||||
<div className="flex gap-1 mb-2">
|
||||
<select value={linkType} onChange={e => setLinkType(e.target.value)}
|
||||
className="px-2 py-1 bg-[var(--bg)] border border-[var(--border)] rounded text-xs">
|
||||
<option value="depends_on">Зависит от</option>
|
||||
<option value="blocks">Блокирует</option>
|
||||
<option value="relates_to">Связано с</option>
|
||||
</select>
|
||||
<input
|
||||
value={linkTargetId}
|
||||
onChange={e => setLinkTargetId(e.target.value)}
|
||||
placeholder="ID задачи..."
|
||||
className="flex-1 px-2 py-1 bg-[var(--bg)] border border-[var(--border)] rounded text-xs outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!linkTargetId) return;
|
||||
const link = await createTaskLink(task.id, linkTargetId, linkType);
|
||||
setLinks([...links, link]);
|
||||
setLinkTargetId("");
|
||||
setShowAddLink(false);
|
||||
}}
|
||||
className="px-2 py-1 bg-[var(--accent)] text-white rounded text-xs"
|
||||
>+</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setShowAddLink(!showAddLink)}
|
||||
className="text-xs text-[var(--accent)] hover:underline mb-2"
|
||||
>
|
||||
{showAddLink ? "Скрыть" : "+ Добавить зависимость"}
|
||||
</button>
|
||||
|
||||
{/* Comments */}
|
||||
<div>
|
||||
<div className="text-sm text-[var(--muted)] mb-2">Комментарии</div>
|
||||
|
||||
@ -426,4 +426,25 @@ export async function addProjectMember(projectId: string, memberId: string): Pro
|
||||
|
||||
export async function removeProjectMember(projectId: string, memberId: string): Promise<{ ok: boolean }> {
|
||||
return request(`/api/v1/projects/${projectId}/members/${memberId}`, { method: "DELETE" });
|
||||
}
|
||||
}
|
||||
// --- Task Links ---
|
||||
export interface TaskLink {
|
||||
id: string;
|
||||
source_id: string;
|
||||
target_id: string;
|
||||
link_type: string;
|
||||
target_title?: string;
|
||||
source_title?: string;
|
||||
}
|
||||
|
||||
export async function getTaskLinks(taskId: string): Promise<TaskLink[]> {
|
||||
return request(`/tasks/${taskId}/links`);
|
||||
}
|
||||
|
||||
export async function createTaskLink(taskId: string, targetId: string, linkType: string): Promise<TaskLink> {
|
||||
return request(`/tasks/${taskId}/links`, { method: "POST", body: JSON.stringify({ target_id: targetId, link_type: linkType }) });
|
||||
}
|
||||
|
||||
export async function deleteTaskLink(taskId: string, linkId: string): Promise<void> {
|
||||
return request(`/tasks/${taskId}/links/${linkId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user