feat(P8.1-P8.2): milestone status action buttons + badge styles + started_at display
- Add freeze/start/close action buttons on MilestoneDetailPage - Freeze: visible in open status, calls POST .../actions/freeze - Start: visible in freeze status, calls POST .../actions/start - Close: visible in open/freeze/undergoing, with reason input + confirmation - Display started_at in milestone meta when present - Hide edit button and create-item buttons in terminal states - Add CSS badge styles for freeze (purple), undergoing (amber), completed (green) - All actions show loading state and error feedback
This commit is contained in:
@@ -72,6 +72,9 @@ tr.clickable:hover { background: var(--bg-hover); }
|
||||
.status-resolved { background: #10b981; }
|
||||
.status-closed { background: #6b7280; }
|
||||
.status-blocked { background: #ef4444; }
|
||||
.status-freeze { background: #8b5cf6; }
|
||||
.status-undergoing { background: #f59e0b; }
|
||||
.status-completed { background: #10b981; }
|
||||
.priority-low { background: #6b7280; }
|
||||
.priority-medium { background: #3b82f6; }
|
||||
.priority-high { background: #f59e0b; }
|
||||
|
||||
@@ -41,6 +41,10 @@ export default function MilestoneDetailPage() {
|
||||
const [newTitle, setNewTitle] = useState('')
|
||||
const [newDesc, setNewDesc] = useState('')
|
||||
const [projectCode, setProjectCode] = useState('')
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
const [actionError, setActionError] = useState<string | null>(null)
|
||||
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
|
||||
const [closeReason, setCloseReason] = useState('')
|
||||
|
||||
const fetchMilestone = () => {
|
||||
api.get<Milestone>(`/milestones/${id}`).then(({ data }) => {
|
||||
@@ -96,7 +100,34 @@ export default function MilestoneDetailPage() {
|
||||
currentMemberRole === 'admin'
|
||||
))
|
||||
|
||||
const isUndergoing = milestone?.status === 'undergoing'
|
||||
const msStatus = milestone?.status
|
||||
const isUndergoing = msStatus === 'undergoing'
|
||||
const isTerminal = msStatus === 'completed' || msStatus === 'closed'
|
||||
|
||||
// --- Milestone action handlers (P8.2) ---
|
||||
const performAction = async (action: string, body?: Record<string, unknown>) => {
|
||||
if (!milestone || !project) return
|
||||
setActionLoading(action)
|
||||
setActionError(null)
|
||||
try {
|
||||
await api.post(`/projects/${project.id}/milestones/${milestone.id}/actions/${action}`, body ?? {})
|
||||
fetchMilestone()
|
||||
refreshMilestoneItems()
|
||||
} catch (err: any) {
|
||||
const detail = err?.response?.data?.detail
|
||||
setActionError(typeof detail === 'string' ? detail : `${action} failed`)
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFreeze = () => performAction('freeze')
|
||||
const handleStart = () => performAction('start')
|
||||
const handleClose = () => {
|
||||
performAction('close', closeReason ? { reason: closeReason } : {})
|
||||
setShowCloseConfirm(false)
|
||||
setCloseReason('')
|
||||
}
|
||||
|
||||
if (!milestone) return <div className="loading">Loading...</div>
|
||||
|
||||
@@ -120,8 +151,65 @@ export default function MilestoneDetailPage() {
|
||||
<span className={`badge status-${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>}
|
||||
{milestone.started_at && <span className="text-dim">Started: {dayjs(milestone.started_at).format('YYYY-MM-DD HH:mm')}</span>}
|
||||
</div>
|
||||
{canEditMilestone && <button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setShowEditMilestone(true)}>Edit Milestone</button>}
|
||||
{canEditMilestone && !isTerminal && <button className="btn-transition" style={{ marginTop: 8 }} onClick={() => setShowEditMilestone(true)}>Edit Milestone</button>}
|
||||
|
||||
{/* Milestone status action buttons (P8.2) */}
|
||||
{!isTerminal && (
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{msStatus === 'open' && (
|
||||
<button
|
||||
className="btn-primary"
|
||||
disabled={actionLoading === 'freeze'}
|
||||
onClick={handleFreeze}
|
||||
>
|
||||
{actionLoading === 'freeze' ? '⏳ Freezing...' : '🧊 Freeze'}
|
||||
</button>
|
||||
)}
|
||||
{msStatus === 'freeze' && (
|
||||
<button
|
||||
className="btn-primary"
|
||||
disabled={actionLoading === 'start'}
|
||||
onClick={handleStart}
|
||||
>
|
||||
{actionLoading === 'start' ? '⏳ Starting...' : '▶️ Start'}
|
||||
</button>
|
||||
)}
|
||||
{(msStatus === 'open' || msStatus === 'freeze' || msStatus === 'undergoing') && (
|
||||
<>
|
||||
{!showCloseConfirm ? (
|
||||
<button
|
||||
className="btn-transition"
|
||||
style={{ color: 'var(--color-danger, #e74c3c)' }}
|
||||
onClick={() => setShowCloseConfirm(true)}
|
||||
>
|
||||
✖ Close
|
||||
</button>
|
||||
) : (
|
||||
<div className="card" style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '8px 12px' }}>
|
||||
<input
|
||||
placeholder="Reason (optional)"
|
||||
value={closeReason}
|
||||
onChange={(e) => setCloseReason(e.target.value)}
|
||||
style={{ minWidth: 180 }}
|
||||
/>
|
||||
<button
|
||||
className="btn-primary"
|
||||
style={{ backgroundColor: 'var(--color-danger, #e74c3c)' }}
|
||||
disabled={actionLoading === 'close'}
|
||||
onClick={handleClose}
|
||||
>
|
||||
{actionLoading === 'close' ? 'Closing...' : 'Confirm Close'}
|
||||
</button>
|
||||
<button className="btn-back" onClick={() => { setShowCloseConfirm(false); setCloseReason('') }}>Cancel</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{actionError && <p style={{ color: 'var(--color-danger, #e74c3c)', marginTop: 4 }}>⚠️ {actionError}</p>}
|
||||
</div>
|
||||
|
||||
{milestone.description && (
|
||||
@@ -154,14 +242,14 @@ export default function MilestoneDetailPage() {
|
||||
|
||||
<div className="section">
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
{!isUndergoing && canEditMilestone && (
|
||||
{!isTerminal && !isUndergoing && canEditMilestone && (
|
||||
<>
|
||||
<button className="btn-primary" onClick={() => { setActiveTab('tasks'); setShowCreateTask(true) }}>+ Create Task</button>
|
||||
<button className="btn-primary" onClick={() => { setActiveTab('supports'); setShowCreateSupport(true) }}>+ Create Support</button>
|
||||
<button className="btn-primary" onClick={() => { setActiveTab('meetings'); setShowCreateMeeting(true) }}>+ Schedule Meeting</button>
|
||||
</>
|
||||
)}
|
||||
{isUndergoing && <span className="text-dim">Milestone is undergoing - cannot add new items</span>}
|
||||
{(isUndergoing || isTerminal) && <span className="text-dim">{isTerminal ? `Milestone is ${msStatus}` : 'Milestone is undergoing'} — cannot add new items</span>}
|
||||
</div>
|
||||
|
||||
<MilestoneFormModal
|
||||
|
||||
Reference in New Issue
Block a user