@@ -24,12 +24,6 @@ import {
2424 FormMessage ,
2525} from "@comp/ui/form" ;
2626import { Input } from "@comp/ui/input" ;
27- import {
28- Popover ,
29- PopoverContent ,
30- PopoverPrimitive ,
31- PopoverTrigger ,
32- } from "@comp/ui/popover" ;
3327import {
3428 Select ,
3529 SelectContent ,
@@ -97,14 +91,13 @@ export function CreateVendorForm({
9791 } ,
9892 } ) ;
9993
100- // Debounced search function using the custom hook
10194 const debouncedSearch = useDebouncedCallback ( ( query : string ) => {
102- if ( query . trim ( ) . length > 1 ) { // Only search if query is longer than 1 char
95+ if ( query . trim ( ) . length > 1 ) {
10396 searchVendors . execute ( { name : query } ) ;
10497 } else {
105- setSearchResults ( [ ] ) ; // Clear results if query is too short
98+ setSearchResults ( [ ] ) ;
10699 }
107- } , 300 ) ; // 300ms debounce delay
100+ } , 300 ) ;
108101
109102 const form = useForm < z . infer < typeof createVendorSchema > > ( {
110103 resolver : zodResolver ( createVendorSchema ) ,
@@ -115,6 +108,7 @@ export function CreateVendorForm({
115108 category : VendorCategory . cloud ,
116109 status : VendorStatus . not_assessed ,
117110 } ,
111+ mode : "onChange" ,
118112 } ) ;
119113
120114 const onSubmit = async ( data : z . infer < typeof createVendorSchema > ) => {
@@ -131,9 +125,9 @@ export function CreateVendorForm({
131125 form . setValue ( "name" , vendor . company_name ?? vendor . legal_name ?? "" ) ;
132126 form . setValue ( "website" , vendor . website ?? "" ) ;
133127 form . setValue ( "description" , vendor . company_description ?? "" ) ;
134- setSearchQuery ( vendor . company_name ?? vendor . legal_name ?? "" ) ; // Update search query display
135- setSearchResults ( [ ] ) ; // Clear results
136- setPopoverOpen ( false ) ; // Close popover
128+ setSearchQuery ( vendor . company_name ?? vendor . legal_name ?? "" ) ;
129+ setSearchResults ( [ ] ) ;
130+ setPopoverOpen ( false ) ;
137131 } ;
138132
139133 return (
@@ -152,82 +146,79 @@ export function CreateVendorForm({
152146 control = { form . control }
153147 name = "name"
154148 render = { ( { field } ) => (
155- < FormItem className = "flex flex-col" >
149+ < FormItem className = "flex flex-col relative " >
156150 < FormLabel >
157151 { t (
158152 "vendors.form.vendor_name" ,
159153 ) }
160154 </ FormLabel >
161- < Popover open = { popoverOpen } onOpenChange = { setPopoverOpen } >
162- < PopoverTrigger asChild >
163- < FormControl >
164- < Button
165- variant = "outline"
166- role = "combobox"
167- aria-expanded = { popoverOpen }
168- className = { cn ( "w-full justify-between" , ! field . value && "text-muted-foreground" ) }
169- >
170- { field . value || t ( "vendors.form.vendor_name_placeholder" ) }
171- < ChevronsUpDown className = "ml-2 h-4 w-4 shrink-0 opacity-50" />
172- </ Button >
173- </ FormControl >
174- </ PopoverTrigger >
175- < PopoverContent className = "w-[--radix-popover-trigger-width] p-0" >
176- < Command shouldFilter = { false } > { /* Disable default filtering */ }
177- < CommandInput
178- placeholder = { t ( "vendors.form.search_vendor_placeholder" ) } // Add a new translation key
179- value = { searchQuery }
180- onValueChange = { ( value ) => {
181- setSearchQuery ( value ) ;
182- // Also update the form field in real-time if user types without selecting
183- // This allows creating a custom vendor
184- field . onChange ( value ) ;
185- // Trigger debounced search
186- debouncedSearch ( value ) ;
187- } }
188- autoFocus
189- />
190- < CommandList >
191- < CommandEmpty >
192- { isSearching ? t ( "common.loading" ) : t ( "vendors.form.no_vendor_found" ) } { /* Add new translation keys */ }
193- </ CommandEmpty >
194- < CommandGroup heading = { t ( "vendors.form.suggestions" ) } > { /* Add new translation key */ }
195- { searchResults . map ( ( vendor ) => (
196- < CommandItem
197- key = { vendor . website }
198- value = { vendor . company_name ?? vendor . website } // Use a unique value for CommandItem
199- onSelect = { ( ) => handleSelectVendor ( vendor ) }
200- className = "cursor-pointer"
201- >
202- { /* Check icon can be used if needed, but maybe confusing here */ }
203- { /* <Check
204- className={cn(
205- "mr-2 h-4 w-4",
206- (form.getValues("name") === vendor.company_name || form.getValues("name") === vendor.legal_name) ? "opacity-100" : "opacity-0",
207- )}
208- /> */ }
209- { vendor . company_name ?? vendor . legal_name ?? vendor . website }
210- </ CommandItem >
211- ) ) }
212- { /* Option to explicitly create the custom vendor typed */ }
213- { searchQuery && ! isSearching && searchResults . length === 0 && (
214- < CommandItem
215- key = "custom"
216- value = { searchQuery }
217- onSelect = { ( ) => {
218- field . onChange ( searchQuery ) ; // Ensure form field has the typed value
155+ < FormControl >
156+ < div className = "relative" >
157+ < Input
158+ placeholder = { t ( "vendors.form.search_vendor_placeholder" ) }
159+ value = { searchQuery }
160+ onChange = { ( e ) => {
161+ const val = e . target . value ;
162+ setSearchQuery ( val ) ;
163+ field . onChange ( val ) ;
164+ debouncedSearch ( val ) ;
165+ if ( val . trim ( ) . length > 1 ) {
166+ setPopoverOpen ( true ) ;
167+ } else {
168+ setPopoverOpen ( false ) ;
169+ setSearchResults ( [ ] ) ;
170+ }
171+ } }
172+ onBlur = { ( ) => {
173+ setTimeout ( ( ) => setPopoverOpen ( false ) , 150 ) ;
174+ } }
175+ onFocus = { ( ) => {
176+ if ( searchQuery . trim ( ) . length > 1 && ( isSearching || searchResults . length > 0 || ( ! isSearching && searchResults . length === 0 ) ) ) {
177+ setPopoverOpen ( true ) ;
178+ }
179+ } }
180+ autoFocus
181+ />
182+ { popoverOpen && (
183+ < div className = "absolute top-full z-10 w-full mt-1 bg-background border rounded-md shadow-lg" >
184+ < div className = "max-h-[300px] overflow-y-auto p-1" >
185+ { isSearching && (
186+ < div className = "p-2 text-sm text-muted-foreground" > { t ( "common.loading" ) } ...</ div >
187+ ) }
188+ { ! isSearching && searchResults . length > 0 && (
189+ < >
190+ < p className = "px-2 py-1.5 text-xs font-medium text-muted-foreground" > { t ( "vendors.form.suggestions" ) } </ p >
191+ { searchResults . map ( ( vendor ) => (
192+ < div
193+ key = { vendor . website ?? vendor . company_name ?? vendor . legal_name ?? Math . random ( ) . toString ( ) }
194+ className = "cursor-pointer p-2 hover:bg-accent rounded-sm text-sm"
195+ onMouseDown = { ( ) => {
196+ handleSelectVendor ( vendor ) ;
197+ setPopoverOpen ( false ) ;
198+ } }
199+ >
200+ { vendor . company_name ?? vendor . legal_name ?? vendor . website }
201+ </ div >
202+ ) ) }
203+ </ >
204+ ) }
205+ { ! isSearching && searchQuery . trim ( ) . length > 1 && searchResults . length === 0 && (
206+ < div
207+ className = "cursor-pointer p-2 hover:bg-accent rounded-sm text-sm italic"
208+ onMouseDown = { ( ) => {
209+ field . onChange ( searchQuery ) ;
210+ setSearchResults ( [ ] ) ;
219211 setPopoverOpen ( false ) ;
220212 } }
221- className = "cursor-pointer italic"
222213 >
223- { t ( "vendors.form.create_custom_vendor" , { name : searchQuery } ) } { /* Add new translation key */ }
224- </ CommandItem >
214+ { t ( "vendors.form.create_custom_vendor" , { name : searchQuery } ) }
215+ </ div >
225216 ) }
226- </ CommandGroup >
227- </ CommandList >
228- </ Command >
229- </ PopoverContent >
230- </ Popover >
217+ </ div >
218+ </ div >
219+ ) }
220+ </ div >
221+ </ FormControl >
231222 < FormMessage />
232223 </ FormItem >
233224 ) }
0 commit comments