Skip to content

Commit 063bd61

Browse files
icecrasher321Sg312
andauthored
fix(jira): issue selector inf render (#1693)
* improvement(copilot): version update, edit previous messages, revert logic, model selector, observability, add haiku 4.5 (#1688) * Add exa to search online tool * Larger font size * Copilot UI improvements * Fix models options * Add haiku 4.5 to copilot * Model ui for haiku * Fix lint * Revert * Only allow one revert to message * Clear diff on revert * Fix welcome screen flash * Add focus onto the user input box when clicked * Fix grayout of new stream on old edit message * Lint * Make edit message submit smoother * Allow message sent while streaming * Revert popup improvements: gray out stuff below, show cursor on revert * Fix lint * Improve chat history dropdown * Improve get block metadata tool * Update update cost route * Fix env * Context usage endpoint * Make chat history scrollable * Fix lint * Copilot revert popup updates * Fix lint * Fix tests and lint * Add summary tool * fix(jira): issue selector inf render * fix * fixed * fix endpoints * fix * more detailed error * fix endpoint * revert environment.ts file --------- Co-authored-by: Siddharth Ganesan <[email protected]>
1 parent 9132cd2 commit 063bd61

File tree

4 files changed

+104
-76
lines changed

4 files changed

+104
-76
lines changed

apps/sim/app/api/tools/jira/issues/route.ts

Lines changed: 62 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -45,45 +45,50 @@ export async function POST(request: Request) {
4545
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
4646
}
4747

48-
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/bulkfetch`
49-
50-
const requestBody = {
51-
expand: ['names'],
52-
fields: ['summary', 'status', 'assignee', 'updated', 'project'],
53-
fieldsByKeys: false,
54-
issueIdsOrKeys: issueKeys,
55-
properties: [],
56-
}
57-
58-
const requestConfig = {
59-
method: 'POST',
48+
// Use search/jql endpoint (GET) with URL parameters
49+
const jql = `issueKey in (${issueKeys.map((k: string) => k.trim()).join(',')})`
50+
const params = new URLSearchParams({
51+
jql,
52+
fields: 'summary,status,assignee,updated,project',
53+
maxResults: String(Math.min(issueKeys.length, 100)),
54+
})
55+
const searchUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`
56+
57+
const response = await fetch(searchUrl, {
58+
method: 'GET',
6059
headers: {
6160
Authorization: `Bearer ${accessToken}`,
6261
Accept: 'application/json',
63-
'Content-Type': 'application/json',
6462
},
65-
body: JSON.stringify(requestBody),
66-
}
67-
68-
const response = await fetch(url, requestConfig)
63+
})
6964

7065
if (!response.ok) {
7166
logger.error(`Jira API error: ${response.status} ${response.statusText}`)
7267
const errorMessage = await createErrorResponse(
7368
response,
7469
`Failed to fetch Jira issues (${response.status})`
7570
)
71+
if (response.status === 401 || response.status === 403) {
72+
return NextResponse.json(
73+
{
74+
error: errorMessage,
75+
authRequired: true,
76+
requiredScopes: ['read:jira-work', 'read:project:jira'],
77+
},
78+
{ status: response.status }
79+
)
80+
}
7681
return NextResponse.json({ error: errorMessage }, { status: response.status })
7782
}
7883

7984
const data = await response.json()
80-
const issues = (data.issues || []).map((issue: any) => ({
81-
id: issue.key,
82-
name: issue.fields.summary,
85+
const issues = (data.issues || []).map((it: any) => ({
86+
id: it.key,
87+
name: it.fields?.summary || it.key,
8388
mimeType: 'jira/issue',
84-
url: `https://${domain}/browse/${issue.key}`,
85-
modifiedTime: issue.fields.updated,
86-
webViewLink: `https://${domain}/browse/${issue.key}`,
89+
url: `https://${domain}/browse/${it.key}`,
90+
modifiedTime: it.fields?.updated,
91+
webViewLink: `https://${domain}/browse/${it.key}`,
8792
}))
8893

8994
return NextResponse.json({ issues, cloudId })
@@ -138,46 +143,44 @@ export async function GET(request: Request) {
138143

139144
let data: any
140145

141-
if (query) {
142-
const params = new URLSearchParams({ query })
143-
const apiUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/picker?${params}`
144-
const response = await fetch(apiUrl, {
145-
headers: {
146-
Authorization: `Bearer ${accessToken}`,
147-
Accept: 'application/json',
148-
},
149-
})
150-
151-
if (!response.ok) {
152-
const errorMessage = await createErrorResponse(
153-
response,
154-
`Failed to fetch issue suggestions (${response.status})`
155-
)
156-
return NextResponse.json({ error: errorMessage }, { status: response.status })
157-
}
158-
data = await response.json()
159-
} else if (projectId || manualProjectId) {
146+
if (query || projectId || manualProjectId) {
160147
const SAFETY_CAP = 1000
161148
const PAGE_SIZE = 100
162149
const target = Math.min(all ? limit || SAFETY_CAP : 25, SAFETY_CAP)
163-
const projectKey = (projectId || manualProjectId).trim()
150+
const projectKey = (projectId || manualProjectId || '').trim()
151+
152+
const escapeJql = (s: string) => s.replace(/"/g, '\\"')
164153

165-
const buildSearchUrl = (startAt: number) => {
154+
const buildJql = (startAt: number) => {
155+
const jqlParts: string[] = []
156+
if (projectKey) jqlParts.push(`project = ${projectKey}`)
157+
if (query) {
158+
const q = escapeJql(query)
159+
// Match by key prefix or summary text
160+
jqlParts.push(`(key ~ "${q}" OR summary ~ "${q}")`)
161+
}
162+
const jql = `${jqlParts.length ? `${jqlParts.join(' AND ')} ` : ''}ORDER BY updated DESC`
166163
const params = new URLSearchParams({
167-
jql: `project=${projectKey} ORDER BY updated DESC`,
168-
maxResults: String(Math.min(PAGE_SIZE, target)),
169-
startAt: String(startAt),
164+
jql,
170165
fields: 'summary,key,updated',
166+
maxResults: String(Math.min(PAGE_SIZE, target)),
171167
})
172-
return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search?${params}`
168+
if (startAt > 0) {
169+
params.set('startAt', String(startAt))
170+
}
171+
return {
172+
url: `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`,
173+
}
173174
}
174175

175176
let startAt = 0
176177
let collected: any[] = []
177178
let total = 0
178179

179180
do {
180-
const response = await fetch(buildSearchUrl(startAt), {
181+
const { url: apiUrl } = buildJql(startAt)
182+
const response = await fetch(apiUrl, {
183+
method: 'GET',
181184
headers: {
182185
Authorization: `Bearer ${accessToken}`,
183186
Accept: 'application/json',
@@ -189,6 +192,16 @@ export async function GET(request: Request) {
189192
response,
190193
`Failed to fetch issues (${response.status})`
191194
)
195+
if (response.status === 401 || response.status === 403) {
196+
return NextResponse.json(
197+
{
198+
error: errorMessage,
199+
authRequired: true,
200+
requiredScopes: ['read:jira-work', 'read:project:jira'],
201+
},
202+
{ status: response.status }
203+
)
204+
}
192205
return NextResponse.json({ error: errorMessage }, { status: response.status })
193206
}
194207

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react'
55
import { JiraIcon } from '@/components/icons'
66
import { Button } from '@/components/ui/button'
@@ -75,7 +75,6 @@ export function JiraIssueSelector({
7575
const [selectedIssue, setSelectedIssue] = useState<JiraIssueInfo | null>(null)
7676
const [isLoading, setIsLoading] = useState(false)
7777
const [showOAuthModal, setShowOAuthModal] = useState(false)
78-
const initialFetchRef = useRef(false)
7978
const [error, setError] = useState<string | null>(null)
8079
const [cloudId, setCloudId] = useState<string | null>(null)
8180

@@ -123,17 +122,17 @@ export function JiraIssueSelector({
123122
return getServiceIdFromScopes(provider, requiredScopes)
124123
}
125124

126-
// Determine the appropriate provider ID based on service and scopes
127-
const getProviderId = (): string => {
125+
// Determine the appropriate provider ID based on service and scopes (stabilized)
126+
const providerId = useMemo(() => {
128127
const effectiveServiceId = getServiceId()
129128
return getProviderIdFromServiceId(effectiveServiceId)
130-
}
129+
}, [serviceId, provider, requiredScopes])
131130

132131
// Fetch available credentials for this provider
133132
const fetchCredentials = useCallback(async () => {
133+
if (!providerId) return
134134
setIsLoading(true)
135135
try {
136-
const providerId = getProviderId()
137136
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
138137

139138
if (response.ok) {
@@ -145,7 +144,7 @@ export function JiraIssueSelector({
145144
} finally {
146145
setIsLoading(false)
147146
}
148-
}, [provider, getProviderId, selectedCredentialId])
147+
}, [providerId])
149148

150149
// Fetch issue info when we have a selected issue ID
151150
const fetchIssueInfo = useCallback(

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react'
55
import { JiraIcon } from '@/components/icons'
66
import { Button } from '@/components/ui/button'
@@ -114,17 +114,17 @@ export function JiraProjectSelector({
114114
return getServiceIdFromScopes(provider, requiredScopes)
115115
}
116116

117-
// Determine the appropriate provider ID based on service and scopes
118-
const getProviderId = (): string => {
117+
// Determine the appropriate provider ID based on service and scopes (stabilized)
118+
const providerId = useMemo(() => {
119119
const effectiveServiceId = getServiceId()
120120
return getProviderIdFromServiceId(effectiveServiceId)
121-
}
121+
}, [serviceId, provider, requiredScopes])
122122

123123
// Fetch available credentials for this provider
124124
const fetchCredentials = useCallback(async () => {
125+
if (!providerId) return
125126
setIsLoading(true)
126127
try {
127-
const providerId = getProviderId()
128128
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
129129

130130
if (response.ok) {
@@ -137,7 +137,7 @@ export function JiraProjectSelector({
137137
} finally {
138138
setIsLoading(false)
139139
}
140-
}, [provider, getProviderId, selectedCredentialId])
140+
}, [providerId])
141141

142142
// Fetch detailed project information
143143
const fetchProjectInfo = useCallback(

apps/sim/tools/jira/bulk_read.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,23 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
4242

4343
request: {
4444
url: (params: JiraRetrieveBulkParams) => {
45-
if (params.cloudId) {
46-
const base = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/search`
47-
// Don't encode JQL here - transformResponse will handle project resolution
48-
// Initial page; transformResponse will paginate to retrieve all (with a safety cap)
49-
return `${base}?maxResults=100&startAt=0&fields=summary,description,created,updated`
50-
}
51-
// If no cloudId, use the accessible resources endpoint
45+
// Always return accessible resources endpoint; transformResponse will build search URLs
5246
return 'https://api.atlassian.com/oauth/token/accessible-resources'
5347
},
5448
method: 'GET',
5549
headers: (params: JiraRetrieveBulkParams) => ({
5650
Authorization: `Bearer ${params.accessToken}`,
5751
Accept: 'application/json',
5852
}),
59-
body: (params: JiraRetrieveBulkParams) => ({}),
53+
body: (params: JiraRetrieveBulkParams) =>
54+
params.cloudId
55+
? {
56+
jql: '', // Will be set in transformResponse when we know the resolved project key
57+
startAt: 0,
58+
maxResults: 100,
59+
fields: ['summary', 'description', 'created', 'updated'],
60+
}
61+
: {},
6062
},
6163

6264
transformResponse: async (response: Response, params?: JiraRetrieveBulkParams) => {
@@ -101,20 +103,27 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
101103
(r: any) => r.url.toLowerCase() === normalizedInput
102104
)
103105

104-
const base = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/search`
105106
const projectKey = await resolveProjectKey(
106107
matchedResource.id,
107108
params!.accessToken,
108109
params!.projectId
109110
)
110-
const jql = encodeURIComponent(`project=${projectKey} ORDER BY updated DESC`)
111+
const jql = `project = ${projectKey} ORDER BY updated DESC`
111112

112113
let startAt = 0
113114
let collected: any[] = []
114115
let total = 0
115116

116117
while (startAt < MAX_TOTAL) {
117-
const url = `${base}?jql=${jql}&maxResults=${PAGE_SIZE}&startAt=${startAt}&fields=summary,description,created,updated`
118+
const queryParams = new URLSearchParams({
119+
jql,
120+
fields: 'summary,description,created,updated',
121+
maxResults: String(PAGE_SIZE),
122+
})
123+
if (startAt > 0) {
124+
queryParams.set('startAt', String(startAt))
125+
}
126+
const url = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/search/jql?${queryParams.toString()}`
118127
const pageResponse = await fetch(url, {
119128
method: 'GET',
120129
headers: {
@@ -152,15 +161,22 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
152161
params!.projectId
153162
)
154163

155-
const base = `https://api.atlassian.com/ex/jira/${params?.cloudId}/rest/api/3/search`
156-
const jql = encodeURIComponent(`project=${projectKey} ORDER BY updated DESC`)
164+
const jql = `project = ${projectKey} ORDER BY updated DESC`
157165

158166
// Always do full pagination with resolved key
159167
let collected: any[] = []
160168
let total = 0
161169
let startAt = 0
162170
while (startAt < MAX_TOTAL) {
163-
const url = `${base}?jql=${jql}&maxResults=${PAGE_SIZE}&startAt=${startAt}&fields=summary,description,created,updated`
171+
const queryParams = new URLSearchParams({
172+
jql,
173+
fields: 'summary,description,created,updated',
174+
maxResults: String(PAGE_SIZE),
175+
})
176+
if (startAt > 0) {
177+
queryParams.set('startAt', String(startAt))
178+
}
179+
const url = `https://api.atlassian.com/ex/jira/${params?.cloudId}/rest/api/3/search/jql?${queryParams.toString()}`
164180
const pageResponse = await fetch(url, {
165181
method: 'GET',
166182
headers: {

0 commit comments

Comments
 (0)