Compare commits

...

1 Commits

Author SHA1 Message Date
zhi
01affdb020 refactor: rename Issue → Task throughout frontend
- 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
2026-03-16 07:47:58 +00:00
11 changed files with 94 additions and 94 deletions

View File

@@ -5,9 +5,9 @@ import Sidebar from '@/components/Sidebar'
import LoginPage from '@/pages/LoginPage'
import SetupWizardPage from '@/pages/SetupWizardPage'
import DashboardPage from '@/pages/DashboardPage'
import IssuesPage from '@/pages/IssuesPage'
import IssueDetailPage from '@/pages/IssueDetailPage'
import CreateIssuePage from '@/pages/CreateIssuePage'
import TasksPage from '@/pages/TasksPage'
import TaskDetailPage from '@/pages/TaskDetailPage'
import CreateTaskPage from '@/pages/CreateTaskPage'
import ProjectsPage from '@/pages/ProjectsPage'
import ProjectDetailPage from '@/pages/ProjectDetailPage'
import MilestonesPage from '@/pages/MilestonesPage'
@@ -85,9 +85,9 @@ export default function App() {
<main className="main-content">
<Routes>
<Route path="/" element={<DashboardPage />} />
<Route path="/issues" element={<IssuesPage />} />
<Route path="/issues/new" element={<CreateIssuePage />} />
<Route path="/issues/:id" element={<IssueDetailPage />} />
<Route path="/tasks" element={<TasksPage />} />
<Route path="/tasks/new" element={<CreateTaskPage />} />
<Route path="/tasks/:id" element={<TaskDetailPage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/projects/:id" element={<ProjectDetailPage />} />
<Route path="/milestones" element={<MilestonesPage />} />

View File

@@ -63,7 +63,7 @@ thead th { text-align: left; padding: 10px 12px; border-bottom: 2px solid var(--
tbody td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
tr.clickable { cursor: pointer; }
tr.clickable:hover { background: var(--bg-hover); }
.issue-title { font-weight: 500; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-title { font-weight: 500; max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Badges */
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: .75rem; font-weight: 600; text-transform: capitalize; color: #fff; background: var(--text-dim); }
@@ -92,10 +92,10 @@ tr.clickable:hover { background: var(--bg-hover); }
.pagination button { padding: 6px 14px; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); border-radius: 6px; cursor: pointer; }
.pagination button:disabled { opacity: .4; cursor: default; }
/* Issue detail */
.issue-header { margin-bottom: 20px; }
.issue-header h2 { margin-bottom: 8px; }
.issue-meta { display: flex; gap: 8px; flex-wrap: wrap; }
/* Task detail */
.task-header { margin-bottom: 20px; }
.task-header h2 { margin-bottom: 8px; }
.task-meta { display: flex; gap: 8px; flex-wrap: wrap; }
.tags { color: var(--accent); font-size: .85rem; }
.section { margin: 20px 0; }
.section h3 { margin-bottom: 8px; color: var(--text-dim); font-size: .9rem; text-transform: uppercase; }
@@ -113,11 +113,11 @@ dd { font-size: .9rem; }
.comment-form textarea { width: 100%; min-height: 80px; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); resize: vertical; margin-bottom: 8px; }
.comment-form button { padding: 8px 16px; background: var(--accent); color: #fff; border: none; border-radius: 6px; cursor: pointer; }
/* Create Issue form */
.create-issue form { max-width: 600px; display: flex; flex-direction: column; gap: 14px; }
.create-issue label { display: flex; flex-direction: column; gap: 4px; font-size: .85rem; color: var(--text-dim); }
.create-issue input, .create-issue textarea, .create-issue select { padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: .95rem; }
.create-issue textarea { min-height: 100px; resize: vertical; }
/* Create Task form */
.create-task form { max-width: 600px; display: flex; flex-direction: column; gap: 14px; }
.create-task label { display: flex; flex-direction: column; gap: 4px; font-size: .85rem; color: var(--text-dim); }
.create-task input, .create-task textarea, .create-task select { padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: .95rem; }
.create-task textarea { min-height: 100px; resize: vertical; }
/* Project grid */
.project-grid, .milestone-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin-top: 16px; }

View File

@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
import api from '@/services/api'
import type { Project } from '@/types'
const ISSUE_TYPES = [
const TASK_TYPES = [
{ value: 'story', label: 'Story', subtypes: ['feature', 'improvement', 'refactor'] },
{ value: 'issue', label: 'Issue', subtypes: ['infrastructure', 'performance', 'regression', 'security', 'user_experience', 'defect'] },
{ value: 'task', label: 'Task', subtypes: ['defect'] },
@@ -16,11 +16,11 @@ const ISSUE_TYPES = [
{ value: 'resolution', label: 'Resolution', subtypes: [] },
]
export default function CreateIssuePage() {
export default function CreateTaskPage() {
const navigate = useNavigate()
const [projects, setProjects] = useState<Project[]>([])
const [form, setForm] = useState({
title: '', description: '', project_id: 0, issue_type: 'issue',
title: '', description: '', project_id: 0, issue_type: 'task',
issue_subtype: '', priority: 'medium', tags: '', reporter_id: 1,
})
@@ -31,7 +31,7 @@ export default function CreateIssuePage() {
})
}, [])
const currentType = ISSUE_TYPES.find(t => t.value === form.issue_type) || ISSUE_TYPES[1]
const currentType = TASK_TYPES.find(t => t.value === form.issue_type) || TASK_TYPES[2]
const subtypes = currentType.subtypes || []
const handleTypeChange = (newType: string) => {
@@ -43,12 +43,12 @@ export default function CreateIssuePage() {
const payload: any = { ...form, tags: form.tags || null }
if (!form.issue_subtype) delete payload.issue_subtype
await api.post('/issues', payload)
navigate('/issues')
navigate('/tasks')
}
return (
<div className="create-issue">
<h2>Create Issue</h2>
<div className="create-task">
<h2>Create Task</h2>
<form onSubmit={submit}>
<label>Title <input required value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></label>
<label>Description <textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
@@ -59,7 +59,7 @@ export default function CreateIssuePage() {
</label>
<label>Type
<select value={form.issue_type} onChange={(e) => handleTypeChange(e.target.value)}>
{ISSUE_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
{TASK_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
</select>
</label>
{subtypes.length > 0 && (

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import api from '@/services/api'
import type { DashboardStats } from '@/types'
import type { DashboardStats, Task } from '@/types'
export default function DashboardPage() {
const [stats, setStats] = useState<DashboardStats | null>(null)
@@ -26,7 +26,7 @@ export default function DashboardPage() {
<div className="stats-grid">
<div className="stat-card total">
<span className="stat-number">{stats.total_issues}</span>
<span className="stat-label">Total Issues</span>
<span className="stat-label">Total Tasks</span>
</div>
{Object.entries(stats.by_status || {}).map(([k, v]) => (
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
@@ -52,7 +52,7 @@ export default function DashboardPage() {
</div>
<div className="section">
<h3>Recent Issues</h3>
<h3>Recent Tasks</h3>
<table>
<thead>
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Subtype</th></tr>
@@ -61,7 +61,7 @@ export default function DashboardPage() {
{(stats.recent_issues || []).map((i) => (
<tr key={i.id}>
<td>#{i.id}</td>
<td><a href={`/issues/${i.id}`}>{i.title}</a></td>
<td><a href={`/tasks/${i.id}`}>{i.title}</a></td>
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
<td>{i.issue_type}</td><td>{i.issue_subtype || "-"}</td>

View File

@@ -1,10 +1,10 @@
import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import api from '@/services/api'
import type { Milestone, MilestoneProgress, Issue } from '@/types'
import type { Milestone, MilestoneProgress, Task } from '@/types'
import dayjs from 'dayjs'
interface Task {
interface MilestoneTask {
id: number
title: string
description?: string
@@ -24,9 +24,9 @@ export default function MilestoneDetailPage() {
const navigate = useNavigate()
const [milestone, setMilestone] = useState<Milestone | null>(null)
const [progress, setProgress] = useState<MilestoneProgress | null>(null)
const [tasks, setTasks] = useState<Task[]>([])
const [supports, setSupports] = useState<Issue[]>([])
const [meetings, setMeetings] = useState<Issue[]>([])
const [tasks, setTasks] = useState<MilestoneTask[]>([])
const [supports, setSupports] = useState<Task[]>([])
const [meetings, setMeetings] = useState<Task[]>([])
const [activeTab, setActiveTab] = useState<'tasks' | 'supports' | 'meetings'>('tasks')
const [showCreateTask, setShowCreateTask] = useState(false)
const [showCreateSupport, setShowCreateSupport] = useState(false)
@@ -52,9 +52,9 @@ export default function MilestoneDetailPage() {
useEffect(() => {
if (!projectCode || !id) return
api.get<Task[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data)).catch(() => {})
api.get<Issue[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data)).catch(() => {})
api.get<Issue[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data)).catch(() => {})
api.get<MilestoneTask[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data)).catch(() => {})
api.get<Task[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data)).catch(() => {})
api.get<Task[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data)).catch(() => {})
}, [projectCode, id])
const createItem = async (type: 'tasks' | 'supports' | 'meetings') => {
@@ -74,19 +74,19 @@ export default function MilestoneDetailPage() {
setShowCreateSupport(false)
setShowCreateMeeting(false)
// Refresh
api.get<Task[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data))
api.get<Issue[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data))
api.get<Issue[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data))
api.get<MilestoneTask[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data))
api.get<Task[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data))
api.get<Task[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data))
}
const isProgressing = milestone?.status === 'progressing'
if (!milestone) return <div className="loading">Loading...</div>
const renderTaskRow = (t: Task) => (
const renderTaskRow = (t: MilestoneTask) => (
<tr key={t.id} className="clickable" onClick={() => navigate(`/milestones/${id}`)}>
<td>{t.task_code || t.id}</td>
<td className="issue-title">{t.title}</td>
<td className="task-title">{t.title}</td>
<td><span className={`badge status-${t.task_status || t.status}`}>{t.task_status || t.status}</span></td>
<td>{t.estimated_effort || '-'}</td>
<td>{t.estimated_working_time || '-'}</td>
@@ -97,9 +97,9 @@ export default function MilestoneDetailPage() {
<div className="milestone-detail">
<button className="btn-back" onClick={() => navigate('/milestones')}> Back to Milestones</button>
<div className="issue-header">
<div className="task-header">
<h2>🏁 {milestone.title}</h2>
<div className="issue-meta">
<div className="task-meta">
<span className={`badge status-${milestone.status === 'progressing' ? 'in_progress' : milestone.status}`}>{milestone.status}</span>
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
{milestone.planned_release_date && <span className="text-dim">Planned Release: {dayjs(milestone.planned_release_date).format('YYYY-MM-DD')}</span>}
@@ -207,7 +207,7 @@ export default function MilestoneDetailPage() {
{supports.map((i) => (
<tr key={i.id} className="clickable" onClick={() => navigate(`/supports/${projectCode}/${id}/${i.id}`)}>
<td>{i.id}</td>
<td className="issue-title">{i.title}</td>
<td className="task-title">{i.title}</td>
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
</tr>
@@ -224,7 +224,7 @@ export default function MilestoneDetailPage() {
{meetings.map((i) => (
<tr key={i.id} className="clickable" onClick={() => navigate(`/meetings/${projectCode}/${id}/${i.id}`)}>
<td>{i.id}</td>
<td className="issue-title">{i.title}</td>
<td className="task-title">{i.title}</td>
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
</tr>

View File

@@ -110,7 +110,7 @@ export default function MonitorPage() {
<div className="stats-grid">
<div className="stat-card total">
<span className="stat-number">{data.issues.total_issues}</span>
<span className="stat-label">Total Issues</span>
<span className="stat-label">Total Tasks</span>
</div>
<div className="stat-card" style={{ borderLeftColor: 'var(--accent)' }}>
<span className="stat-number">{data.issues.new_issues_24h}</span>

View File

@@ -52,7 +52,7 @@ export default function NotificationsPage() {
className={`notification-item ${n.is_read ? 'read' : 'unread'}`}
onClick={() => {
if (!n.is_read) markRead(n.id)
if (n.issue_id) navigate(`/issues/${n.issue_id}`)
if (n.issue_id) navigate(`/tasks/${n.issue_id}`)
}}
>
<div className="notification-dot">{n.is_read ? '' : '●'}</div>

View File

@@ -96,7 +96,7 @@ export default function ProjectDetailPage() {
<div className="project-detail">
<button className="btn-back" onClick={() => navigate('/projects')}> Back to projects</button>
<div className="issue-header">
<div className="task-header">
{editing ? (
<form className="inline-form" onSubmit={updateProject}>
<div style={{ fontWeight: 600 }}>{project.name}</div>

View File

@@ -1,24 +1,24 @@
import { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import api from '@/services/api'
import type { Issue, Comment } from '@/types'
import type { Task, Comment } from '@/types'
import dayjs from 'dayjs'
export default function IssueDetailPage() {
export default function TaskDetailPage() {
const { id } = useParams()
const navigate = useNavigate()
const [issue, setIssue] = useState<Issue | null>(null)
const [task, setTask] = useState<Task | null>(null)
const [comments, setComments] = useState<Comment[]>([])
const [newComment, setNewComment] = useState('')
useEffect(() => {
api.get<Issue>(`/issues/${id}`).then(({ data }) => setIssue(data))
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() || !issue) return
await api.post('/comments', { content: newComment, issue_id: issue.id, author_id: 1 })
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)
@@ -26,11 +26,11 @@ export default function IssueDetailPage() {
const transition = async (newStatus: string) => {
await api.post(`/issues/${id}/transition?new_status=${newStatus}`)
const { data } = await api.get<Issue>(`/issues/${id}`)
setIssue(data)
const { data } = await api.get<Task>(`/issues/${id}`)
setTask(data)
}
if (!issue) return <div className="loading">Loading...</div>
if (!task) return <div className="loading">Loading...</div>
const statusActions: Record<string, string[]> = {
open: ['in_progress', 'blocked'],
@@ -41,38 +41,38 @@ export default function IssueDetailPage() {
}
return (
<div className="issue-detail">
<button className="btn-back" onClick={() => navigate('/issues')}> Back</button>
<div className="task-detail">
<button className="btn-back" onClick={() => navigate('/tasks')}> Back</button>
<div className="issue-header">
<h2>#{issue.id} {issue.title}</h2>
<div className="issue-meta">
<span className={`badge status-${issue.status}`}>{issue.status}</span>
<span className={`badge priority-${issue.priority}`}>{issue.priority}</span>
<span className="badge">{issue.issue_type}</span>{issue.issue_subtype && <span className="badge">{issue.issue_subtype}</span>}
{issue.tags && <span className="tags">{issue.tags}</span>}
<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="issue-body">
<div className="task-body">
<div className="section">
<h3>Description</h3>
<p>{issue.description || 'No description'}</p>
<p>{task.description || 'No description'}</p>
</div>
<div className="section">
<h3>Details</h3>
<dl>
<dt>Created</dt><dd>{dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</dd>
{issue.due_date && <><dt>Due date</dt><dd>{dayjs(issue.due_date).format('YYYY-MM-DD')}</dd></>}
{issue.updated_at && <><dt>Updated</dt><dd>{dayjs(issue.updated_at).format('YYYY-MM-DD HH:mm')}</dd></>}
<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[issue.status] || []).map((s) => (
{(statusActions[task.status] || []).map((s) => (
<button key={s} className="btn-transition" onClick={() => transition(s)}>{s}</button>
))}
</div>

View File

@@ -1,10 +1,10 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '@/services/api'
import type { Issue, PaginatedResponse } from '@/types'
import type { Task, PaginatedResponse } from '@/types'
export default function IssuesPage() {
const [issues, setIssues] = useState<Issue[]>([])
export default function TasksPage() {
const [tasks, setTasks] = useState<Task[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
@@ -12,17 +12,17 @@ export default function IssuesPage() {
const [priorityFilter, setPriorityFilter] = useState('')
const navigate = useNavigate()
const fetchIssues = () => {
const fetchTasks = () => {
const params = new URLSearchParams({ page: String(page), page_size: '20' })
if (statusFilter) params.set('issue_status', statusFilter)
api.get<PaginatedResponse<Issue>>(`/issues?${params}`).then(({ data }) => {
setIssues(data.items)
api.get<PaginatedResponse<Task>>(`/issues?${params}`).then(({ data }) => {
setTasks(data.items)
setTotal(data.total)
setTotalPages(data.total_pages)
})
}
useEffect(() => { fetchIssues() }, [page, statusFilter, priorityFilter])
useEffect(() => { fetchTasks() }, [page, statusFilter, priorityFilter])
const statusColors: Record<string, string> = {
open: '#3b82f6', in_progress: '#f59e0b', resolved: '#10b981',
@@ -30,10 +30,10 @@ export default function IssuesPage() {
}
return (
<div className="issues-page">
<div className="tasks-page">
<div className="page-header">
<h2>📋 Issues ({total})</h2>
<button className="btn-primary" onClick={() => navigate('/issues/new')}>+ Create Issue</button>
<h2>📋 Tasks ({total})</h2>
<button className="btn-primary" onClick={() => navigate('/tasks/new')}>+ Create Task</button>
</div>
<div className="filters">
@@ -47,20 +47,20 @@ export default function IssuesPage() {
</select>
</div>
<table className="issues-table">
<table className="tasks-table">
<thead>
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Subtype</th><th>Tags</th><th>Created</th></tr>
</thead>
<tbody>
{issues.map((i) => (
<tr key={i.id} onClick={() => navigate(`/issues/${i.id}`)} className="clickable">
<td>{i.id}</td>
<td className="issue-title">{i.title}</td>
<td><span className="badge" style={{ backgroundColor: statusColors[i.status] || '#ccc' }}>{i.status}</span></td>
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
<td>{i.issue_type}</td><td>{i.issue_subtype || "-"}</td>
<td>{i.tags || '-'}</td>
<td>{new Date(i.created_at).toLocaleDateString()}</td>
{tasks.map((t) => (
<tr key={t.id} onClick={() => navigate(`/tasks/${t.id}`)} className="clickable">
<td>{t.id}</td>
<td className="task-title">{t.title}</td>
<td><span className="badge" style={{ backgroundColor: statusColors[t.status] || '#ccc' }}>{t.status}</span></td>
<td><span className={`badge priority-${t.priority}`}>{t.priority}</span></td>
<td>{t.issue_type}</td><td>{t.issue_subtype || "-"}</td>
<td>{t.tags || '-'}</td>
<td>{new Date(t.created_at).toLocaleDateString()}</td>
</tr>
))}
</tbody>

View File

@@ -29,7 +29,7 @@ export interface ProjectMember {
role: string
}
export interface Issue {
export interface Task {
id: number
title: string
description: string | null
@@ -114,7 +114,7 @@ export interface DashboardStats {
by_status: Record<string, number>
by_priority: Record<string, number>
by_type: Record<string, number>
recent_issues: Issue[]
recent_issues: Task[]
}
export interface LoginResponse {