-
Notifications
You must be signed in to change notification settings - Fork 3.7k
feat(linear): added Linear tool #430
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
c15ed30
feat(linear): add Linear Issue Reader and Writer tools with types
sriram2k4 cf5498c
chore(tools): register Linear tools in global tool registry
sriram2k4 481d0c6
feat(icons): add LinearIcon for Linear block
sriram2k4 bd0079c
feat(blocks): register Linear block in global block registry
sriram2k4 15077eb
feat(linear): implement OAuth integration for Linear block
sriram2k4 fe058dc
feat(linear): add dynamic team and project selectors for Linear block
sriram2k4 3b83c63
feat(linear): add backend API endpoints for teams and projects
sriram2k4 e9a6194
feat(linear): update UI components for Linear selectors and modal
sriram2k4 23abcee
refactor(linear): update create/read issue tools and types
sriram2k4 40d9641
chore(linear): update block config for Linear integration
sriram2k4 84c2d6b
fix(auth): update auth and oauth logic for Linear
sriram2k4 7dd3b35
minor fix
sriram2k4 d744f7c
improvement[linear]: require teamId and projectId for all tools and t…
sriram2k4 8db78a1
style[lint]: fix code style and lint errors
sriram2k4 b370609
chore(linear): install @linear/sdk package
sriram2k4 7483381
fix[linear]: address greptile-apps feedback for type safety and error…
sriram2k4 211d5b1
fix[linear]: handle teams API response errors
sriram2k4 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import type { Project } from '@linear/sdk' | ||
| import { LinearClient } from '@linear/sdk' | ||
| import { NextResponse } from 'next/server' | ||
| import { getSession } from '@/lib/auth' | ||
| import { createLogger } from '@/lib/logs/console-logger' | ||
| import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' | ||
|
|
||
| export const dynamic = 'force-dynamic' | ||
|
|
||
| const logger = createLogger('LinearProjects') | ||
|
|
||
| export async function POST(request: Request) { | ||
| try { | ||
| const session = await getSession() | ||
| const body = await request.json() | ||
| const { credential, teamId, workflowId } = body | ||
|
|
||
| if (!credential || !teamId) { | ||
| logger.error('Missing credential or teamId in request') | ||
| return NextResponse.json({ error: 'Credential and teamId are required' }, { status: 400 }) | ||
| } | ||
|
|
||
| const userId = session?.user?.id || '' | ||
| if (!userId) { | ||
| logger.error('No user ID found in session') | ||
| return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) | ||
| } | ||
|
|
||
| const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId) | ||
| if (!accessToken) { | ||
| logger.error('Failed to get access token', { credentialId: credential, userId }) | ||
| return NextResponse.json( | ||
| { | ||
| error: 'Could not retrieve access token', | ||
| authRequired: true, | ||
| }, | ||
| { status: 401 } | ||
| ) | ||
| } | ||
|
|
||
| const linearClient = new LinearClient({ accessToken }) | ||
| let projects = [] | ||
|
|
||
| const team = await linearClient.team(teamId) | ||
| const projectsResult = await team.projects() | ||
| projects = projectsResult.nodes.map((project: Project) => ({ | ||
| id: project.id, | ||
| name: project.name, | ||
| })) | ||
|
|
||
| if (projects.length === 0) { | ||
| logger.info('No projects found for team', { teamId }) | ||
| } | ||
|
|
||
| return NextResponse.json({ projects }) | ||
| } catch (error) { | ||
| logger.error('Error processing Linear projects request:', error) | ||
| return NextResponse.json( | ||
| { error: 'Failed to retrieve Linear projects', details: (error as Error).message }, | ||
| { status: 500 } | ||
| ) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import type { Team } from '@linear/sdk' | ||
| import { LinearClient } from '@linear/sdk' | ||
| import { NextResponse } from 'next/server' | ||
| import { getSession } from '@/lib/auth' | ||
| import { createLogger } from '@/lib/logs/console-logger' | ||
| import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' | ||
|
|
||
| export const dynamic = 'force-dynamic' | ||
|
|
||
| const logger = createLogger('LinearTeams') | ||
|
|
||
| export async function POST(request: Request) { | ||
| try { | ||
| const session = await getSession() | ||
| const body = await request.json() | ||
| const { credential, workflowId } = body | ||
|
|
||
| if (!credential) { | ||
| logger.error('Missing credential in request') | ||
| return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) | ||
| } | ||
|
|
||
| const userId = session?.user?.id || '' | ||
| if (!userId) { | ||
| logger.error('No user ID found in session') | ||
| return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) | ||
| } | ||
|
|
||
| const accessToken = await refreshAccessTokenIfNeeded(credential, userId, workflowId) | ||
| if (!accessToken) { | ||
| logger.error('Failed to get access token', { credentialId: credential, userId }) | ||
| return NextResponse.json( | ||
| { | ||
| error: 'Could not retrieve access token', | ||
| authRequired: true, | ||
| }, | ||
| { status: 401 } | ||
| ) | ||
| } | ||
|
|
||
| const linearClient = new LinearClient({ accessToken }) | ||
| const teamsResult = await linearClient.teams() | ||
| const teams = teamsResult.nodes.map((team: Team) => ({ | ||
| id: team.id, | ||
| name: team.name, | ||
| })) | ||
|
|
||
| return NextResponse.json({ teams }) | ||
| } catch (error) { | ||
| logger.error('Error processing Linear teams request:', error) | ||
| return NextResponse.json( | ||
| { error: 'Failed to retrieve Linear teams', details: (error as Error).message }, | ||
| { status: 500 } | ||
| ) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
...k/components/sub-block/components/project-selector/components/linear-project-selector.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { useEffect, useState } from 'react' | ||
| import { | ||
| Select, | ||
| SelectContent, | ||
| SelectItem, | ||
| SelectTrigger, | ||
| SelectValue, | ||
| } from '@/components/ui/select' | ||
|
|
||
| export interface LinearProjectInfo { | ||
| id: string | ||
| name: string | ||
| } | ||
|
|
||
| interface LinearProjectSelectorProps { | ||
| value: string | ||
| onChange: (projectId: string, projectInfo?: LinearProjectInfo) => void | ||
| credential: string | ||
| teamId: string | ||
| label?: string | ||
| disabled?: boolean | ||
| } | ||
|
|
||
| export function LinearProjectSelector({ | ||
| value, | ||
| onChange, | ||
| credential, | ||
| teamId, | ||
| label = 'Select Linear project', | ||
| disabled = false, | ||
| }: LinearProjectSelectorProps) { | ||
| const [projects, setProjects] = useState<LinearProjectInfo[]>([]) | ||
| const [loading, setLoading] = useState(false) | ||
| const [error, setError] = useState<string | null>(null) | ||
|
|
||
| useEffect(() => { | ||
| if (!credential || !teamId) return | ||
| const controller = new AbortController() | ||
| setLoading(true) | ||
| fetch('/api/tools/linear/projects', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ credential, teamId }), | ||
| signal: controller.signal, | ||
| }) | ||
| .then(async (res) => { | ||
| if (!res.ok) { | ||
| const errorText = await res.text() | ||
| throw new Error(`HTTP error! status: ${res.status} - ${errorText}`) | ||
| } | ||
| return res.json() | ||
| }) | ||
| .then((data) => { | ||
| if (data.error) { | ||
| setError(data.error) | ||
| setProjects([]) | ||
| } else { | ||
| setProjects(data.projects) | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| if (err.name === 'AbortError') return | ||
| setError(err.message) | ||
| setProjects([]) | ||
| }) | ||
| .finally(() => setLoading(false)) | ||
| return () => controller.abort() | ||
| }, [credential, teamId]) | ||
|
|
||
| return ( | ||
| <Select | ||
| value={value} | ||
| onValueChange={(projectId) => { | ||
| const projectInfo = projects.find((p) => p.id === projectId) | ||
| onChange(projectId, projectInfo) | ||
| }} | ||
| disabled={disabled || loading || !credential || !teamId} | ||
| > | ||
| <SelectTrigger className='w-full'> | ||
| <SelectValue placeholder={loading ? 'Loading projects...' : label} /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| {projects.map((project) => ( | ||
| <SelectItem key={project.id} value={project.id}> | ||
| {project.name} | ||
| </SelectItem> | ||
| ))} | ||
| {error && <div className='px-2 py-1 text-red-500'>{error}</div>} | ||
| </SelectContent> | ||
| </Select> | ||
| ) | ||
| } |
88 changes: 88 additions & 0 deletions
88
...lock/components/sub-block/components/project-selector/components/linear-team-selector.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import { useEffect, useState } from 'react' | ||
| import { | ||
| Select, | ||
| SelectContent, | ||
| SelectItem, | ||
| SelectTrigger, | ||
| SelectValue, | ||
| } from '@/components/ui/select' | ||
|
|
||
| export interface LinearTeamInfo { | ||
| id: string | ||
| name: string | ||
| } | ||
|
|
||
| interface LinearTeamSelectorProps { | ||
| value: string | ||
| onChange: (teamId: string, teamInfo?: LinearTeamInfo) => void | ||
| credential: string | ||
| label?: string | ||
| disabled?: boolean | ||
| showPreview?: boolean | ||
|
waleedlatif1 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| export function LinearTeamSelector({ | ||
| value, | ||
| onChange, | ||
| credential, | ||
| label = 'Select Linear team', | ||
| disabled = false, | ||
| }: LinearTeamSelectorProps) { | ||
| const [teams, setTeams] = useState<LinearTeamInfo[]>([]) | ||
| const [loading, setLoading] = useState(false) | ||
| const [error, setError] = useState<string | null>(null) | ||
|
|
||
| useEffect(() => { | ||
| if (!credential) return | ||
| const controller = new AbortController() | ||
| setLoading(true) | ||
| fetch('/api/tools/linear/teams', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ credential }), | ||
| signal: controller.signal, | ||
| }) | ||
| .then((res) => { | ||
| if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`) | ||
| return res.json() | ||
| }) | ||
| .then((data) => { | ||
| if (data.error) { | ||
| setError(data.error) | ||
| setTeams([]) | ||
| } else { | ||
| setTeams(data.teams) | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| if (err.name === 'AbortError') return | ||
| setError(err.message) | ||
| setTeams([]) | ||
| }) | ||
| .finally(() => setLoading(false)) | ||
| return () => controller.abort() | ||
| }, [credential]) | ||
|
|
||
| return ( | ||
| <Select | ||
| value={value} | ||
| onValueChange={(teamId) => { | ||
| const teamInfo = teams.find((t) => t.id === teamId) | ||
| onChange(teamId, teamInfo) | ||
| }} | ||
| disabled={disabled || loading || !credential} | ||
| > | ||
| <SelectTrigger className='w-full'> | ||
| <SelectValue placeholder={loading ? 'Loading teams...' : label} /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| {teams.map((team) => ( | ||
| <SelectItem key={team.id} value={team.id}> | ||
| {team.name} | ||
| </SelectItem> | ||
| ))} | ||
| {error && <div className='px-2 py-1 text-red-500'>{error}</div>} | ||
| </SelectContent> | ||
| </Select> | ||
| ) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.