155 lines
5.0 KiB
TypeScript
155 lines
5.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import api from '@/services/api'
|
|
import type { Milestone, Project } from '@/types'
|
|
|
|
type Props = {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
onSaved?: (milestone: Milestone) => void | Promise<void>
|
|
milestone?: Milestone | null
|
|
initialProjectId?: number
|
|
lockProject?: boolean
|
|
}
|
|
|
|
type FormState = {
|
|
title: string
|
|
description: string
|
|
project_id: number
|
|
status: string
|
|
due_date: string
|
|
planned_release_date: string
|
|
}
|
|
|
|
const emptyForm: FormState = {
|
|
title: '',
|
|
description: '',
|
|
project_id: 0,
|
|
status: 'open',
|
|
due_date: '',
|
|
planned_release_date: '',
|
|
}
|
|
|
|
const toDateInput = (value?: string | null) => (value ? String(value).slice(0, 10) : '')
|
|
|
|
export default function MilestoneFormModal({ isOpen, onClose, onSaved, milestone, initialProjectId, lockProject = false }: Props) {
|
|
const [projects, setProjects] = useState<Project[]>([])
|
|
const [saving, setSaving] = useState(false)
|
|
const [form, setForm] = useState<FormState>(emptyForm)
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
|
|
const init = async () => {
|
|
const { data } = await api.get<Project[]>('/projects')
|
|
setProjects(data)
|
|
const defaultProjectId = milestone?.project_id || initialProjectId || data[0]?.id || 0
|
|
setForm({
|
|
title: milestone?.title || '',
|
|
description: milestone?.description || '',
|
|
project_id: defaultProjectId,
|
|
status: milestone?.status || 'open',
|
|
due_date: toDateInput(milestone?.due_date),
|
|
planned_release_date: toDateInput(milestone?.planned_release_date),
|
|
})
|
|
}
|
|
|
|
init().catch(console.error)
|
|
}, [isOpen, milestone, initialProjectId])
|
|
|
|
const submit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setSaving(true)
|
|
try {
|
|
const payload = {
|
|
title: form.title,
|
|
description: form.description || null,
|
|
project_id: form.project_id,
|
|
status: form.status,
|
|
due_date: form.due_date || null,
|
|
planned_release_date: form.planned_release_date || null,
|
|
}
|
|
if (milestone) {
|
|
const { data } = await api.patch<Milestone>(`/milestones/${milestone.id}`, payload)
|
|
await onSaved?.(data)
|
|
} else {
|
|
const { data } = await api.post<Milestone>('/milestones', payload)
|
|
await onSaved?.(data)
|
|
}
|
|
onClose()
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
if (!isOpen) return null
|
|
|
|
return (
|
|
<div className="modal-overlay" onClick={onClose}>
|
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
<div className="modal-header">
|
|
<h3>{milestone ? 'Edit Milestone' : 'Create Milestone'}</h3>
|
|
<button type="button" className="btn-secondary" onClick={onClose}>✕</button>
|
|
</div>
|
|
|
|
<form className="task-create-form" onSubmit={submit}>
|
|
<label>
|
|
Title
|
|
<input
|
|
data-testid="milestone-title-input"
|
|
required
|
|
placeholder="Milestone title"
|
|
value={form.title}
|
|
onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}
|
|
/>
|
|
</label>
|
|
|
|
<label>
|
|
Description
|
|
<textarea
|
|
data-testid="milestone-description-input"
|
|
placeholder="Description (optional)"
|
|
value={form.description}
|
|
onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))}
|
|
/>
|
|
</label>
|
|
|
|
<div className="task-create-grid">
|
|
<label>
|
|
Project
|
|
<select value={form.project_id} onChange={(e) => setForm((f) => ({ ...f, project_id: Number(e.target.value) }))} disabled={lockProject}>
|
|
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
Status
|
|
<select value={form.status} onChange={(e) => setForm((f) => ({ ...f, status: e.target.value }))}>
|
|
<option value="open">Open</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="deferred">Deferred</option>
|
|
<option value="progressing">Progressing</option>
|
|
<option value="closed">Closed</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label>
|
|
Due date
|
|
<input type="date" value={form.due_date} onChange={(e) => setForm((f) => ({ ...f, due_date: e.target.value }))} />
|
|
</label>
|
|
|
|
<label>
|
|
Planned release date
|
|
<input type="date" value={form.planned_release_date} onChange={(e) => setForm((f) => ({ ...f, planned_release_date: e.target.value }))} />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="modal-actions">
|
|
<button type="submit" className="btn-primary" disabled={saving}>{saving ? 'Saving...' : (milestone ? 'Save' : 'Create')}</button>
|
|
<button type="button" className="btn-secondary" onClick={onClose} disabled={saving}>Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|