11import { useEffect , useState } from 'react'
2+ import { Check , ChevronDown , RefreshCw } from 'lucide-react'
3+ import { LinearIcon } from '@/components/icons'
4+ import { Button } from '@/components/ui/button'
25import {
3- Select ,
4- SelectContent ,
5- SelectItem ,
6- SelectTrigger ,
7- SelectValue ,
8- } from '@/components/ui/select'
6+ Command ,
7+ CommandEmpty ,
8+ CommandGroup ,
9+ CommandInput ,
10+ CommandItem ,
11+ CommandList ,
12+ } from '@/components/ui/command'
13+ import { Popover , PopoverContent , PopoverTrigger } from '@/components/ui/popover'
914
1015export interface LinearProjectInfo {
1116 id : string
@@ -32,11 +37,15 @@ export function LinearProjectSelector({
3237 const [ projects , setProjects ] = useState < LinearProjectInfo [ ] > ( [ ] )
3338 const [ loading , setLoading ] = useState ( false )
3439 const [ error , setError ] = useState < string | null > ( null )
40+ const [ open , setOpen ] = useState ( false )
41+ const [ selectedProject , setSelectedProject ] = useState < LinearProjectInfo | null > ( null )
3542
3643 useEffect ( ( ) => {
3744 if ( ! credential || ! teamId ) return
3845 const controller = new AbortController ( )
3946 setLoading ( true )
47+ setError ( null )
48+
4049 fetch ( '/api/tools/linear/projects' , {
4150 method : 'POST' ,
4251 headers : { 'Content-Type' : 'application/json' } ,
@@ -56,6 +65,12 @@ export function LinearProjectSelector({
5665 setProjects ( [ ] )
5766 } else {
5867 setProjects ( data . projects )
68+
69+ // Find selected project info if we have a value
70+ if ( value ) {
71+ const projectInfo = data . projects . find ( ( p : LinearProjectInfo ) => p . id === value )
72+ setSelectedProject ( projectInfo || null )
73+ }
5974 }
6075 } )
6176 . catch ( ( err ) => {
@@ -65,28 +80,107 @@ export function LinearProjectSelector({
6580 } )
6681 . finally ( ( ) => setLoading ( false ) )
6782 return ( ) => controller . abort ( )
68- } , [ credential , teamId ] )
83+ } , [ credential , teamId , value ] )
84+
85+ // Sync selected project with value prop
86+ useEffect ( ( ) => {
87+ if ( value && projects . length > 0 ) {
88+ const projectInfo = projects . find ( ( p ) => p . id === value )
89+ setSelectedProject ( projectInfo || null )
90+ } else if ( ! value ) {
91+ setSelectedProject ( null )
92+ }
93+ } , [ value , projects ] )
94+
95+ const handleSelectProject = ( project : LinearProjectInfo ) => {
96+ setSelectedProject ( project )
97+ onChange ( project . id , project )
98+ setOpen ( false )
99+ }
100+
101+ const handleOpenChange = ( isOpen : boolean ) => {
102+ setOpen ( isOpen )
103+ }
69104
70105 return (
71- < Select
72- value = { value }
73- onValueChange = { ( projectId ) => {
74- const projectInfo = projects . find ( ( p ) => p . id === projectId )
75- onChange ( projectId , projectInfo )
76- } }
77- disabled = { disabled || loading || ! credential || ! teamId }
78- >
79- < SelectTrigger className = 'w-full' >
80- < SelectValue placeholder = { loading ? 'Loading projects...' : label } />
81- </ SelectTrigger >
82- < SelectContent >
83- { projects . map ( ( project ) => (
84- < SelectItem key = { project . id } value = { project . id } >
85- { project . name }
86- </ SelectItem >
87- ) ) }
88- { error && < div className = 'px-2 py-1 text-red-500' > { error } </ div > }
89- </ SelectContent >
90- </ Select >
106+ < Popover open = { open } onOpenChange = { handleOpenChange } >
107+ < PopoverTrigger asChild >
108+ < Button
109+ variant = 'outline'
110+ role = 'combobox'
111+ aria-expanded = { open }
112+ className = 'w-full justify-between'
113+ disabled = { disabled || ! credential || ! teamId }
114+ >
115+ { selectedProject ? (
116+ < div className = 'flex items-center gap-2 overflow-hidden' >
117+ < LinearIcon className = 'h-4 w-4' />
118+ < span className = 'truncate font-normal' > { selectedProject . name } </ span >
119+ </ div >
120+ ) : (
121+ < div className = 'flex items-center gap-2' >
122+ < LinearIcon className = 'h-4 w-4' />
123+ < span className = 'text-muted-foreground' > { label } </ span >
124+ </ div >
125+ ) }
126+ < ChevronDown className = 'ml-2 h-4 w-4 shrink-0 opacity-50' />
127+ </ Button >
128+ </ PopoverTrigger >
129+ < PopoverContent className = 'w-[300px] p-0' align = 'start' >
130+ < Command >
131+ < CommandInput placeholder = 'Search projects...' />
132+ < CommandList >
133+ < CommandEmpty >
134+ { loading ? (
135+ < div className = 'flex items-center justify-center p-4' >
136+ < RefreshCw className = 'h-4 w-4 animate-spin' />
137+ < span className = 'ml-2' > Loading projects...</ span >
138+ </ div >
139+ ) : error ? (
140+ < div className = 'p-4 text-center' >
141+ < p className = 'text-destructive text-sm' > { error } </ p >
142+ </ div >
143+ ) : ! credential || ! teamId ? (
144+ < div className = 'p-4 text-center' >
145+ < p className = 'font-medium text-sm' > Missing credentials or team</ p >
146+ < p className = 'text-muted-foreground text-xs' >
147+ Please configure Linear credentials and select a team.
148+ </ p >
149+ </ div >
150+ ) : (
151+ < div className = 'p-4 text-center' >
152+ < p className = 'font-medium text-sm' > No projects found</ p >
153+ < p className = 'text-muted-foreground text-xs' >
154+ No projects available for the selected team.
155+ </ p >
156+ </ div >
157+ ) }
158+ </ CommandEmpty >
159+
160+ { projects . length > 0 && (
161+ < CommandGroup >
162+ < div className = 'px-2 py-1.5 font-medium text-muted-foreground text-xs' >
163+ Projects
164+ </ div >
165+ { projects . map ( ( project ) => (
166+ < CommandItem
167+ key = { project . id }
168+ value = { `project-${ project . id } -${ project . name } ` }
169+ onSelect = { ( ) => handleSelectProject ( project ) }
170+ className = 'cursor-pointer'
171+ >
172+ < div className = 'flex items-center gap-2 overflow-hidden' >
173+ < LinearIcon className = 'h-4 w-4' />
174+ < span className = 'truncate font-normal' > { project . name } </ span >
175+ </ div >
176+ { project . id === value && < Check className = 'ml-auto h-4 w-4' /> }
177+ </ CommandItem >
178+ ) ) }
179+ </ CommandGroup >
180+ ) }
181+ </ CommandList >
182+ </ Command >
183+ </ PopoverContent >
184+ </ Popover >
91185 )
92186}
0 commit comments