diff --git a/src/app/projects/[projectId]/settings/components/ProjectDetailsForm.tsx b/src/app/projects/[projectId]/settings/components/ProjectDetailsForm.tsx new file mode 100644 index 00000000..f503ed16 --- /dev/null +++ b/src/app/projects/[projectId]/settings/components/ProjectDetailsForm.tsx @@ -0,0 +1,279 @@ +"use client"; + +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { + projectDetailsToFormValues, + updateProjectSchema, + type ProjectDetails, + type UpdateProjectFormValues, +} from "@/lib/projects/updateProject"; +import type { SelectOption } from "@/lib/projects/createProject"; +import { DateInput } from "@/app/components/DateInput"; +import { Button } from "@/app/shadcn_components/ui/button"; +import { Checkbox } from "@/app/shadcn_components/ui/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/app/shadcn_components/ui/form"; +import { Input } from "@/app/shadcn_components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/app/shadcn_components/ui/select"; + +/** + * The HTTP call, kept outside the component so the component contains no + * `fetch` — TanStack Query owns the request lifecycle. Route handlers answer + * errors as `{ error: string }`, so a failure surfaces the server's own message. + */ +async function updateProject( + projectId: string, + values: UpdateProjectFormValues, +): Promise<{ id: string }> { + const response = await fetch(`/api/projects/${projectId}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(values), + }); + + const body = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(body?.error ?? "Could not save the project details"); + } + return body; +} + +export interface ProjectDetailsFormProps { + details: ProjectDetails; + projectTypes: SelectOption[]; +} + +/** + * Edit a project's details from the settings hub (issue #444). + * + * The same six fields the create-project modal collects, minus the owning + * organisation — re-homing a project would change who can see it, so it is + * shown read-only rather than offered as an edit (see `updateProjectSchema`). + * The resolver is `updateProjectSchema`, which is derived from + * `createProjectSchema`, so the two forms validate identically. + * + * On success the form resets to the values it just saved — which clears the + * dirty state and makes the Save button correctly go quiet — and + * `router.refresh()` re-renders the server component above it, so the heading + * and the readiness panel reflect the edit without a full reload. + */ +export function ProjectDetailsForm({ + details, + projectTypes, +}: ProjectDetailsFormProps) { + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver(updateProjectSchema), + defaultValues: projectDetailsToFormValues(details), + }); + + const mutation = useMutation( + (values: UpdateProjectFormValues) => updateProject(details.id, values), + { + onSuccess: (_data, values) => { + form.reset(values); + router.refresh(); + }, + }, + ); + + return ( +
+

Project details

+

+ What this programme of works is called, who it is for, and when it runs. +

+ +
+ mutation.mutate(values))} + className="mt-4 space-y-4 rounded-xl border border-gray-200 bg-white p-6" + data-testid="project-details-form" + > + ( + + Project name + + + + + + )} + /> + + + Client +

+ {details.organisationName ?? "—"} +

+ + A project cannot be moved to another organisation — that would + change who can see it. + +
+ + ( + + Project type + + + + )} + /> + + ( + + Description + +