- Rename files: IssuesPage → TasksPage, IssueDetailPage → TaskDetailPage, CreateIssuePage → CreateTaskPage - Rename TypeScript interface: Issue → Task (keep backend field names) - Update routes: /issues → /tasks, /issues/new → /tasks/new, /issues/:id → /tasks/:id - Update CSS class names: issue-* → task-*, create-issue → create-task - Update UI text: 'Issues' → 'Tasks', 'Create Issue' → 'Create Task' - Keep 'issue' as a task subtype value in TASK_TYPES dropdown - Keep all backend API endpoint paths unchanged (/issues, /comments, etc.) - Rename local Task interface in MilestoneDetailPage to MilestoneTask to avoid conflict with the global Task type
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useParams, useNavigate } from 'react-router-dom'
|
|
import api from '@/services/api'
|
|
import type { Task, Comment } from '@/types'
|
|
import dayjs from 'dayjs'
|
|
|
|
export default function TaskDetailPage() {
|
|
const { id } = useParams()
|
|
const navigate = useNavigate()
|
|
const [task, setTask] = useState<Task | null>(null)
|
|
const [comments, setComments] = useState<Comment[]>([])
|
|
const [newComment, setNewComment] = useState('')
|
|
|
|
useEffect(() => {
|
|
api.get<Task>(`/issues/${id}`).then(({ data }) => setTask(data))
|
|
api.get<Comment[]>(`/issues/${id}/comments`).then(({ data }) => setComments(data))
|
|
}, [id])
|
|
|
|
const addComment = async () => {
|
|
if (!newComment.trim() || !task) return
|
|
await api.post('/comments', { content: newComment, issue_id: task.id, author_id: 1 })
|
|
setNewComment('')
|
|
const { data } = await api.get<Comment[]>(`/issues/${id}/comments`)
|
|
setComments(data)
|
|
}
|
|
|
|
const transition = async (newStatus: string) => {
|
|
await api.post(`/issues/${id}/transition?new_status=${newStatus}`)
|
|
const { data } = await api.get<Task>(`/issues/${id}`)
|
|
setTask(data)
|
|
}
|
|
|
|
if (!task) return <div className="loading">Loading...</div>
|
|
|
|
const statusActions: Record<string, string[]> = {
|
|
open: ['in_progress', 'blocked'],
|
|
in_progress: ['resolved', 'blocked'],
|
|
blocked: ['open', 'in_progress'],
|
|
resolved: ['closed', 'open'],
|
|
closed: ['open'],
|
|
}
|
|
|
|
return (
|
|
<div className="task-detail">
|
|
<button className="btn-back" onClick={() => navigate('/tasks')}>← Back</button>
|
|
|
|
<div className="task-header">
|
|
<h2>#{task.id} {task.title}</h2>
|
|
<div className="task-meta">
|
|
<span className={`badge status-${task.status}`}>{task.status}</span>
|
|
<span className={`badge priority-${task.priority}`}>{task.priority}</span>
|
|
<span className="badge">{task.issue_type}</span>{task.issue_subtype && <span className="badge">{task.issue_subtype}</span>}
|
|
{task.tags && <span className="tags">{task.tags}</span>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="task-body">
|
|
<div className="section">
|
|
<h3>Description</h3>
|
|
<p>{task.description || 'No description'}</p>
|
|
</div>
|
|
|
|
<div className="section">
|
|
<h3>Details</h3>
|
|
<dl>
|
|
<dt>Created</dt><dd>{dayjs(task.created_at).format('YYYY-MM-DD HH:mm')}</dd>
|
|
{task.due_date && <><dt>Due date</dt><dd>{dayjs(task.due_date).format('YYYY-MM-DD')}</dd></>}
|
|
{task.updated_at && <><dt>Updated</dt><dd>{dayjs(task.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
|
|
</dl>
|
|
</div>
|
|
|
|
<div className="section">
|
|
<h3>Status changes</h3>
|
|
<div className="actions">
|
|
{(statusActions[task.status] || []).map((s) => (
|
|
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="section">
|
|
<h3>Comments ({comments.length})</h3>
|
|
{comments.map((c) => (
|
|
<div className="comment" key={c.id}>
|
|
<div className="comment-meta">User #{c.author_id} · {dayjs(c.created_at).format('MM-DD HH:mm')}</div>
|
|
<p>{c.content}</p>
|
|
</div>
|
|
))}
|
|
<div className="comment-form">
|
|
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="Add a comment..." />
|
|
<button onClick={addComment} disabled={!newComment.trim()}>Submit comment</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|