Reviewed-on: #9 Co-authored-by: Drew Galbraith <drew@tiramisu.one> Co-committed-by: Drew Galbraith <drew@tiramisu.one>
231 lines
5.4 KiB
TypeScript
231 lines
5.4 KiB
TypeScript
import { useState } from 'react'
|
|
import {
|
|
Box,
|
|
TextField,
|
|
Button,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
Paper,
|
|
Typography,
|
|
Alert,
|
|
} from '@mui/material'
|
|
import { useNavigate } from 'react-router'
|
|
import type { CreateTaskRequest, Task } from '~/types/task'
|
|
import { TaskStatus } from '~/types/task'
|
|
import { useTasks } from '~/hooks/useTasks'
|
|
|
|
interface TaskFormProps {
|
|
task?: Task
|
|
onSuccess?: (task: Task) => void
|
|
onCancel?: () => void
|
|
inline?: boolean
|
|
}
|
|
|
|
interface FormData {
|
|
title: string
|
|
description: string
|
|
status: TaskStatus
|
|
}
|
|
|
|
interface FormErrors {
|
|
title?: string
|
|
description?: string
|
|
}
|
|
|
|
export function TaskForm({
|
|
task,
|
|
onSuccess,
|
|
onCancel,
|
|
inline = false,
|
|
}: TaskFormProps) {
|
|
const navigate = useNavigate()
|
|
const { createTask, loading, error } = useTasks({ autoFetch: false })
|
|
|
|
const [formData, setFormData] = useState<FormData>({
|
|
title: task?.title || '',
|
|
description: task?.description || '',
|
|
status: task?.status || TaskStatus.Todo,
|
|
})
|
|
|
|
const [errors, setErrors] = useState<FormErrors>({})
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
|
|
const validateForm = (): boolean => {
|
|
const newErrors: FormErrors = {}
|
|
|
|
if (!formData.title.trim()) {
|
|
newErrors.title = 'Title is required'
|
|
} else if (formData.title.trim().length < 3) {
|
|
newErrors.title = 'Title must be at least 3 characters long'
|
|
} else if (formData.title.trim().length > 200) {
|
|
newErrors.title = 'Title must be less than 200 characters'
|
|
}
|
|
|
|
if (formData.description.length > 1000) {
|
|
newErrors.description = 'Description must be less than 1000 characters'
|
|
}
|
|
|
|
setErrors(newErrors)
|
|
return Object.keys(newErrors).length === 0
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (!validateForm()) {
|
|
return
|
|
}
|
|
|
|
setIsSubmitting(true)
|
|
|
|
try {
|
|
if (task) {
|
|
// Update existing task - for now we only support create
|
|
// This would use updateTask hook when implemented
|
|
console.log('Update not implemented yet')
|
|
} else {
|
|
// Create new task
|
|
const createData: CreateTaskRequest = {
|
|
title: formData.title.trim(),
|
|
description: formData.description.trim() || undefined,
|
|
}
|
|
|
|
const newTask = await createTask(createData)
|
|
|
|
if (newTask) {
|
|
if (onSuccess) {
|
|
onSuccess(newTask)
|
|
} else {
|
|
navigate('/')
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Form submission error:', err)
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
if (onCancel) {
|
|
onCancel()
|
|
} else {
|
|
navigate('/')
|
|
}
|
|
}
|
|
|
|
const handleInputChange =
|
|
(field: keyof FormData) =>
|
|
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[field]: e.target.value,
|
|
}))
|
|
|
|
// Clear error for this field when user starts typing
|
|
if (errors[field as keyof FormErrors]) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
[field]: undefined,
|
|
}))
|
|
}
|
|
}
|
|
|
|
const handleStatusChange = (
|
|
e: React.ChangeEvent<{ name?: string; value: unknown }>
|
|
) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
status: e.target.value as TaskStatus,
|
|
}))
|
|
}
|
|
|
|
const content = (
|
|
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}>
|
|
<Typography variant="h4" component="h1" gutterBottom>
|
|
{task ? 'Edit Task' : 'Create New Task'}
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Title"
|
|
value={formData.title}
|
|
onChange={handleInputChange('title')}
|
|
error={!!errors.title}
|
|
helperText={errors.title}
|
|
margin="normal"
|
|
required
|
|
autoFocus
|
|
disabled={isSubmitting || loading}
|
|
/>
|
|
|
|
<TextField
|
|
fullWidth
|
|
label="Description"
|
|
value={formData.description}
|
|
onChange={handleInputChange('description')}
|
|
error={!!errors.description}
|
|
helperText={errors.description}
|
|
margin="normal"
|
|
multiline
|
|
rows={4}
|
|
disabled={isSubmitting || loading}
|
|
/>
|
|
|
|
{task && (
|
|
<FormControl fullWidth margin="normal">
|
|
<InputLabel>Status</InputLabel>
|
|
<Select
|
|
value={formData.status}
|
|
onChange={handleStatusChange}
|
|
label="Status"
|
|
disabled={isSubmitting || loading}
|
|
>
|
|
<MenuItem value={TaskStatus.Todo}>Todo</MenuItem>
|
|
<MenuItem value={TaskStatus.Done}>Done</MenuItem>
|
|
<MenuItem value={TaskStatus.Backlog}>Backlog</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
)}
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, mt: 3 }}>
|
|
<Button
|
|
type="submit"
|
|
variant="contained"
|
|
disabled={isSubmitting || loading}
|
|
sx={{ minWidth: 120 }}
|
|
>
|
|
{isSubmitting ? 'Saving...' : task ? 'Update' : 'Create'}
|
|
</Button>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outlined"
|
|
onClick={handleCancel}
|
|
disabled={isSubmitting || loading}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</Box>
|
|
</Box>
|
|
)
|
|
|
|
if (inline) {
|
|
return content
|
|
}
|
|
|
|
return (
|
|
<Paper elevation={2} sx={{ p: 3, maxWidth: 600, mx: 'auto' }}>
|
|
{content}
|
|
</Paper>
|
|
)
|
|
}
|