From c15ed309bc5f88951bf0c3e0448b6de0ce8a25f9 Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 09:34:03 +0530 Subject: [PATCH 01/17] feat(linear): add Linear Issue Reader and Writer tools with types --- apps/sim/tools/linear/create_issue.ts | 73 +++++++++++++++++++++++++++ apps/sim/tools/linear/index.ts | 4 ++ apps/sim/tools/linear/read_issues.ts | 72 ++++++++++++++++++++++++++ apps/sim/tools/linear/types.ts | 36 +++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 apps/sim/tools/linear/create_issue.ts create mode 100644 apps/sim/tools/linear/index.ts create mode 100644 apps/sim/tools/linear/read_issues.ts create mode 100644 apps/sim/tools/linear/types.ts diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts new file mode 100644 index 00000000000..45016f5d6ab --- /dev/null +++ b/apps/sim/tools/linear/create_issue.ts @@ -0,0 +1,73 @@ +import type { ToolConfig } from '../types' +import type { LinearCreateIssueParams, LinearCreateIssueResponse } from './types' + +export const linearCreateIssueTool: ToolConfig = + { + id: 'linear_create_issue', + name: 'Linear Issue Writer', + description: 'Create a new issue in Linear', + version: '1.0.0', + params: { + teamId: { type: 'string', required: true, description: 'Linear team ID' }, + projectId: { type: 'string', required: false, description: 'Linear project ID' }, + title: { type: 'string', required: true, description: 'Issue title' }, + description: { type: 'string', required: false, description: 'Issue description' }, + }, + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || ''}`, + }), + body: (params) => ({ + query: ` + mutation CreateIssue($teamId: String!, $projectId: String, $title: String!, $description: String) { + issueCreate( + input: { + teamId: $teamId + projectId: $projectId + title: $title + description: $description + } + ) { + issue { + id + title + description + state { name } + team { id } + project { id } + } + } + } + `, + variables: { + teamId: params.teamId, + projectId: params.projectId, + title: params.title, + description: params.description, + }, + }), + }, + transformResponse: async (response) => { + const data = await response.json() + if (data.errors) { + return { success: false, output: { issue: null }, error: data.errors[0].message } + } + return { + success: true, + output: { + issue: { + id: data.data.issueCreate.issue.id, + title: data.data.issueCreate.issue.title, + description: data.data.issueCreate.issue.description, + state: data.data.issueCreate.issue.state?.name, + teamId: data.data.issueCreate.issue.team?.id, + projectId: data.data.issueCreate.issue.project?.id, + }, + }, + } + }, + transformError: (error) => error.message || 'Failed to create Linear issue', + } diff --git a/apps/sim/tools/linear/index.ts b/apps/sim/tools/linear/index.ts new file mode 100644 index 00000000000..5726982ed4e --- /dev/null +++ b/apps/sim/tools/linear/index.ts @@ -0,0 +1,4 @@ +import { linearCreateIssueTool } from './create_issue' +import { linearReadIssuesTool } from './read_issues' + +export { linearReadIssuesTool, linearCreateIssueTool } diff --git a/apps/sim/tools/linear/read_issues.ts b/apps/sim/tools/linear/read_issues.ts new file mode 100644 index 00000000000..dc4cb24d0df --- /dev/null +++ b/apps/sim/tools/linear/read_issues.ts @@ -0,0 +1,72 @@ +import type { ToolConfig } from '../types' +import type { LinearReadIssuesParams, LinearReadIssuesResponse } from './types' + +export const linearReadIssuesTool: ToolConfig = { + id: 'linear_read_issues', + name: 'Linear Issue Reader', + description: 'Fetch and filter issues from Linear', + version: '1.0.0', + params: { + teamId: { type: 'string', required: false, description: 'Linear team ID' }, + projectId: { type: 'string', required: false, description: 'Linear project ID' }, + state: { type: 'string', required: false, description: 'Issue state' }, + search: { type: 'string', required: false, description: 'Search query' }, + }, + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || ''}`, + }), + body: (params) => ({ + query: ` + query Issues($teamId: String, $projectId: String, $state: String, $search: String) { + issues( + filter: { + team: { id: $teamId } + project: { id: $projectId } + state: { name: { eq: $state } } + search: $search + } + ) { + nodes { + id + title + description + state { name } + team { id } + project { id } + } + } + } + `, + variables: { + teamId: params.teamId, + projectId: params.projectId, + state: params.state, + search: params.search, + }, + }), + }, + transformResponse: async (response) => { + const data = await response.json() + if (data.errors) { + return { success: false, output: { issues: [] }, error: data.errors[0].message } + } + return { + success: true, + output: { + issues: data.data.issues.nodes.map((issue: any) => ({ + id: issue.id, + title: issue.title, + description: issue.description, + state: issue.state?.name, + teamId: issue.team?.id, + projectId: issue.project?.id, + })), + }, + } + }, + transformError: (error) => error.message || 'Failed to fetch Linear issues', +} diff --git a/apps/sim/tools/linear/types.ts b/apps/sim/tools/linear/types.ts new file mode 100644 index 00000000000..cbc85166cb7 --- /dev/null +++ b/apps/sim/tools/linear/types.ts @@ -0,0 +1,36 @@ +import type { ToolResponse } from '../types' + +export interface LinearIssue { + id: string + title: string + description?: string + state?: string + teamId?: string + projectId?: string +} + +export interface LinearReadIssuesParams { + teamId?: string + projectId?: string + state?: string + search?: string +} + +export interface LinearCreateIssueParams { + teamId: string + projectId?: string + title: string + description?: string +} + +export interface LinearReadIssuesResponse extends ToolResponse { + output: { + issues: LinearIssue[] + } +} + +export interface LinearCreateIssueResponse extends ToolResponse { + output: { + issue: LinearIssue + } +} From cf5498c77b5b709ef9f9acecba3d978b46b8e03c Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 09:41:09 +0530 Subject: [PATCH 02/17] chore(tools): register Linear tools in global tool registry --- apps/sim/tools/registry.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 46b6aeebf4d..21807c4f032 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -45,6 +45,7 @@ import { requestTool as httpRequest } from './http' import { contactsTool as hubspotContacts } from './hubspot/contacts' import { readUrlTool } from './jina' import { jiraBulkRetrieveTool, jiraRetrieveTool, jiraUpdateTool, jiraWriteTool } from './jira' +import { linearCreateIssueTool, linearReadIssuesTool } from './linear' import { linkupSearchTool } from './linkup' import { mem0AddMemoriesTool, mem0GetMemoriesTool, mem0SearchMemoriesTool } from './mem0' import { memoryAddTool, memoryDeleteTool, memoryGetAllTool, memoryGetTool } from './memory' @@ -187,4 +188,6 @@ export const tools: Record = { outlook_read: outlookReadTool, outlook_send: outlookSendTool, outlook_draft: outlookDraftTool, + linear_read_issues: linearReadIssuesTool, + linear_create_issue: linearCreateIssueTool, } From 481d0c6cfa11a99eede79073839c15ba13c9bd19 Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 09:48:45 +0530 Subject: [PATCH 03/17] feat(icons): add LinearIcon for Linear block --- apps/sim/components/icons.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index d7e2f3f22fe..7be99c905de 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2278,6 +2278,28 @@ export function JiraIcon(props: SVGProps) { ) } +export function LinearIcon(props: React.SVGProps) { + return ( + + + + + ) +} + export function TelegramIcon(props: SVGProps) { return ( Date: Wed, 28 May 2025 09:56:58 +0530 Subject: [PATCH 04/17] feat(blocks): register Linear block in global block registry --- apps/sim/blocks/blocks/linear.ts | 33 ++++++++++++++++++++++++++++++++ apps/sim/blocks/registry.ts | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 apps/sim/blocks/blocks/linear.ts diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts new file mode 100644 index 00000000000..660cdb26645 --- /dev/null +++ b/apps/sim/blocks/blocks/linear.ts @@ -0,0 +1,33 @@ +import { LinearIcon } from '@/components/icons' +import type { LinearReadIssuesResponse } from '@/tools/linear/types' +import type { BlockConfig } from '../types' + +export const LinearBlock: BlockConfig = { + type: 'linear', + name: 'Linear', + description: 'Read and create issues in Linear', + longDescription: + 'Integrate with Linear to fetch, filter, and create issues directly from your workflow.', + category: 'tools', + bgColor: '#5E6AD2', + icon: LinearIcon, + subBlocks: [], + tools: { + access: ['linear_read_issues', 'linear_create_issue'], + }, + inputs: { + teamId: { type: 'string', required: false, description: 'Linear team ID' }, + projectId: { type: 'string', required: false, description: 'Linear project ID' }, + state: { type: 'string', required: false, description: 'Issue state' }, + search: { type: 'string', required: false, description: 'Search query' }, + title: { type: 'string', required: false, description: 'Issue title (for create)' }, + description: { type: 'string', required: false, description: 'Issue description (for create)' }, + }, + outputs: { + response: { + type: { + issues: 'json', + }, + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 4571e9910cc..0d38fcfc052 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -28,6 +28,7 @@ import { GoogleSheetsBlock } from './blocks/google_sheets' import { ImageGeneratorBlock } from './blocks/image_generator' import { JinaBlock } from './blocks/jina' import { JiraBlock } from './blocks/jira' +import { LinearBlock } from './blocks/linear' import { LinkupBlock } from './blocks/linkup' import { Mem0Block } from './blocks/mem0' // import { GuestyBlock } from './blocks/guesty' @@ -116,6 +117,7 @@ export const registry: Record = { whatsapp: WhatsAppBlock, x: XBlock, youtube: YouTubeBlock, + linear: LinearBlock, } // Helper functions to access the registry From 15077eb289f29cd7cab604d09df527150ead9864 Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 10:39:33 +0530 Subject: [PATCH 05/17] feat(linear): implement OAuth integration for Linear block --- apps/sim/blocks/blocks/linear.ts | 106 ++++++++++++++++++++++++-- apps/sim/lib/auth.ts | 45 +++++++++++ apps/sim/lib/env.ts | 2 + apps/sim/lib/oauth.ts | 22 ++++++ apps/sim/tools/linear/create_issue.ts | 4 + apps/sim/tools/linear/read_issues.ts | 4 + apps/sim/tools/linear/types.ts | 2 + 7 files changed, 178 insertions(+), 7 deletions(-) diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 660cdb26645..2cc0a11703c 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -11,17 +11,109 @@ export const LinearBlock: BlockConfig = { category: 'tools', bgColor: '#5E6AD2', icon: LinearIcon, - subBlocks: [], + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Read Issues', id: 'read' }, + { label: 'Create Issue', id: 'write' }, + ], + }, + { + id: 'credential', + title: 'Linear Account', + type: 'oauth-input', + layout: 'full', + provider: 'linear', + serviceId: 'linear', + requiredScopes: [], + placeholder: 'Select Linear account', + }, + { + id: 'teamId', + title: 'Team', + type: 'project-selector', + layout: 'full', + provider: 'linear', + serviceId: 'linear', + placeholder: 'Select a team', + }, + { + id: 'projectId', + title: 'Project', + type: 'project-selector', + layout: 'full', + provider: 'linear', + serviceId: 'linear', + placeholder: 'Select a project', + }, + { + id: 'state', + title: 'Issue State', + type: 'dropdown', + options: ['Backlog', 'Todo', 'In Progress', 'Done', 'Canceled'], + description: 'Filter by issue state', + condition: { field: 'operation', value: ['read'] }, + }, + { + id: 'search', + title: 'Search', + type: 'short-input', + description: 'Search issues', + condition: { field: 'operation', value: ['read'] }, + }, + { + id: 'title', + title: 'Title', + type: 'short-input', + description: 'Title for new issue', + condition: { field: 'operation', value: ['write'] }, + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + description: 'Description for new issue', + condition: { field: 'operation', value: ['write'] }, + }, + ], tools: { access: ['linear_read_issues', 'linear_create_issue'], + config: { + tool: (params) => + params.operation === 'write' ? 'linear_create_issue' : 'linear_read_issues', + params: (params) => { + if (params.operation === 'write') { + return { + credential: params.credential, + teamId: params.teamId, + projectId: params.projectId, + title: params.title, + description: params.description, + } + } + return { + credential: params.credential, + teamId: params.teamId, + projectId: params.projectId, + state: params.state, + search: params.search, + } + }, + }, }, inputs: { - teamId: { type: 'string', required: false, description: 'Linear team ID' }, - projectId: { type: 'string', required: false, description: 'Linear project ID' }, - state: { type: 'string', required: false, description: 'Issue state' }, - search: { type: 'string', required: false, description: 'Search query' }, - title: { type: 'string', required: false, description: 'Issue title (for create)' }, - description: { type: 'string', required: false, description: 'Issue description (for create)' }, + operation: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + teamId: { type: 'string', required: false }, + projectId: { type: 'string', required: false }, + state: { type: 'string', required: false }, + search: { type: 'string', required: false }, + title: { type: 'string', required: false }, + description: { type: 'string', required: false }, }, outputs: { response: { diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 2963b22611c..9260bc013ab 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -776,6 +776,51 @@ export const auth = betterAuth({ } }, }, + + { + providerId: 'linear', + clientId: env.LINEAR_CLIENT_ID as string, + clientSecret: env.LINEAR_CLIENT_SECRET as string, + authorizationUrl: 'https://linear.app/oauth/authorize', + tokenUrl: 'https://api.linear.app/oauth/token', + scopes: [], // Add required scopes if Linear supports them + responseType: 'code', + accessType: 'offline', + prompt: 'consent', + pkce: false, + redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/linear`, + getUserInfo: async (tokens) => { + const response = await fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `\n query {\n viewer {\n id\n name\n email\n avatarUrl\n }\n }\n `, + }), + }) + + if (!response.ok) { + throw new Error('Failed to fetch Linear user info') + } + + const { data } = await response.json() + const user = data?.viewer + if (!user) throw new Error('No user info returned from Linear') + + const now = new Date() + return { + id: user.id, + name: user.name, + email: user.email, + image: user.avatarUrl, + emailVerified: true, + createdAt: now, + updatedAt: now, + } + }, + }, ], }), // Only include the Stripe plugin in production diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index a247f00957d..c66eb8f79c2 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -97,6 +97,8 @@ export const env = createEnv({ HUBSPOT_CLIENT_ID: z.string().optional(), HUBSPOT_CLIENT_SECRET: z.string().optional(), DOCKER_BUILD: z.boolean().optional(), + LINEAR_CLIENT_ID: z.string().optional(), + LINEAR_CLIENT_SECRET: z.string().optional(), }, client: { diff --git a/apps/sim/lib/oauth.ts b/apps/sim/lib/oauth.ts index ba295661b05..aadafd6a0f1 100644 --- a/apps/sim/lib/oauth.ts +++ b/apps/sim/lib/oauth.ts @@ -11,6 +11,7 @@ import { GoogleIcon, GoogleSheetsIcon, JiraIcon, + LinearIcon, MicrosoftIcon, MicrosoftTeamsIcon, NotionIcon, @@ -35,6 +36,7 @@ export type OAuthProvider = | 'jira' | 'discord' | 'microsoft' + | 'linear' | string export type OAuthService = @@ -53,6 +55,7 @@ export type OAuthService = | 'discord' | 'microsoft-teams' | 'outlook' + | 'linear' // Define the interface for OAuth provider configuration export interface OAuthProviderConfig { id: OAuthProvider @@ -330,6 +333,23 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'notion', }, + linear: { + id: 'linear', + name: 'Linear', + icon: (props) => LinearIcon(props), + services: { + linear: { + id: 'linear', + name: 'Linear', + description: 'Manage issues and projects in Linear.', + providerId: 'linear', + icon: (props) => LinearIcon(props), + baseProviderIcon: (props) => LinearIcon(props), + scopes: [], // Add required scopes if Linear supports granular OAuth scopes + }, + }, + defaultService: 'linear', + }, } // Helper function to get a service by provider and service ID @@ -394,6 +414,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] return 'notion' } else if (provider === 'discord') { return 'discord' + } else if (provider === 'linear') { + return 'linear' } return providerConfig.defaultService diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index 45016f5d6ab..32e93416acc 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -70,4 +70,8 @@ export const linearCreateIssueTool: ToolConfig error.message || 'Failed to create Linear issue', + oauth: { + required: true, + provider: 'linear', + }, } diff --git a/apps/sim/tools/linear/read_issues.ts b/apps/sim/tools/linear/read_issues.ts index dc4cb24d0df..f0f9ae0dc9d 100644 --- a/apps/sim/tools/linear/read_issues.ts +++ b/apps/sim/tools/linear/read_issues.ts @@ -69,4 +69,8 @@ export const linearReadIssuesTool: ToolConfig error.message || 'Failed to fetch Linear issues', + oauth: { + required: true, + provider: 'linear', + }, } diff --git a/apps/sim/tools/linear/types.ts b/apps/sim/tools/linear/types.ts index cbc85166cb7..ddf48b59c23 100644 --- a/apps/sim/tools/linear/types.ts +++ b/apps/sim/tools/linear/types.ts @@ -14,6 +14,7 @@ export interface LinearReadIssuesParams { projectId?: string state?: string search?: string + accessToken?: string } export interface LinearCreateIssueParams { @@ -21,6 +22,7 @@ export interface LinearCreateIssueParams { projectId?: string title: string description?: string + accessToken?: string } export interface LinearReadIssuesResponse extends ToolResponse { From fe058dc3c06c18f70045403b44c5fc83bb4feb6d Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 11:16:26 +0530 Subject: [PATCH 06/17] feat(linear): add dynamic team and project selectors for Linear block --- .../components/linear-project-selector.tsx | 77 +++++++++++++++++++ .../components/linear-team-selector.tsx | 75 ++++++++++++++++++ .../project-selector-input.tsx | 51 +++++++++++- apps/sim/blocks/blocks/linear.ts | 34 ++++---- 4 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx create mode 100644 apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx new file mode 100644 index 00000000000..00bfd29bcf9 --- /dev/null +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx @@ -0,0 +1,77 @@ +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 + showPreview?: boolean +} + +export function LinearProjectSelector({ value, onChange, credential, teamId, label = 'Select Linear project', disabled = false }: LinearProjectSelectorProps) { + const [projects, setProjects] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!credential || !teamId) return + setLoading(true) + setError(null) + fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${credential}`, + }, + body: JSON.stringify({ + query: `query($teamId: String!) { team(id: $teamId) { projects { nodes { id name } } } }`, + variables: { teamId }, + }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.errors) { + setError(data.errors[0].message) + setProjects([]) + } else { + setProjects(data.data.team?.projects?.nodes || []) + } + }) + .catch((err) => { + setError(err.message) + setProjects([]) + }) + .finally(() => setLoading(false)) + }, [credential, teamId]) + + return ( +
{error}
} + + + ) +} \ No newline at end of file diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx new file mode 100644 index 00000000000..66ff836a994 --- /dev/null +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx @@ -0,0 +1,75 @@ +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 +} + +export function LinearTeamSelector({ value, onChange, credential, label = 'Select Linear team', disabled = false }: LinearTeamSelectorProps) { + const [teams, setTeams] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!credential) return + setLoading(true) + setError(null) + fetch('https://api.linear.app/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${credential}`, + }, + body: JSON.stringify({ + query: `query { teams { nodes { id name } } }`, + }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.errors) { + setError(data.errors[0].message) + setTeams([]) + } else { + setTeams(data.data.teams.nodes) + } + }) + .catch((err) => { + setError(err.message) + setTeams([]) + }) + .finally(() => setLoading(false)) + }, [credential]) + + return ( + + ) +} \ No newline at end of file diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx index 3e04d60f1e3..e00a568779b 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx @@ -6,6 +6,8 @@ import type { SubBlockConfig } from '@/blocks/types' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { type DiscordServerInfo, DiscordServerSelector } from './components/discord-server-selector' import { type JiraProjectInfo, JiraProjectSelector } from './components/jira-project-selector' +import { type LinearTeamInfo, LinearTeamSelector } from './components/linear-team-selector' +import { type LinearProjectInfo, LinearProjectSelector } from './components/linear-project-selector' interface ProjectSelectorInputProps { blockId: string @@ -27,6 +29,7 @@ export function ProjectSelectorInput({ // Get provider-specific values const provider = subBlock.provider || 'jira' const isDiscord = provider === 'discord' + const isLinear = provider === 'linear' // For Jira, we need the domain const domain = !isDiscord ? (getValue(blockId, 'domain') as string) || '' : '' @@ -41,7 +44,7 @@ export function ProjectSelectorInput({ }, [blockId, subBlock.id, getValue]) // Handle project selection - const handleProjectChange = (projectId: string, info?: JiraProjectInfo | DiscordServerInfo) => { + const handleProjectChange = (projectId: string, info?: JiraProjectInfo | DiscordServerInfo | LinearTeamInfo | LinearProjectInfo) => { setSelectedProjectId(projectId) setProjectInfo(info || null) setValue(blockId, subBlock.id, projectId) @@ -53,6 +56,9 @@ export function ProjectSelectorInput({ setValue(blockId, 'issueKey', '') } else if (provider === 'discord') { setValue(blockId, 'channelId', '') + } else if (provider === 'linear') { + setValue(blockId, 'credential', '') + setValue(blockId, 'teamId', '') } onProjectSelect?.(projectId) @@ -87,6 +93,49 @@ export function ProjectSelectorInput({ ) } + // Render Linear team/project selector if provider is linear + if (isLinear) { + return ( + + + +
+ {subBlock.id === 'teamId' ? ( + { + handleProjectChange(teamId, teamInfo) + }} + credential={getValue(blockId, 'credential') as string} + label={subBlock.placeholder || 'Select Linear team'} + disabled={disabled || !getValue(blockId, 'credential')} + showPreview={true} + /> + ) : ( + { + handleProjectChange(projectId, projectInfo) + }} + credential={getValue(blockId, 'credential') as string} + teamId={getValue(blockId, 'teamId') as string} + label={subBlock.placeholder || 'Select Linear project'} + disabled={disabled || !getValue(blockId, 'credential') || !getValue(blockId, 'teamId')} + showPreview={true} + /> + )} +
+
+ {!getValue(blockId, 'credential') && ( + +

Please select a Linear account first

+
+ )} +
+
+ ) + } + // Default to Jira project selector return ( diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 2cc0a11703c..c7c6a211253 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -1,16 +1,18 @@ import { LinearIcon } from '@/components/icons' -import type { LinearReadIssuesResponse } from '@/tools/linear/types' +import type { LinearReadIssuesResponse, LinearCreateIssueResponse } from '@/tools/linear/types' import type { BlockConfig } from '../types' -export const LinearBlock: BlockConfig = { +type LinearResponse = LinearReadIssuesResponse | LinearCreateIssueResponse + +export const LinearBlock: BlockConfig = { type: 'linear', name: 'Linear', description: 'Read and create issues in Linear', longDescription: 'Integrate with Linear to fetch, filter, and create issues directly from your workflow.', category: 'tools', - bgColor: '#5E6AD2', icon: LinearIcon, + bgColor: '#5E6AD2', subBlocks: [ { id: 'operation', @@ -18,7 +20,7 @@ export const LinearBlock: BlockConfig = { type: 'dropdown', layout: 'full', options: [ - { label: 'Read Issues', id: 'read' }, + { label: 'Read Issues', id: 'read-bulk' }, { label: 'Create Issue', id: 'write' }, ], }, @@ -29,7 +31,6 @@ export const LinearBlock: BlockConfig = { layout: 'full', provider: 'linear', serviceId: 'linear', - requiredScopes: [], placeholder: 'Select Linear account', }, { @@ -54,37 +55,37 @@ export const LinearBlock: BlockConfig = { id: 'state', title: 'Issue State', type: 'dropdown', + layout: 'full', options: ['Backlog', 'Todo', 'In Progress', 'Done', 'Canceled'], - description: 'Filter by issue state', - condition: { field: 'operation', value: ['read'] }, + condition: { field: 'operation', value: ['read-bulk'] }, }, { id: 'search', title: 'Search', type: 'short-input', - description: 'Search issues', - condition: { field: 'operation', value: ['read'] }, + layout: 'full', + condition: { field: 'operation', value: ['read-bulk'] }, }, { id: 'title', title: 'Title', type: 'short-input', - description: 'Title for new issue', + layout: 'full', condition: { field: 'operation', value: ['write'] }, }, { id: 'description', title: 'Description', type: 'long-input', - description: 'Description for new issue', + layout: 'full', condition: { field: 'operation', value: ['write'] }, }, + // Add assignee, label, priority, etc. as needed ], tools: { access: ['linear_read_issues', 'linear_create_issue'], config: { - tool: (params) => - params.operation === 'write' ? 'linear_create_issue' : 'linear_read_issues', + tool: (params) => (params.operation === 'write' ? 'linear_create_issue' : 'linear_read_issues'), params: (params) => { if (params.operation === 'write') { return { @@ -93,14 +94,17 @@ export const LinearBlock: BlockConfig = { projectId: params.projectId, title: params.title, description: params.description, + // Add assigneeId, labelIds, etc. if supported } } + // read-bulk return { credential: params.credential, teamId: params.teamId, projectId: params.projectId, state: params.state, search: params.search, + // Add assigneeId, labelId, etc. if supported } }, }, @@ -114,11 +118,13 @@ export const LinearBlock: BlockConfig = { search: { type: 'string', required: false }, title: { type: 'string', required: false }, description: { type: 'string', required: false }, + // Add assigneeId, labelIds, etc. as needed }, outputs: { response: { type: { - issues: 'json', + issues: 'json', // For read-bulk + issue: 'json', // For write }, }, }, From 3b83c63682c144ef0d944d0f06e23a677502070b Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 13:42:46 +0530 Subject: [PATCH 07/17] feat(linear): add backend API endpoints for teams and projects --- .../app/api/tools/linear/projects/route.ts | 70 +++++++++++++++++++ apps/sim/app/api/tools/linear/teams/route.ts | 55 +++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 apps/sim/app/api/tools/linear/projects/route.ts create mode 100644 apps/sim/app/api/tools/linear/teams/route.ts diff --git a/apps/sim/app/api/tools/linear/projects/route.ts b/apps/sim/app/api/tools/linear/projects/route.ts new file mode 100644 index 00000000000..8587eec8293 --- /dev/null +++ b/apps/sim/app/api/tools/linear/projects/route.ts @@ -0,0 +1,70 @@ +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' +import { LinearClient } from '@linear/sdk' + +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 = [] + + if (teamId) { + const team = await linearClient.team(teamId) + const projectsResult = await team.projects(); + projects = projectsResult.nodes.map((project: any) => ({ + id: project.id, + name: project.name, + })) + } else { + const allProjects = await linearClient.projects() + projects = allProjects.nodes.map((project: any) => ({ + 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 } + ) + } +} diff --git a/apps/sim/app/api/tools/linear/teams/route.ts b/apps/sim/app/api/tools/linear/teams/route.ts new file mode 100644 index 00000000000..c472ad64b4e --- /dev/null +++ b/apps/sim/app/api/tools/linear/teams/route.ts @@ -0,0 +1,55 @@ +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' +import { LinearClient } from '@linear/sdk' + +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: any) => ({ + 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 } + ) + } +} From e9a61944c67f6879631891683c6a7b3168140a71 Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 13:59:14 +0530 Subject: [PATCH 08/17] feat(linear): update UI components for Linear selectors and modal --- .../components/oauth-required-modal.tsx | 2 + .../components/linear-project-selector.tsx | 38 +++++++++------- .../components/linear-team-selector.tsx | 36 +++++++++------- .../project-selector-input.tsx | 43 ++++++++++++------- 4 files changed, 73 insertions(+), 46 deletions(-) diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 59fbd6798db..6e830026b91 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -106,6 +106,8 @@ const SCOPE_DESCRIPTIONS: Record = { 'messages.read': 'Read your Discord messages', guilds: 'Read your Discord guilds', 'guilds.members.read': 'Read your Discord guild members', + read: 'Read access to your Linear workspace', + write: 'Write access to your Linear workspace', } // Convert OAuth scope to user-friendly description diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx index 00bfd29bcf9..d6e8e1fd46c 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx @@ -1,5 +1,11 @@ import { useEffect, useState } from 'react' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' export interface LinearProjectInfo { id: string @@ -16,7 +22,14 @@ interface LinearProjectSelectorProps { showPreview?: boolean } -export function LinearProjectSelector({ value, onChange, credential, teamId, label = 'Select Linear project', disabled = false }: LinearProjectSelectorProps) { +export function LinearProjectSelector({ + value, + onChange, + credential, + teamId, + label = 'Select Linear project', + disabled = false, +}: LinearProjectSelectorProps) { const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -24,25 +37,18 @@ export function LinearProjectSelector({ value, onChange, credential, teamId, lab useEffect(() => { if (!credential || !teamId) return setLoading(true) - setError(null) - fetch('https://api.linear.app/graphql', { + fetch('/api/tools/linear/projects', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${credential}`, - }, - body: JSON.stringify({ - query: `query($teamId: String!) { team(id: $teamId) { projects { nodes { id name } } } }`, - variables: { teamId }, - }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credential, teamId }), }) .then((res) => res.json()) .then((data) => { - if (data.errors) { - setError(data.errors[0].message) + if (data.error) { + setError(data.error) setProjects([]) } else { - setProjects(data.data.team?.projects?.nodes || []) + setProjects(data.projects) } }) .catch((err) => { @@ -74,4 +80,4 @@ export function LinearProjectSelector({ value, onChange, credential, teamId, lab ) -} \ No newline at end of file +} diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx index 66ff836a994..979cb507dcf 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx @@ -1,5 +1,11 @@ import { useEffect, useState } from 'react' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' export interface LinearTeamInfo { id: string @@ -15,7 +21,13 @@ interface LinearTeamSelectorProps { showPreview?: boolean } -export function LinearTeamSelector({ value, onChange, credential, label = 'Select Linear team', disabled = false }: LinearTeamSelectorProps) { +export function LinearTeamSelector({ + value, + onChange, + credential, + label = 'Select Linear team', + disabled = false, +}: LinearTeamSelectorProps) { const [teams, setTeams] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -23,24 +35,18 @@ export function LinearTeamSelector({ value, onChange, credential, label = 'Selec useEffect(() => { if (!credential) return setLoading(true) - setError(null) - fetch('https://api.linear.app/graphql', { + fetch('/api/tools/linear/teams', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${credential}`, - }, - body: JSON.stringify({ - query: `query { teams { nodes { id name } } }`, - }), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ credential }), }) .then((res) => res.json()) .then((data) => { - if (data.errors) { - setError(data.errors[0].message) + if (data.error) { + setError(data.error) setTeams([]) } else { - setTeams(data.data.teams.nodes) + setTeams(data.teams) } }) .catch((err) => { @@ -72,4 +78,4 @@ export function LinearTeamSelector({ value, onChange, credential, label = 'Selec ) -} \ No newline at end of file +} diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx index e00a568779b..3db21fd86f3 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx @@ -6,8 +6,8 @@ import type { SubBlockConfig } from '@/blocks/types' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { type DiscordServerInfo, DiscordServerSelector } from './components/discord-server-selector' import { type JiraProjectInfo, JiraProjectSelector } from './components/jira-project-selector' -import { type LinearTeamInfo, LinearTeamSelector } from './components/linear-team-selector' import { type LinearProjectInfo, LinearProjectSelector } from './components/linear-project-selector' +import { type LinearTeamInfo, LinearTeamSelector } from './components/linear-team-selector' interface ProjectSelectorInputProps { blockId: string @@ -44,7 +44,10 @@ export function ProjectSelectorInput({ }, [blockId, subBlock.id, getValue]) // Handle project selection - const handleProjectChange = (projectId: string, info?: JiraProjectInfo | DiscordServerInfo | LinearTeamInfo | LinearProjectInfo) => { + const handleProjectChange = ( + projectId: string, + info?: JiraProjectInfo | DiscordServerInfo | LinearTeamInfo | LinearProjectInfo + ) => { setSelectedProjectId(projectId) setProjectInfo(info || null) setValue(blockId, subBlock.id, projectId) @@ -57,8 +60,10 @@ export function ProjectSelectorInput({ } else if (provider === 'discord') { setValue(blockId, 'channelId', '') } else if (provider === 'linear') { - setValue(blockId, 'credential', '') - setValue(blockId, 'teamId', '') + if (subBlock.id === 'teamId') { + setValue(blockId, 'teamId', projectId) + setValue(blockId, 'projectId', '') // Optionally clear project selection + } } onProjectSelect?.(projectId) @@ -112,17 +117,25 @@ export function ProjectSelectorInput({ showPreview={true} /> ) : ( - { - handleProjectChange(projectId, projectInfo) - }} - credential={getValue(blockId, 'credential') as string} - teamId={getValue(blockId, 'teamId') as string} - label={subBlock.placeholder || 'Select Linear project'} - disabled={disabled || !getValue(blockId, 'credential') || !getValue(blockId, 'teamId')} - showPreview={true} - /> + (() => { + const credential = getValue(blockId, 'credential') as string + const teamId = getValue(blockId, 'teamId') as string + const isDisabled = disabled || !credential || !teamId + console.log('ProjectSelector:', { credential, teamId, disabled: isDisabled }) + return ( + { + handleProjectChange(projectId, projectInfo) + }} + credential={credential} + teamId={teamId} + label={subBlock.placeholder || 'Select Linear project'} + disabled={isDisabled} + showPreview={true} + /> + ) + })() )} From 23abcee49e476ff1367c5fbcceac2777ed71928c Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 14:00:43 +0530 Subject: [PATCH 09/17] refactor(linear): update create/read issue tools and types --- apps/sim/tools/linear/create_issue.ts | 119 +++++++++++++++----------- apps/sim/tools/linear/read_issues.ts | 20 ++--- apps/sim/tools/linear/types.ts | 2 - 3 files changed, 77 insertions(+), 64 deletions(-) diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index 32e93416acc..ccb16aaaa6d 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -1,28 +1,31 @@ import type { ToolConfig } from '../types' import type { LinearCreateIssueParams, LinearCreateIssueResponse } from './types' -export const linearCreateIssueTool: ToolConfig = - { - id: 'linear_create_issue', - name: 'Linear Issue Writer', - description: 'Create a new issue in Linear', - version: '1.0.0', - params: { - teamId: { type: 'string', required: true, description: 'Linear team ID' }, - projectId: { type: 'string', required: false, description: 'Linear project ID' }, - title: { type: 'string', required: true, description: 'Issue title' }, - description: { type: 'string', required: false, description: 'Issue description' }, - }, - request: { - url: 'https://api.linear.app/graphql', - method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken || ''}`, - }), - body: (params) => ({ - query: ` - mutation CreateIssue($teamId: String!, $projectId: String, $title: String!, $description: String) { +export const linearCreateIssueTool: ToolConfig = { + id: 'linear_create_issue', + name: 'Linear Issue Writer', + description: 'Create a new issue in Linear', + version: '1.0.0', + oauth: { + required: true, + provider: 'linear', + }, + params: { + teamId: { type: 'string', required: true, description: 'Linear team ID' }, + projectId: { type: 'string', required: false, description: 'Linear project ID' }, + title: { type: 'string', required: true, description: 'Issue title' }, + description: { type: 'string', required: false, description: 'Issue description' }, + }, + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || ''}`, + }), + body: (params) => ({ + query: ` + mutation CreateIssue($teamId: ID!, $projectId: ID, $title: String!, $description: String) { issueCreate( input: { teamId: $teamId @@ -42,36 +45,54 @@ export const linearCreateIssueTool: ToolConfig { - const data = await response.json() - if (data.errors) { - return { success: false, output: { issue: null }, error: data.errors[0].message } - } + variables: { + teamId: params.teamId, + projectId: params.projectId, + title: params.title, + description: params.description, + }, + }), + }, + transformResponse: async (response) => { + const data = await response.json() + if (data.errors) { return { - success: true, + success: false, output: { issue: { - id: data.data.issueCreate.issue.id, - title: data.data.issueCreate.issue.title, - description: data.data.issueCreate.issue.description, - state: data.data.issueCreate.issue.state?.name, - teamId: data.data.issueCreate.issue.team?.id, - projectId: data.data.issueCreate.issue.project?.id, + id: '', + title: '', + description: '', + state: '', + teamId: '', + projectId: '', }, }, + error: data.errors[0].message, } - }, - transformError: (error) => error.message || 'Failed to create Linear issue', - oauth: { - required: true, - provider: 'linear', - }, - } + } + return { + success: true, + output: { + issue: data.data.issueCreate.issue + ? { + id: data.data.issueCreate.issue.id, + title: data.data.issueCreate.issue.title, + description: data.data.issueCreate.issue.description, + state: data.data.issueCreate.issue.state?.name, + teamId: data.data.issueCreate.issue.team?.id, + projectId: data.data.issueCreate.issue.project?.id, + } + : { + id: '', + title: '', + description: '', + state: '', + teamId: '', + projectId: '', + }, + }, + } + }, + transformError: (error) => error.message || 'Failed to create Linear issue', +} diff --git a/apps/sim/tools/linear/read_issues.ts b/apps/sim/tools/linear/read_issues.ts index f0f9ae0dc9d..068c6834935 100644 --- a/apps/sim/tools/linear/read_issues.ts +++ b/apps/sim/tools/linear/read_issues.ts @@ -6,11 +6,13 @@ export const linearReadIssuesTool: ToolConfig ({ query: ` - query Issues($teamId: String, $projectId: String, $state: String, $search: String) { + query Issues($teamId: ID, $projectId: ID) { issues( filter: { - team: { id: $teamId } - project: { id: $projectId } - state: { name: { eq: $state } } - search: $search + team: { id: { eq: $teamId } } + project: { id: { eq: $projectId } } } ) { nodes { @@ -44,8 +44,6 @@ export const linearReadIssuesTool: ToolConfig error.message || 'Failed to fetch Linear issues', - oauth: { - required: true, - provider: 'linear', - }, } diff --git a/apps/sim/tools/linear/types.ts b/apps/sim/tools/linear/types.ts index ddf48b59c23..c8b2d7734ef 100644 --- a/apps/sim/tools/linear/types.ts +++ b/apps/sim/tools/linear/types.ts @@ -12,8 +12,6 @@ export interface LinearIssue { export interface LinearReadIssuesParams { teamId?: string projectId?: string - state?: string - search?: string accessToken?: string } From 40d9641b775a1f543870a494c255ff1075491782 Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 14:01:24 +0530 Subject: [PATCH 10/17] chore(linear): update block config for Linear integration --- apps/sim/blocks/blocks/linear.ts | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index c7c6a211253..2d35dba4b76 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -1,5 +1,5 @@ import { LinearIcon } from '@/components/icons' -import type { LinearReadIssuesResponse, LinearCreateIssueResponse } from '@/tools/linear/types' +import type { LinearCreateIssueResponse, LinearReadIssuesResponse } from '@/tools/linear/types' import type { BlockConfig } from '../types' type LinearResponse = LinearReadIssuesResponse | LinearCreateIssueResponse @@ -31,6 +31,7 @@ export const LinearBlock: BlockConfig = { layout: 'full', provider: 'linear', serviceId: 'linear', + requiredScopes: ['read', 'write'], placeholder: 'Select Linear account', }, { @@ -51,21 +52,6 @@ export const LinearBlock: BlockConfig = { serviceId: 'linear', placeholder: 'Select a project', }, - { - id: 'state', - title: 'Issue State', - type: 'dropdown', - layout: 'full', - options: ['Backlog', 'Todo', 'In Progress', 'Done', 'Canceled'], - condition: { field: 'operation', value: ['read-bulk'] }, - }, - { - id: 'search', - title: 'Search', - type: 'short-input', - layout: 'full', - condition: { field: 'operation', value: ['read-bulk'] }, - }, { id: 'title', title: 'Title', @@ -85,7 +71,8 @@ export const LinearBlock: BlockConfig = { tools: { access: ['linear_read_issues', 'linear_create_issue'], config: { - tool: (params) => (params.operation === 'write' ? 'linear_create_issue' : 'linear_read_issues'), + tool: (params) => + params.operation === 'write' ? 'linear_create_issue' : 'linear_read_issues', params: (params) => { if (params.operation === 'write') { return { @@ -102,8 +89,6 @@ export const LinearBlock: BlockConfig = { credential: params.credential, teamId: params.teamId, projectId: params.projectId, - state: params.state, - search: params.search, // Add assigneeId, labelId, etc. if supported } }, @@ -114,8 +99,6 @@ export const LinearBlock: BlockConfig = { credential: { type: 'string', required: true }, teamId: { type: 'string', required: false }, projectId: { type: 'string', required: false }, - state: { type: 'string', required: false }, - search: { type: 'string', required: false }, title: { type: 'string', required: false }, description: { type: 'string', required: false }, // Add assigneeId, labelIds, etc. as needed @@ -124,7 +107,7 @@ export const LinearBlock: BlockConfig = { response: { type: { issues: 'json', // For read-bulk - issue: 'json', // For write + issue: 'json', // For write }, }, }, From 84c2d6bf5a064bf665b46de48bc19b4d47c8c97f Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 14:01:45 +0530 Subject: [PATCH 11/17] fix(auth): update auth and oauth logic for Linear --- apps/sim/lib/auth.ts | 2 +- apps/sim/lib/oauth.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 9260bc013ab..90a1aa4020e 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -783,7 +783,7 @@ export const auth = betterAuth({ clientSecret: env.LINEAR_CLIENT_SECRET as string, authorizationUrl: 'https://linear.app/oauth/authorize', tokenUrl: 'https://api.linear.app/oauth/token', - scopes: [], // Add required scopes if Linear supports them + scopes: ['read', 'write'], responseType: 'code', accessType: 'offline', prompt: 'consent', diff --git a/apps/sim/lib/oauth.ts b/apps/sim/lib/oauth.ts index aadafd6a0f1..8cf52e9348e 100644 --- a/apps/sim/lib/oauth.ts +++ b/apps/sim/lib/oauth.ts @@ -345,7 +345,7 @@ export const OAUTH_PROVIDERS: Record = { providerId: 'linear', icon: (props) => LinearIcon(props), baseProviderIcon: (props) => LinearIcon(props), - scopes: [], // Add required scopes if Linear supports granular OAuth scopes + scopes: ['read', 'write'], }, }, defaultService: 'linear', From 7dd3b3515d0648b52882ef3514a3cacd54961076 Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 15:42:34 +0530 Subject: [PATCH 12/17] minor fix --- apps/sim/components/icons.tsx | 18 +++++++----------- apps/sim/tools/linear/create_issue.ts | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 7be99c905de..b4ba6ad8fa5 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2281,20 +2281,16 @@ export function JiraIcon(props: SVGProps) { export function LinearIcon(props: React.SVGProps) { return ( - ) diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index ccb16aaaa6d..00da27683eb 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -25,7 +25,7 @@ export const linearCreateIssueTool: ToolConfig ({ query: ` - mutation CreateIssue($teamId: ID!, $projectId: ID, $title: String!, $description: String) { + mutation CreateIssue($teamId: String!, $projectId: String, $title: String!, $description: String) { issueCreate( input: { teamId: $teamId From d744f7ce8b3618b2c7b672019a12fe99e78d788c Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 22:34:26 +0530 Subject: [PATCH 13/17] improvement[linear]: require teamId and projectId for all tools and types --- apps/sim/blocks/blocks/linear.ts | 15 +++++---------- apps/sim/tools/linear/create_issue.ts | 4 ++-- apps/sim/tools/linear/read_issues.ts | 6 +++--- apps/sim/tools/linear/types.ts | 10 +++++----- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 2d35dba4b76..1524bc94b3a 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -66,7 +66,6 @@ export const LinearBlock: BlockConfig = { layout: 'full', condition: { field: 'operation', value: ['write'] }, }, - // Add assignee, label, priority, etc. as needed ], tools: { access: ['linear_read_issues', 'linear_create_issue'], @@ -81,15 +80,12 @@ export const LinearBlock: BlockConfig = { projectId: params.projectId, title: params.title, description: params.description, - // Add assigneeId, labelIds, etc. if supported } } - // read-bulk return { credential: params.credential, teamId: params.teamId, projectId: params.projectId, - // Add assigneeId, labelId, etc. if supported } }, }, @@ -97,17 +93,16 @@ export const LinearBlock: BlockConfig = { inputs: { operation: { type: 'string', required: true }, credential: { type: 'string', required: true }, - teamId: { type: 'string', required: false }, - projectId: { type: 'string', required: false }, - title: { type: 'string', required: false }, + teamId: { type: 'string', required: true }, + projectId: { type: 'string', required: true }, + title: { type: 'string', required: true }, description: { type: 'string', required: false }, - // Add assigneeId, labelIds, etc. as needed }, outputs: { response: { type: { - issues: 'json', // For read-bulk - issue: 'json', // For write + issues: 'json', + issue: 'json', }, }, }, diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index 00da27683eb..49cc17a506b 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -12,7 +12,7 @@ export const linearCreateIssueTool: ToolConfig ({ query: ` - mutation CreateIssue($teamId: String!, $projectId: String, $title: String!, $description: String) { + mutation CreateIssue($teamId: String!, $projectId: String!, $title: String!, $description: String) { issueCreate( input: { teamId: $teamId diff --git a/apps/sim/tools/linear/read_issues.ts b/apps/sim/tools/linear/read_issues.ts index 068c6834935..9d4b7f43985 100644 --- a/apps/sim/tools/linear/read_issues.ts +++ b/apps/sim/tools/linear/read_issues.ts @@ -11,8 +11,8 @@ export const linearReadIssuesTool: ToolConfig ({ query: ` - query Issues($teamId: ID, $projectId: ID) { + query Issues($teamId: ID!, $projectId: ID!) { issues( filter: { team: { id: { eq: $teamId } } diff --git a/apps/sim/tools/linear/types.ts b/apps/sim/tools/linear/types.ts index c8b2d7734ef..d91c05b9b44 100644 --- a/apps/sim/tools/linear/types.ts +++ b/apps/sim/tools/linear/types.ts @@ -5,19 +5,19 @@ export interface LinearIssue { title: string description?: string state?: string - teamId?: string - projectId?: string + teamId: string + projectId: string } export interface LinearReadIssuesParams { - teamId?: string - projectId?: string + teamId: string + projectId: string accessToken?: string } export interface LinearCreateIssueParams { teamId: string - projectId?: string + projectId: string title: string description?: string accessToken?: string From 8db78a172edefce92fdb443e9168d6b19da6752d Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 22:36:35 +0530 Subject: [PATCH 14/17] style[lint]: fix code style and lint errors --- .../app/api/tools/linear/projects/route.ts | 4 +- apps/sim/app/api/tools/linear/teams/route.ts | 2 +- apps/sim/components/icons.tsx | 14 +- apps/sim/tools/linear/create_issue.ts | 135 +++++++++--------- 4 files changed, 78 insertions(+), 77 deletions(-) diff --git a/apps/sim/app/api/tools/linear/projects/route.ts b/apps/sim/app/api/tools/linear/projects/route.ts index 8587eec8293..1b70a06f6b3 100644 --- a/apps/sim/app/api/tools/linear/projects/route.ts +++ b/apps/sim/app/api/tools/linear/projects/route.ts @@ -1,8 +1,8 @@ +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' -import { LinearClient } from '@linear/sdk' export const dynamic = 'force-dynamic' @@ -42,7 +42,7 @@ export async function POST(request: Request) { if (teamId) { const team = await linearClient.team(teamId) - const projectsResult = await team.projects(); + const projectsResult = await team.projects() projects = projectsResult.nodes.map((project: any) => ({ id: project.id, name: project.name, diff --git a/apps/sim/app/api/tools/linear/teams/route.ts b/apps/sim/app/api/tools/linear/teams/route.ts index c472ad64b4e..2715d7c3884 100644 --- a/apps/sim/app/api/tools/linear/teams/route.ts +++ b/apps/sim/app/api/tools/linear/teams/route.ts @@ -1,8 +1,8 @@ +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' -import { LinearClient } from '@linear/sdk' export const dynamic = 'force-dynamic' diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index b4ba6ad8fa5..a04745c2906 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2282,15 +2282,15 @@ export function LinearIcon(props: React.SVGProps) { return ( ) diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index 49cc17a506b..0899ba1a5ee 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -1,30 +1,31 @@ import type { ToolConfig } from '../types' import type { LinearCreateIssueParams, LinearCreateIssueResponse } from './types' -export const linearCreateIssueTool: ToolConfig = { - id: 'linear_create_issue', - name: 'Linear Issue Writer', - description: 'Create a new issue in Linear', - version: '1.0.0', - oauth: { - required: true, - provider: 'linear', - }, - params: { - teamId: { type: 'string', required: true, description: 'Linear team ID' }, - projectId: { type: 'string', required: true, description: 'Linear project ID' }, - title: { type: 'string', required: true, description: 'Issue title' }, - description: { type: 'string', required: false, description: 'Issue description' }, - }, - request: { - url: 'https://api.linear.app/graphql', - method: 'POST', - headers: (params) => ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken || ''}`, - }), - body: (params) => ({ - query: ` +export const linearCreateIssueTool: ToolConfig = + { + id: 'linear_create_issue', + name: 'Linear Issue Writer', + description: 'Create a new issue in Linear', + version: '1.0.0', + oauth: { + required: true, + provider: 'linear', + }, + params: { + teamId: { type: 'string', required: true, description: 'Linear team ID' }, + projectId: { type: 'string', required: true, description: 'Linear project ID' }, + title: { type: 'string', required: true, description: 'Issue title' }, + description: { type: 'string', required: false, description: 'Issue description' }, + }, + request: { + url: 'https://api.linear.app/graphql', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken || ''}`, + }), + body: (params) => ({ + query: ` mutation CreateIssue($teamId: String!, $projectId: String!, $title: String!, $description: String) { issueCreate( input: { @@ -45,45 +46,21 @@ export const linearCreateIssueTool: ToolConfig { - const data = await response.json() - if (data.errors) { - return { - success: false, - output: { - issue: { - id: '', - title: '', - description: '', - state: '', - teamId: '', - projectId: '', - }, + variables: { + teamId: params.teamId, + projectId: params.projectId, + title: params.title, + description: params.description, }, - error: data.errors[0].message, - } - } - return { - success: true, - output: { - issue: data.data.issueCreate.issue - ? { - id: data.data.issueCreate.issue.id, - title: data.data.issueCreate.issue.title, - description: data.data.issueCreate.issue.description, - state: data.data.issueCreate.issue.state?.name, - teamId: data.data.issueCreate.issue.team?.id, - projectId: data.data.issueCreate.issue.project?.id, - } - : { + }), + }, + transformResponse: async (response) => { + const data = await response.json() + if (data.errors) { + return { + success: false, + output: { + issue: { id: '', title: '', description: '', @@ -91,8 +68,32 @@ export const linearCreateIssueTool: ToolConfig error.message || 'Failed to create Linear issue', -} + }, + error: data.errors[0].message, + } + } + return { + success: true, + output: { + issue: data.data.issueCreate.issue + ? { + id: data.data.issueCreate.issue.id, + title: data.data.issueCreate.issue.title, + description: data.data.issueCreate.issue.description, + state: data.data.issueCreate.issue.state?.name, + teamId: data.data.issueCreate.issue.team?.id, + projectId: data.data.issueCreate.issue.project?.id, + } + : { + id: '', + title: '', + description: '', + state: '', + teamId: '', + projectId: '', + }, + }, + } + }, + transformError: (error) => error.message || 'Failed to create Linear issue', + } From b370609443a1b84bd465a89f077e7921ed270c6a Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Wed, 28 May 2025 23:19:50 +0530 Subject: [PATCH 15/17] chore(linear): install @linear/sdk package --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index de944b30649..40240f5164d 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "tailwindcss": "3.4.1" }, "dependencies": { + "@linear/sdk": "40.0.0", "@t3-oss/env-nextjs": "0.13.4", "@vercel/analytics": "1.5.0", "remark-gfm": "4.0.1" From 7483381e98281816f53aa2a24ce28e0ffb21c5bc Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Thu, 29 May 2025 12:42:52 +0530 Subject: [PATCH 16/17] fix[linear]: address greptile-apps feedback for type safety and error handling --- .../app/api/tools/linear/projects/route.ts | 21 ++-- apps/sim/app/api/tools/linear/teams/route.ts | 3 +- .../components/linear-project-selector.tsx | 13 ++- .../components/linear-team-selector.tsx | 4 + .../project-selector-input.tsx | 6 +- apps/sim/blocks/blocks/linear.ts | 4 +- apps/sim/blocks/registry.ts | 2 +- apps/sim/lib/auth.ts | 5 +- apps/sim/tools/linear/create_issue.ts | 96 ++++++++++--------- apps/sim/tools/linear/read_issues.ts | 49 +++++++--- 10 files changed, 122 insertions(+), 81 deletions(-) diff --git a/apps/sim/app/api/tools/linear/projects/route.ts b/apps/sim/app/api/tools/linear/projects/route.ts index 1b70a06f6b3..f8920eddfc4 100644 --- a/apps/sim/app/api/tools/linear/projects/route.ts +++ b/apps/sim/app/api/tools/linear/projects/route.ts @@ -1,3 +1,4 @@ +import type { Project } from '@linear/sdk' import { LinearClient } from '@linear/sdk' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' @@ -40,20 +41,12 @@ export async function POST(request: Request) { const linearClient = new LinearClient({ accessToken }) let projects = [] - if (teamId) { - const team = await linearClient.team(teamId) - const projectsResult = await team.projects() - projects = projectsResult.nodes.map((project: any) => ({ - id: project.id, - name: project.name, - })) - } else { - const allProjects = await linearClient.projects() - projects = allProjects.nodes.map((project: any) => ({ - id: project.id, - name: project.name, - })) - } + 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 }) diff --git a/apps/sim/app/api/tools/linear/teams/route.ts b/apps/sim/app/api/tools/linear/teams/route.ts index 2715d7c3884..232cfa45da1 100644 --- a/apps/sim/app/api/tools/linear/teams/route.ts +++ b/apps/sim/app/api/tools/linear/teams/route.ts @@ -1,3 +1,4 @@ +import type { Team } from '@linear/sdk' import { LinearClient } from '@linear/sdk' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' @@ -39,7 +40,7 @@ export async function POST(request: Request) { const linearClient = new LinearClient({ accessToken }) const teamsResult = await linearClient.teams() - const teams = teamsResult.nodes.map((team: any) => ({ + const teams = teamsResult.nodes.map((team: Team) => ({ id: team.id, name: team.name, })) diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx index d6e8e1fd46c..4c156a251a6 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-project-selector.tsx @@ -19,7 +19,6 @@ interface LinearProjectSelectorProps { teamId: string label?: string disabled?: boolean - showPreview?: boolean } export function LinearProjectSelector({ @@ -36,13 +35,21 @@ export function LinearProjectSelector({ 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((res) => res.json()) + .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) @@ -52,10 +59,12 @@ export function LinearProjectSelector({ } }) .catch((err) => { + if (err.name === 'AbortError') return setError(err.message) setProjects([]) }) .finally(() => setLoading(false)) + return () => controller.abort() }, [credential, teamId]) return ( diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx index 979cb507dcf..42bb5b969b1 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx @@ -34,11 +34,13 @@ export function LinearTeamSelector({ 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) => res.json()) .then((data) => { @@ -50,10 +52,12 @@ export function LinearTeamSelector({ } }) .catch((err) => { + if (err.name === 'AbortError') return setError(err.message) setTeams([]) }) .finally(() => setLoading(false)) + return () => controller.abort() }, [credential]) return ( diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx index 3db21fd86f3..59dcabaa115 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx @@ -62,7 +62,9 @@ export function ProjectSelectorInput({ } else if (provider === 'linear') { if (subBlock.id === 'teamId') { setValue(blockId, 'teamId', projectId) - setValue(blockId, 'projectId', '') // Optionally clear project selection + setValue(blockId, 'projectId', '') + } else if (subBlock.id === 'projectId') { + setValue(blockId, 'projectId', projectId) } } @@ -121,7 +123,6 @@ export function ProjectSelectorInput({ const credential = getValue(blockId, 'credential') as string const teamId = getValue(blockId, 'teamId') as string const isDisabled = disabled || !credential || !teamId - console.log('ProjectSelector:', { credential, teamId, disabled: isDisabled }) return ( ) })() diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 1524bc94b3a..f4eacc5c891 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -20,7 +20,7 @@ export const LinearBlock: BlockConfig = { type: 'dropdown', layout: 'full', options: [ - { label: 'Read Issues', id: 'read-bulk' }, + { label: 'Read Issues', id: 'read' }, { label: 'Create Issue', id: 'write' }, ], }, @@ -95,7 +95,7 @@ export const LinearBlock: BlockConfig = { credential: { type: 'string', required: true }, teamId: { type: 'string', required: true }, projectId: { type: 'string', required: true }, - title: { type: 'string', required: true }, + title: { type: 'string', required: false }, description: { type: 'string', required: false }, }, outputs: { diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 0d38fcfc052..448fcce52b9 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -89,6 +89,7 @@ export const registry: Record = { image_generator: ImageGeneratorBlock, jina: JinaBlock, jira: JiraBlock, + linear: LinearBlock, linkup: LinkupBlock, mem0: Mem0Block, mistral_parse: MistralParseBlock, @@ -117,7 +118,6 @@ export const registry: Record = { whatsapp: WhatsAppBlock, x: XBlock, youtube: YouTubeBlock, - linear: LinearBlock, } // Helper functions to access the registry diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 90a1aa4020e..8b5233a54da 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -805,7 +805,10 @@ export const auth = betterAuth({ throw new Error('Failed to fetch Linear user info') } - const { data } = await response.json() + const data = await response.json() + if (data.errors && data.errors.length > 0) { + throw new Error(data.errors.map((e: any) => e.message).join('; ')) + } const user = data?.viewer if (!user) throw new Error('No user info returned from Linear') diff --git a/apps/sim/tools/linear/create_issue.ts b/apps/sim/tools/linear/create_issue.ts index 0899ba1a5ee..28ea96a8798 100644 --- a/apps/sim/tools/linear/create_issue.ts +++ b/apps/sim/tools/linear/create_issue.ts @@ -20,12 +20,21 @@ export const linearCreateIssueTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken || ''}`, - }), - body: (params) => ({ - query: ` + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, + body: (params) => { + if (!params.title || !params.title.trim()) { + throw new Error('Title is required to create a Linear issue') + } + return { + query: ` mutation CreateIssue($teamId: String!, $projectId: String!, $title: String!, $description: String) { issueCreate( input: { @@ -46,54 +55,49 @@ export const linearCreateIssueTool: ToolConfig { const data = await response.json() - if (data.errors) { - return { - success: false, - output: { - issue: { - id: '', - title: '', - description: '', - state: '', - teamId: '', - projectId: '', - }, - }, - error: data.errors[0].message, - } + const issue = data.data.issueCreate.issue + if (!issue) { + throw new Error('Failed to create issue: No issue returned from Linear API') } return { success: true, output: { - issue: data.data.issueCreate.issue - ? { - id: data.data.issueCreate.issue.id, - title: data.data.issueCreate.issue.title, - description: data.data.issueCreate.issue.description, - state: data.data.issueCreate.issue.state?.name, - teamId: data.data.issueCreate.issue.team?.id, - projectId: data.data.issueCreate.issue.project?.id, - } - : { - id: '', - title: '', - description: '', - state: '', - teamId: '', - projectId: '', - }, + issue: { + id: issue.id, + title: issue.title, + description: issue.description, + state: issue.state?.name, + teamId: issue.team?.id, + projectId: issue.project?.id, + }, }, } }, - transformError: (error) => error.message || 'Failed to create Linear issue', + transformError: (error) => { + if (error instanceof Error) { + return error.message + } + + if (typeof error === 'object' && error !== null) { + if (error.error) { + return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) + } + if (error.message) { + return error.message + } + } + + return 'Failed to create Linear issue' + }, } diff --git a/apps/sim/tools/linear/read_issues.ts b/apps/sim/tools/linear/read_issues.ts index 9d4b7f43985..38562188bd3 100644 --- a/apps/sim/tools/linear/read_issues.ts +++ b/apps/sim/tools/linear/read_issues.ts @@ -1,5 +1,5 @@ import type { ToolConfig } from '../types' -import type { LinearReadIssuesParams, LinearReadIssuesResponse } from './types' +import type { LinearIssue, LinearReadIssuesParams, LinearReadIssuesResponse } from './types' export const linearReadIssuesTool: ToolConfig = { id: 'linear_read_issues', @@ -17,10 +17,15 @@ export const linearReadIssuesTool: ToolConfig ({ - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken || ''}`, - }), + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Linear API request') + } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + } + }, body: (params) => ({ query: ` query Issues($teamId: ID!, $projectId: ID!) { @@ -50,21 +55,43 @@ export const linearReadIssuesTool: ToolConfig { const data = await response.json() if (data.errors) { - return { success: false, output: { issues: [] }, error: data.errors[0].message } + return { + success: false, + output: { issues: [] }, + error: data.errors.map((e: any) => e.message).join('; '), + } } return { success: true, output: { - issues: data.data.issues.nodes.map((issue: any) => ({ + issues: (data.data.issues.nodes as LinearIssue[]).map((issue) => ({ id: issue.id, title: issue.title, description: issue.description, - state: issue.state?.name, - teamId: issue.team?.id, - projectId: issue.project?.id, + state: issue.state, + teamId: issue.teamId, + projectId: issue.projectId, })), }, } }, - transformError: (error) => error.message || 'Failed to fetch Linear issues', + transformError: (error) => { + // If it's an Error instance with a message, use that + if (error instanceof Error) { + return error.message + } + + // If it's an object with an error or message property + if (typeof error === 'object' && error !== null) { + if (error.error) { + return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) + } + if (error.message) { + return error.message + } + } + + // Default fallback message + return 'Failed to fetch Linear issues' + }, } From 211d5b1655fe3fdafd63313f72dfdb162397578c Mon Sep 17 00:00:00 2001 From: sriram2k4 Date: Thu, 29 May 2025 12:56:14 +0530 Subject: [PATCH 17/17] fix[linear]: handle teams API response errors --- .../project-selector/components/linear-team-selector.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx index 42bb5b969b1..13178e5366d 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/linear-team-selector.tsx @@ -42,7 +42,10 @@ export function LinearTeamSelector({ body: JSON.stringify({ credential }), signal: controller.signal, }) - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`) + return res.json() + }) .then((data) => { if (data.error) { setError(data.error)