Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | 69x 69x 69x 69x 69x 69x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x | import { useState, forwardRef, useImperativeHandle, useEffect } from 'react'
import { useCreateProject, useUpdateProject } from '@/features/projects/projectsApi'
export interface CrudFormRef {
submit: () => Promise<void>
}
export interface ProjectFormProps {
id?: string
defaults?: { name?: string; repo_url?: string; description?: string }
onSuccess: (result: { id: string }) => void
onClose: () => void
onDirtyChange?: (dirty: boolean) => void
onPendingChange?: (pending: boolean) => void
}
interface FormState {
name: string
repo_url: string
description: string
}
const NAME_MIN = 3
const NAME_MAX = 100
const DESC_MAX = 5000
export const ProjectForm = forwardRef<CrudFormRef, ProjectFormProps>(function ProjectForm(
{ id, defaults, onSuccess, onClose: _onClose, onDirtyChange, onPendingChange },
ref,
) {
const [form, setForm] = useState<FormState>({
name: defaults?.name ?? '',
repo_url: defaults?.repo_url ?? '',
description: defaults?.description ?? '',
})
const [errors, setErrors] = useState<Record<string, string>>({})
const [submitted, setSubmitted] = useState(false)
const isEdit = !!id
const { mutateAsync: createProject, isPending: isCreatePending } = useCreateProject()
const { mutateAsync: updateProject, isPending: isUpdatePending } = useUpdateProject(id ?? '')
const isPending = isCreatePending || isUpdatePending
const isDirty =
!submitted && (
form.name !== (defaults?.name ?? '') ||
form.repo_url !== (defaults?.repo_url ?? '') ||
form.description !== (defaults?.description ?? '')
)
useEffect(() => { onDirtyChange?.(isDirty) }, [isDirty, onDirtyChange])
useEffect(() => { onPendingChange?.(isPending) }, [isPending, onPendingChange])
function validate(): Record<string, string> {
const errs: Record<string, string> = {}
if (!form.name || form.name.length < NAME_MIN) {
errs.name = `Name must be at least ${NAME_MIN} characters`
}
if (form.name.length > NAME_MAX) {
errs.name = `Name must be at most ${NAME_MAX} characters`
}
if (form.description.length > DESC_MAX) {
errs.description = `Description must be at most ${DESC_MAX} characters`
}
return errs
}
async function handleSubmit() {
if (isPending) return
const errs = validate()
setErrors(errs)
if (Object.keys(errs).length > 0) return
setSubmitted(true)
try {
if (isEdit) {
await updateProject({ name: form.name, repo_url: form.repo_url, description: form.description || null })
onSuccess({ id: id! })
} else {
const result = await createProject({ name: form.name, repo_url: form.repo_url, description: form.description || null })
onSuccess({ id: result.id })
}
} catch {
setSubmitted(false)
}
}
useImperativeHandle(ref, () => ({
submit: handleSubmit,
}))
return (
<fieldset disabled={isPending} className="flex flex-col gap-4">
<label className="flex flex-col gap-1 text-sm font-medium">
Name
<input
className="rounded-md border border-input bg-background px-3 py-2 text-sm"
value={form.name}
onChange={(e) => {
setForm((prev) => ({ ...prev, name: e.target.value }))
setErrors((prev) => ({ ...prev, name: '' }))
}}
placeholder="Project name"
/>
{errors.name && <p className="text-sm text-destructive">{errors.name}</p>}
</label>
<label className="flex flex-col gap-1 text-sm font-medium">
Repo URL
<input
className="rounded-md border border-input bg-background px-3 py-2 text-sm"
value={form.repo_url}
onChange={(e) => {
setForm((prev) => ({ ...prev, repo_url: e.target.value }))
setErrors((prev) => ({ ...prev, repo_url: '' }))
}}
placeholder="git@github.com:org/repo.git"
/>
{errors.repo_url && <p className="text-sm text-destructive">{errors.repo_url}</p>}
</label>
<label className="flex flex-col gap-1 text-sm font-medium">
Description
<textarea
className="rounded-md border border-input bg-background px-3 py-2 text-sm"
value={form.description}
onChange={(e) => {
setForm((prev) => ({ ...prev, description: e.target.value }))
setErrors((prev) => ({ ...prev, description: '' }))
}}
placeholder="Description (optional)"
rows={3}
/>
{errors.description && (
<p className="text-sm text-destructive">{errors.description}</p>
)}
</label>
</fieldset>
)
})
|