22
33import { forwardRef , useEffect , useImperativeHandle } from "react" ;
44import { zodResolver } from "@hookform/resolvers/zod" ;
5- import { FormProvider , useForm , useFormContext , useWatch } from "react-hook-form" ;
5+ import { FormProvider , useForm , useFormContext , useWatch , type Resolver } from "react-hook-form" ;
66import { Label } from "@/components/ui/label" ;
77import { Textarea } from "@/components/ui/textarea" ;
88import {
@@ -25,50 +25,50 @@ export interface TemplateDetailsFormProps {
2525 slug : string ;
2626}
2727
28- export const TemplateDetailsForm = forwardRef <
29- TemplateDetailsFormHandle ,
30- TemplateDetailsFormProps
31- > ( function TemplateDetailsForm ( { slug } , ref ) {
32- const schema = getTemplateSchema ( slug ) ;
33- const parent = useFormContext < ProposalFormValues > ( ) ;
28+ export const TemplateDetailsForm = forwardRef < TemplateDetailsFormHandle , TemplateDetailsFormProps > (
29+ function TemplateDetailsForm ( { slug } , ref ) {
30+ const schema = getTemplateSchema ( slug ) ;
31+ const parent = useFormContext < ProposalFormValues > ( ) ;
3432
35- const initialValues = ( parent . getValues ( "templateFields" ) as TemplateValues | undefined ) ?? { } ;
36- const templateForm = useForm < TemplateValues > ( {
37- resolver : zodResolver ( buildTemplateValidator ( slug ) ) ,
38- defaultValues : buildDefaults ( slug , initialValues ) ,
39- mode : "onChange" ,
40- } ) ;
33+ const initialValues = ( parent . getValues ( "templateFields" ) as TemplateValues | undefined ) ?? { } ;
34+ // zodResolver's type expects a schema with specific input generics; our builder
35+ // is slug-indexed so we bridge via a runtime-safe cast.
36+ const validator = buildTemplateValidator ( slug ) as unknown as Parameters < typeof zodResolver > [ 0 ] ;
37+ const templateForm = useForm < TemplateValues > ( {
38+ resolver : zodResolver ( validator ) as unknown as Resolver < TemplateValues > ,
39+ defaultValues : buildDefaults ( slug , initialValues ) ,
40+ mode : "onChange" ,
41+ } ) ;
4142
42- const watchedValues = useWatch ( { control : templateForm . control } ) as TemplateValues ;
43+ const watchedValues = useWatch ( { control : templateForm . control } ) as TemplateValues ;
4344
44- useEffect ( ( ) => {
45- if ( ! schema ) return ;
46- parent . setValue ( "templateFields" , watchedValues , { shouldDirty : true } ) ;
47- const compiled = compileTemplate ( slug , watchedValues ) ;
48- parent . setValue ( "description" , compiled , { shouldDirty : true , shouldValidate : false } ) ;
49- } , [ watchedValues , slug , schema , parent ] ) ;
45+ useEffect ( ( ) => {
46+ if ( ! schema ) return ;
47+ parent . setValue ( "templateFields" , watchedValues , { shouldDirty : true } ) ;
48+ const compiled = compileTemplate ( slug , watchedValues ) ;
49+ parent . setValue ( "description" , compiled , { shouldDirty : true , shouldValidate : false } ) ;
50+ } , [ watchedValues , slug , schema , parent ] ) ;
5051
51- useImperativeHandle (
52- ref ,
53- ( ) => ( {
54- validate : async ( ) => {
55- const ok = await templateForm . trigger ( ) ;
56- if ( ! ok ) {
57- const firstError = Object . keys ( templateForm . formState . errors ) [ 0 ] ;
58- if ( firstError ) {
59- templateForm . setFocus ( firstError as keyof TemplateValues ) ;
52+ useImperativeHandle (
53+ ref ,
54+ ( ) => ( {
55+ validate : async ( ) => {
56+ const ok = await templateForm . trigger ( ) ;
57+ if ( ! ok ) {
58+ const firstError = Object . keys ( templateForm . formState . errors ) [ 0 ] ;
59+ if ( firstError ) {
60+ templateForm . setFocus ( firstError as keyof TemplateValues ) ;
61+ }
6062 }
61- }
62- return ok ;
63- } ,
64- } ) ,
65- [ templateForm ] ,
66- ) ;
63+ return ok ;
64+ } ,
65+ } ) ,
66+ [ templateForm ] ,
67+ ) ;
6768
68- if ( ! schema ) return null ;
69+ if ( ! schema ) return null ;
6970
70- return (
71- < FormProvider { ...templateForm } >
71+ return (
7272 < div className = "space-y-6" >
7373 < div >
7474 < h2 className = "text-2xl font-bold mb-1" > { schema . title } </ h2 >
@@ -77,61 +77,64 @@ export const TemplateDetailsForm = forwardRef<
7777 </ p >
7878 </ div >
7979
80+ { /* Header uses the parent form (title + banner live on ProposalFormValues). */ }
8081 < ProposalDetailsHeader />
8182
82- < div className = "space-y-5" >
83- { schema . fields . map ( ( field ) => {
84- const error = templateForm . formState . errors [ field . id ] as
85- | { message ?: string }
86- | undefined ;
87- return (
88- < div key = { field . id } >
89- < Label htmlFor = { field . id } >
90- { field . label }
91- { field . required ? " *" : "" }
92- </ Label >
93- < p className = "text-xs text-muted-foreground mt-1 mb-2" > { field . helper } </ p >
94- { field . type === "textarea" ? (
95- < Textarea
96- id = { field . id }
97- rows = { field . rows ?? 4 }
98- className = "resize-y"
99- { ...templateForm . register ( field . id ) }
100- />
101- ) : (
102- < BudgetRepeater
103- name = { field . id }
104- topLevelError = { error ?. message }
105- getRowError = { ( index ) => {
106- const arrErrors = templateForm . formState . errors [ field . id ] as
107- | Record <
108- number ,
109- {
110- label ?: { message ?: string } ;
111- amount ?: { message ?: string } ;
112- }
113- >
114- | undefined ;
115- const rowErr = arrErrors ?. [ index ] ;
116- if ( ! rowErr ) return undefined ;
117- return {
118- label : rowErr . label ?. message ,
119- amount : rowErr . amount ?. message ,
120- } ;
121- } }
122- />
123- ) }
124- { field . type === "textarea" && error ?. message ? (
125- < p className = "text-xs text-red-500 mt-1" > { error . message } </ p >
126- ) : null }
127- </ div >
128- ) ;
129- } ) }
130- </ div >
83+ < FormProvider { ...templateForm } >
84+ < div className = "space-y-5" >
85+ { schema . fields . map ( ( field ) => {
86+ const error = templateForm . formState . errors [ field . id ] as
87+ | { message ?: string }
88+ | undefined ;
89+ return (
90+ < div key = { field . id } >
91+ < Label htmlFor = { field . id } >
92+ { field . label }
93+ { field . required ? " *" : "" }
94+ </ Label >
95+ < p className = "text-xs text-muted-foreground mt-1 mb-2" > { field . helper } </ p >
96+ { field . type === "textarea" ? (
97+ < Textarea
98+ id = { field . id }
99+ rows = { field . rows ?? 4 }
100+ className = "resize-y"
101+ { ...templateForm . register ( field . id ) }
102+ />
103+ ) : (
104+ < BudgetRepeater
105+ name = { field . id }
106+ topLevelError = { error ?. message }
107+ getRowError = { ( index ) => {
108+ const arrErrors = templateForm . formState . errors [ field . id ] as
109+ | Record <
110+ number ,
111+ {
112+ label ?: { message ?: string } ;
113+ amount ?: { message ?: string } ;
114+ }
115+ >
116+ | undefined ;
117+ const rowErr = arrErrors ?. [ index ] ;
118+ if ( ! rowErr ) return undefined ;
119+ return {
120+ label : rowErr . label ?. message ,
121+ amount : rowErr . amount ?. message ,
122+ } ;
123+ } }
124+ />
125+ ) }
126+ { field . type === "textarea" && error ?. message ? (
127+ < p className = "text-xs text-red-500 mt-1" > { error . message } </ p >
128+ ) : null }
129+ </ div >
130+ ) ;
131+ } ) }
132+ </ div >
133+ </ FormProvider >
131134 </ div >
132- </ FormProvider >
133- ) ;
134- } ) ;
135+ ) ;
136+ } ,
137+ ) ;
135138
136139function buildDefaults ( slug : string , existing : TemplateValues ) : TemplateValues {
137140 const schema = getTemplateSchema ( slug ) ;
0 commit comments