Subtasks UI in TaskModal (Jira-style)

- Parent breadcrumb: 'TE-1 / TE-3' with clickable parent link
- Subtasks section: colored status dots, clickable keys, assignee
- onOpenTask callback: close current modal, open clicked task
- SubtaskBrief interface in API types
This commit is contained in:
Markov 2026-02-28 00:03:14 +01:00
parent aa7c45657b
commit 4770fc62d7
3 changed files with 57 additions and 2 deletions

View File

@ -218,6 +218,10 @@ export default function KanbanBoard({ projectId, projectSlug }: Props) {
setTasks((prev) => prev.filter((t) => t.id !== id)); setTasks((prev) => prev.filter((t) => t.id !== id));
setSelectedTask(null); setSelectedTask(null);
}} }}
onOpenTask={(taskId) => {
const t = tasks.find(t => t.id === taskId);
if (t) setSelectedTask(t);
}}
/> />
)} )}

View File

@ -36,9 +36,10 @@ interface Props {
onClose: () => void; onClose: () => void;
onUpdated: (task: Task) => void; onUpdated: (task: Task) => void;
onDeleted: (taskId: string) => void; onDeleted: (taskId: string) => void;
onOpenTask?: (taskId: string) => void;
} }
export default function TaskModal({ task, projectId: _projectId, projectSlug: _projectSlug, onClose, onUpdated, onDeleted }: Props) { export default function TaskModal({ task, projectId: _projectId, projectSlug: _projectSlug, onClose, onUpdated, onDeleted, onOpenTask }: Props) {
const [title, setTitle] = useState(task.title); const [title, setTitle] = useState(task.title);
const [description, setDescription] = useState(task.description || ""); const [description, setDescription] = useState(task.description || "");
const [status, setStatus] = useState(task.status); const [status, setStatus] = useState(task.status);
@ -161,7 +162,18 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
</button> </button>
</div> </div>
<div className="text-xs text-[var(--muted)]">{task.key}</div> <div className="text-xs text-[var(--muted)]">
{task.parent_key && task.parent_id ? (
<span>
<button
onClick={() => { onClose(); setTimeout(() => onOpenTask?.(task.parent_id!), 100); }}
className="text-[var(--accent)] hover:underline"
>{task.parent_key}</button>
<span className="mx-1">/</span>
</span>
) : null}
{task.key}
</div>
</div> </div>
<div className="flex flex-col md:flex-row"> <div className="flex flex-col md:flex-row">
@ -239,6 +251,34 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
</div> </div>
</div> </div>
{/* Subtasks */}
{task.subtasks && task.subtasks.length > 0 && (
<div>
<div className="text-sm text-[var(--muted)] mb-2">Подзадачи ({task.subtasks.length})</div>
<div className="space-y-1">
{task.subtasks.map((sub) => (
<div key={sub.id} className="flex items-center justify-between p-1.5 bg-[var(--bg)] border border-[var(--border)] rounded text-xs">
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${
sub.status === "done" ? "bg-green-500" :
sub.status === "in_progress" ? "bg-blue-500" :
sub.status === "in_review" ? "bg-yellow-500" : "bg-gray-500"
}`} />
<button
onClick={() => { onClose(); setTimeout(() => onOpenTask?.(sub.id), 100); }}
className="text-[var(--accent)] hover:underline font-mono"
>{sub.key}</button>
<span>{sub.title}</span>
</div>
{sub.assignee && (
<span className="text-[var(--muted)]"> {sub.assignee.slug}</span>
)}
</div>
))}
</div>
</div>
)}
{/* Dependencies */} {/* Dependencies */}
{(links.length > 0 || showAddLink) && ( {(links.length > 0 || showAddLink) && (
<div> <div>

View File

@ -127,10 +127,21 @@ export interface Task {
position: number; position: number;
time_spent: number; time_spent: number;
steps: Step[]; steps: Step[];
subtasks: SubtaskBrief[];
parent_key: string | null;
parent_title: string | null;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }
export interface SubtaskBrief {
id: string;
key: string;
title: string;
status: string;
assignee: MemberBrief | null;
}
export interface Attachment { export interface Attachment {
id: string; id: string;
filename: string; filename: string;