Skip to content

Commit a919631

Browse files
committed
fix(tools): fix linear block
1 parent 51120c8 commit a919631

4 files changed

Lines changed: 298 additions & 89 deletions

File tree

apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx

Lines changed: 121 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { 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'
25
import {
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

1015
export 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
}

apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx

Lines changed: 119 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { 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'
25
import {
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

1015
export interface LinearTeamInfo {
1116
id: string
@@ -31,11 +36,15 @@ export function LinearTeamSelector({
3136
const [teams, setTeams] = useState<LinearTeamInfo[]>([])
3237
const [loading, setLoading] = useState(false)
3338
const [error, setError] = useState<string | null>(null)
39+
const [open, setOpen] = useState(false)
40+
const [selectedTeam, setSelectedTeam] = useState<LinearTeamInfo | null>(null)
3441

3542
useEffect(() => {
3643
if (!credential) return
3744
const controller = new AbortController()
3845
setLoading(true)
46+
setError(null)
47+
3948
fetch('/api/tools/linear/teams', {
4049
method: 'POST',
4150
headers: { 'Content-Type': 'application/json' },
@@ -52,6 +61,12 @@ export function LinearTeamSelector({
5261
setTeams([])
5362
} else {
5463
setTeams(data.teams)
64+
65+
// Find selected team info if we have a value
66+
if (value) {
67+
const teamInfo = data.teams.find((t: LinearTeamInfo) => t.id === value)
68+
setSelectedTeam(teamInfo || null)
69+
}
5570
}
5671
})
5772
.catch((err) => {
@@ -61,28 +76,105 @@ export function LinearTeamSelector({
6176
})
6277
.finally(() => setLoading(false))
6378
return () => controller.abort()
64-
}, [credential])
79+
}, [credential, value])
80+
81+
// Sync selected team with value prop
82+
useEffect(() => {
83+
if (value && teams.length > 0) {
84+
const teamInfo = teams.find((t) => t.id === value)
85+
setSelectedTeam(teamInfo || null)
86+
} else if (!value) {
87+
setSelectedTeam(null)
88+
}
89+
}, [value, teams])
90+
91+
const handleSelectTeam = (team: LinearTeamInfo) => {
92+
setSelectedTeam(team)
93+
onChange(team.id, team)
94+
setOpen(false)
95+
}
96+
97+
const handleOpenChange = (isOpen: boolean) => {
98+
setOpen(isOpen)
99+
}
65100

66101
return (
67-
<Select
68-
value={value}
69-
onValueChange={(teamId) => {
70-
const teamInfo = teams.find((t) => t.id === teamId)
71-
onChange(teamId, teamInfo)
72-
}}
73-
disabled={disabled || loading || !credential}
74-
>
75-
<SelectTrigger className='w-full'>
76-
<SelectValue placeholder={loading ? 'Loading teams...' : label} />
77-
</SelectTrigger>
78-
<SelectContent>
79-
{teams.map((team) => (
80-
<SelectItem key={team.id} value={team.id}>
81-
{team.name}
82-
</SelectItem>
83-
))}
84-
{error && <div className='px-2 py-1 text-red-500'>{error}</div>}
85-
</SelectContent>
86-
</Select>
102+
<Popover open={open} onOpenChange={handleOpenChange}>
103+
<PopoverTrigger asChild>
104+
<Button
105+
variant='outline'
106+
role='combobox'
107+
aria-expanded={open}
108+
className='w-full justify-between'
109+
disabled={disabled || !credential}
110+
>
111+
{selectedTeam ? (
112+
<div className='flex items-center gap-2 overflow-hidden'>
113+
<LinearIcon className='h-4 w-4' />
114+
<span className='truncate font-normal'>{selectedTeam.name}</span>
115+
</div>
116+
) : (
117+
<div className='flex items-center gap-2'>
118+
<LinearIcon className='h-4 w-4' />
119+
<span className='text-muted-foreground'>{label}</span>
120+
</div>
121+
)}
122+
<ChevronDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
123+
</Button>
124+
</PopoverTrigger>
125+
<PopoverContent className='w-[300px] p-0' align='start'>
126+
<Command>
127+
<CommandInput placeholder='Search teams...' />
128+
<CommandList>
129+
<CommandEmpty>
130+
{loading ? (
131+
<div className='flex items-center justify-center p-4'>
132+
<RefreshCw className='h-4 w-4 animate-spin' />
133+
<span className='ml-2'>Loading teams...</span>
134+
</div>
135+
) : error ? (
136+
<div className='p-4 text-center'>
137+
<p className='text-destructive text-sm'>{error}</p>
138+
</div>
139+
) : !credential ? (
140+
<div className='p-4 text-center'>
141+
<p className='font-medium text-sm'>Missing credentials</p>
142+
<p className='text-muted-foreground text-xs'>
143+
Please configure Linear credentials.
144+
</p>
145+
</div>
146+
) : (
147+
<div className='p-4 text-center'>
148+
<p className='font-medium text-sm'>No teams found</p>
149+
<p className='text-muted-foreground text-xs'>
150+
No teams available for this Linear account.
151+
</p>
152+
</div>
153+
)}
154+
</CommandEmpty>
155+
156+
{teams.length > 0 && (
157+
<CommandGroup>
158+
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>Teams</div>
159+
{teams.map((team) => (
160+
<CommandItem
161+
key={team.id}
162+
value={`team-${team.id}-${team.name}`}
163+
onSelect={() => handleSelectTeam(team)}
164+
className='cursor-pointer'
165+
>
166+
<div className='flex items-center gap-2 overflow-hidden'>
167+
<LinearIcon className='h-4 w-4' />
168+
<span className='truncate font-normal'>{team.name}</span>
169+
</div>
170+
{team.id === value && <Check className='ml-auto h-4 w-4' />}
171+
</CommandItem>
172+
))}
173+
</CommandGroup>
174+
)}
175+
</CommandList>
176+
</Command>
177+
</PopoverContent>
178+
</Popover>
87179
)
88180
}

apps/sim/components/icons.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,13 +2283,13 @@ export function LinearIcon(props: React.SVGProps<SVGSVGElement>) {
22832283
<svg
22842284
{...props}
22852285
xmlns='http://www.w3.org/2000/svg'
2286-
fill='none'
2286+
fill='currentColor'
22872287
width='200'
22882288
height='200'
22892289
viewBox='0 0 100 100'
22902290
>
22912291
<path
2292-
fill='#fff'
2292+
fill='currentColor'
22932293
d='M1.22541 61.5228c-.2225-.9485.90748-1.5459 1.59638-.857L39.3342 97.1782c.6889.6889.0915 1.8189-.857 1.5964C20.0515 94.4522 5.54779 79.9485 1.22541 61.5228ZM.00189135 46.8891c-.01764375.2833.08887215.5599.28957165.7606L52.3503 99.7085c.2007.2007.4773.3075.7606.2896 2.3692-.1476 4.6938-.46 6.9624-.9259.7645-.157 1.0301-1.0963.4782-1.6481L2.57595 39.4485c-.55186-.5519-1.49117-.2863-1.648174.4782-.465915 2.2686-.77832 4.5932-.92588465 6.9624ZM4.21093 29.7054c-.16649.3738-.08169.8106.20765 1.1l64.77602 64.776c.2894.2894.7262.3742 1.1.2077 1.7861-.7956 3.5171-1.6927 5.1855-2.684.5521-.328.6373-1.0867.1832-1.5407L8.43566 24.3367c-.45409-.4541-1.21271-.3689-1.54074.1832-.99132 1.6684-1.88843 3.3994-2.68399 5.1855ZM12.6587 18.074c-.3701-.3701-.393-.9637-.0443-1.3541C21.7795 6.45931 35.1114 0 49.9519 0 77.5927 0 100 22.4073 100 50.0481c0 14.8405-6.4593 28.1724-16.7199 37.3375-.3903.3487-.984.3258-1.3542-.0443L12.6587 18.074Z'
22942294
/>
22952295
</svg>

0 commit comments

Comments
 (0)