feat: unify project milestone and task editing with modals

This commit is contained in:
zhi
2026-03-16 18:13:54 +00:00
parent ef42231697
commit 7587554fd8
9 changed files with 571 additions and 239 deletions

View File

@@ -17,13 +17,27 @@ type Props = {
isOpen: boolean
onClose: () => void
onCreated?: (task: Task) => void | Promise<void>
onSaved?: (task: Task) => void | Promise<void>
initialProjectId?: number
initialMilestoneId?: number
lockProject?: boolean
lockMilestone?: boolean
task?: Task | null
}
const makeInitialForm = (projectId = 0, milestoneId = 0) => ({
type FormState = {
title: string
description: string
project_id: number
milestone_id: number
task_type: string
task_subtype: string
priority: string
tags: string
reporter_id: number
}
const makeInitialForm = (projectId = 0, milestoneId = 0): FormState => ({
title: '',
description: '',
project_id: projectId,
@@ -39,16 +53,19 @@ export default function CreateTaskModal({
isOpen,
onClose,
onCreated,
onSaved,
initialProjectId,
initialMilestoneId,
lockProject = false,
lockMilestone = false,
task,
}: Props) {
const [projects, setProjects] = useState<Project[]>([])
const [milestones, setMilestones] = useState<Milestone[]>([])
const [saving, setSaving] = useState(false)
const [form, setForm] = useState(makeInitialForm(initialProjectId, initialMilestoneId))
const [form, setForm] = useState<FormState>(makeInitialForm(initialProjectId, initialMilestoneId))
const isEdit = Boolean(task)
const currentType = useMemo(
() => TASK_TYPES.find((t) => t.value === form.task_type) || TASK_TYPES[2],
[form.task_type]
@@ -76,13 +93,26 @@ export default function CreateTaskModal({
const { data } = await api.get<Project[]>('/projects')
setProjects(data)
const chosenProjectId = initialProjectId || data[0]?.id || 0
setForm(makeInitialForm(chosenProjectId, initialMilestoneId || 0))
await loadMilestones(chosenProjectId, initialMilestoneId)
const chosenProjectId = task?.project_id || initialProjectId || data[0]?.id || 0
const chosenMilestoneId = task?.milestone_id || initialMilestoneId || 0
setForm(task ? {
title: task.title,
description: task.description || '',
project_id: task.project_id,
milestone_id: task.milestone_id || 0,
task_type: task.task_type,
task_subtype: task.task_subtype || '',
priority: task.priority,
tags: task.tags || '',
reporter_id: task.reporter_id,
} : makeInitialForm(chosenProjectId, chosenMilestoneId))
await loadMilestones(chosenProjectId, chosenMilestoneId)
}
init().catch(console.error)
}, [isOpen, initialProjectId, initialMilestoneId])
}, [isOpen, initialProjectId, initialMilestoneId, task])
const handleProjectChange = async (projectId: number) => {
setForm((f) => ({ ...f, project_id: projectId, milestone_id: 0 }))
@@ -105,8 +135,11 @@ export default function CreateTaskModal({
setSaving(true)
try {
const { data } = await api.post<Task>('/tasks', payload)
const { data } = isEdit
? await api.patch<Task>(`/tasks/${task!.id}`, payload)
: await api.post<Task>('/tasks', payload)
await onCreated?.(data)
await onSaved?.(data)
onClose()
} finally {
setSaving(false)
@@ -119,7 +152,7 @@ export default function CreateTaskModal({
<div className="modal-overlay" onClick={onClose}>
<div className="modal task-create-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>Create Task</h3>
<h3>{isEdit ? 'Edit Task' : 'Create Task'}</h3>
<button type="button" className="btn-secondary" onClick={onClose}></button>
</div>
@@ -226,7 +259,7 @@ export default function CreateTaskModal({
<div className="modal-actions">
<button data-testid="create-task-button" type="submit" className="btn-primary" disabled={saving}>
{saving ? 'Creating...' : 'Create'}
{saving ? (isEdit ? 'Saving...' : 'Creating...') : (isEdit ? 'Save' : 'Create')}
</button>
<button type="button" className="btn-secondary" onClick={onClose} disabled={saving}>Cancel</button>
</div>