feat: add task type hierarchy with subtypes in UI
This commit is contained in:
@@ -3,12 +3,25 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import api from '@/services/api'
|
import api from '@/services/api'
|
||||||
import type { Project } from '@/types'
|
import type { Project } from '@/types'
|
||||||
|
|
||||||
|
const ISSUE_TYPES = [
|
||||||
|
{ value: 'story', label: 'Story', subtypes: ['feature', 'improvement', 'refactor'] },
|
||||||
|
{ value: 'issue', label: 'Issue', subtypes: ['infrastructure', 'performance', 'regression', 'security', 'user_experience'] },
|
||||||
|
{ value: 'task', label: 'Task', subtypes: [] },
|
||||||
|
{ value: 'test', label: 'Test', subtypes: ['regression', 'security', 'smoke', 'stress'] },
|
||||||
|
{ value: 'maintenance', label: 'Maintenance', subtypes: ['deploy', 'release'] },
|
||||||
|
{ value: 'research', label: 'Research', subtypes: [] },
|
||||||
|
{ value: 'review', label: 'Review', subtypes: ['code_review', 'decision_review', 'function_review'] },
|
||||||
|
{ value: 'support', label: 'Support', subtypes: ['access', 'information'] },
|
||||||
|
{ value: 'meeting', label: 'Meeting', subtypes: ['conference', 'handover', 'recap'] },
|
||||||
|
{ value: 'resolution', label: 'Resolution', subtypes: [] },
|
||||||
|
]
|
||||||
|
|
||||||
export default function CreateIssuePage() {
|
export default function CreateIssuePage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [projects, setProjects] = useState<Project[]>([])
|
const [projects, setProjects] = useState<Project[]>([])
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
title: '', description: '', project_id: 0, issue_type: 'task',
|
title: '', description: '', project_id: 0, issue_type: 'issue',
|
||||||
priority: 'medium', tags: '', reporter_id: 1,
|
issue_subtype: '', priority: 'medium', tags: '', reporter_id: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -18,9 +31,17 @@ export default function CreateIssuePage() {
|
|||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const currentType = ISSUE_TYPES.find(t => t.value === form.issue_type) || ISSUE_TYPES[1]
|
||||||
|
const subtypes = currentType.subtypes || []
|
||||||
|
|
||||||
|
const handleTypeChange = (newType: string) => {
|
||||||
|
setForm(f => ({ ...f, issue_type: newType, issue_subtype: '' }))
|
||||||
|
}
|
||||||
|
|
||||||
const submit = async (e: React.FormEvent) => {
|
const submit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const payload = { ...form, tags: form.tags || null }
|
const payload: any = { ...form, tags: form.tags || null }
|
||||||
|
if (!form.issue_subtype) delete payload.issue_subtype
|
||||||
await api.post('/issues', payload)
|
await api.post('/issues', payload)
|
||||||
navigate('/issues')
|
navigate('/issues')
|
||||||
}
|
}
|
||||||
@@ -37,13 +58,18 @@ export default function CreateIssuePage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Type
|
<label>Type
|
||||||
<select value={form.issue_type} onChange={(e) => setForm({ ...form, issue_type: e.target.value })}>
|
<select value={form.issue_type} onChange={(e) => handleTypeChange(e.target.value)}>
|
||||||
<option value="task">Task</option>
|
{ISSUE_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||||||
<option value="bug">Bug</option>
|
|
||||||
<option value="feature">Feature</option>
|
|
||||||
<option value="resolution">Resolution</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{subtypes.length > 0 && (
|
||||||
|
<label>Subtype
|
||||||
|
<select value={form.issue_subtype} onChange={(e) => setForm({ ...form, issue_subtype: e.target.value })}>
|
||||||
|
<option value="">Select subtype</option>
|
||||||
|
{subtypes.map((s) => <option key={s} value={s}>{s.replace('_', ' ')}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<label>Priority
|
<label>Priority
|
||||||
<select value={form.priority} onChange={(e) => setForm({ ...form, priority: e.target.value })}>
|
<select value={form.priority} onChange={(e) => setForm({ ...form, priority: e.target.value })}>
|
||||||
<option value="low">Low</option>
|
<option value="low">Low</option>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function DashboardPage() {
|
|||||||
<h3>Recent Issues</h3>
|
<h3>Recent Issues</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th></tr>
|
<tr><th>ID</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Subtype</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{(stats.recent_issues || []).map((i) => (
|
{(stats.recent_issues || []).map((i) => (
|
||||||
@@ -64,7 +64,7 @@ export default function DashboardPage() {
|
|||||||
<td><a href={`/issues/${i.id}`}>{i.title}</a></td>
|
<td><a href={`/issues/${i.id}`}>{i.title}</a></td>
|
||||||
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
|
<td><span className={`badge status-${i.status}`}>{i.status}</span></td>
|
||||||
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
||||||
<td>{i.issue_type}</td>
|
<td>{i.issue_type}</td><td>{i.issue_subtype || "-"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function IssueDetailPage() {
|
|||||||
<div className="issue-meta">
|
<div className="issue-meta">
|
||||||
<span className={`badge status-${issue.status}`}>{issue.status}</span>
|
<span className={`badge status-${issue.status}`}>{issue.status}</span>
|
||||||
<span className={`badge priority-${issue.priority}`}>{issue.priority}</span>
|
<span className={`badge priority-${issue.priority}`}>{issue.priority}</span>
|
||||||
<span className="badge">{issue.issue_type}</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>}
|
{issue.tags && <span className="tags">{issue.tags}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default function IssuesPage() {
|
|||||||
|
|
||||||
<table className="issues-table">
|
<table className="issues-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>#</th><th>Title</th><th>Status</th><th>Priority</th><th>Type</th><th>Tags</th><th>Created</th></tr>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{issues.map((i) => (
|
{issues.map((i) => (
|
||||||
@@ -58,7 +58,7 @@ export default function IssuesPage() {
|
|||||||
<td className="issue-title">{i.title}</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" style={{ backgroundColor: statusColors[i.status] || '#ccc' }}>{i.status}</span></td>
|
||||||
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
<td><span className={`badge priority-${i.priority}`}>{i.priority}</span></td>
|
||||||
<td>{i.issue_type}</td>
|
<td>{i.issue_type}</td><td>{i.issue_subtype || "-"}</td>
|
||||||
<td>{i.tags || '-'}</td>
|
<td>{i.tags || '-'}</td>
|
||||||
<td>{new Date(i.created_at).toLocaleDateString()}</td>
|
<td>{new Date(i.created_at).toLocaleDateString()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ export interface Issue {
|
|||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
description: string | null
|
description: string | null
|
||||||
issue_type: 'task' | 'story' | 'test' | 'resolution'
|
issue_type: 'meeting' | 'support' | 'issue' | 'maintenance' | 'research' | 'review' | 'story' | 'test' | 'resolution' | 'task'
|
||||||
|
issue_subtype: string | null
|
||||||
status: 'open' | 'in_progress' | 'resolved' | 'closed' | 'blocked'
|
status: 'open' | 'in_progress' | 'resolved' | 'closed' | 'blocked'
|
||||||
priority: 'low' | 'medium' | 'high' | 'critical'
|
priority: 'low' | 'medium' | 'high' | 'critical'
|
||||||
project_id: number
|
project_id: number
|
||||||
|
|||||||
Reference in New Issue
Block a user