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 <33737564+Sg312@users.noreply.github.com>
This commit is contained in:
Vikhyath Mondreti
2025-10-19 15:20:08 -07:00
committed by GitHub
parent 9132cd224d
commit 063bd610b1
4 changed files with 103 additions and 75 deletions

View File

@@ -45,27 +45,22 @@ export async function POST(request: Request) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/bulkfetch`
// Use search/jql endpoint (GET) with URL parameters
const jql = `issueKey in (${issueKeys.map((k: string) => k.trim()).join(',')})`
const params = new URLSearchParams({
jql,
fields: 'summary,status,assignee,updated,project',
maxResults: String(Math.min(issueKeys.length, 100)),
})
const searchUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`
const requestBody = {
expand: ['names'],
fields: ['summary', 'status', 'assignee', 'updated', 'project'],
fieldsByKeys: false,
issueIdsOrKeys: issueKeys,
properties: [],
}
const requestConfig = {
method: 'POST',
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
}
const response = await fetch(url, requestConfig)
})
if (!response.ok) {
logger.error(`Jira API error: ${response.status} ${response.statusText}`)
@@ -73,17 +68,27 @@ export async function POST(request: Request) {
response,
`Failed to fetch Jira issues (${response.status})`
)
if (response.status === 401 || response.status === 403) {
return NextResponse.json(
{
error: errorMessage,
authRequired: true,
requiredScopes: ['read:jira-work', 'read:project:jira'],
},
{ status: response.status }
)
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const issues = (data.issues || []).map((issue: any) => ({
id: issue.key,
name: issue.fields.summary,
const issues = (data.issues || []).map((it: any) => ({
id: it.key,
name: it.fields?.summary || it.key,
mimeType: 'jira/issue',
url: `https://${domain}/browse/${issue.key}`,
modifiedTime: issue.fields.updated,
webViewLink: `https://${domain}/browse/${issue.key}`,
url: `https://${domain}/browse/${it.key}`,
modifiedTime: it.fields?.updated,
webViewLink: `https://${domain}/browse/${it.key}`,
}))
return NextResponse.json({ issues, cloudId })
@@ -138,38 +143,34 @@ export async function GET(request: Request) {
let data: any
if (query) {
const params = new URLSearchParams({ query })
const apiUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/picker?${params}`
const response = await fetch(apiUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorMessage = await createErrorResponse(
response,
`Failed to fetch issue suggestions (${response.status})`
)
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
data = await response.json()
} else if (projectId || manualProjectId) {
if (query || projectId || manualProjectId) {
const SAFETY_CAP = 1000
const PAGE_SIZE = 100
const target = Math.min(all ? limit || SAFETY_CAP : 25, SAFETY_CAP)
const projectKey = (projectId || manualProjectId).trim()
const projectKey = (projectId || manualProjectId || '').trim()
const buildSearchUrl = (startAt: number) => {
const escapeJql = (s: string) => s.replace(/"/g, '\\"')
const buildJql = (startAt: number) => {
const jqlParts: string[] = []
if (projectKey) jqlParts.push(`project = ${projectKey}`)
if (query) {
const q = escapeJql(query)
// Match by key prefix or summary text
jqlParts.push(`(key ~ "${q}" OR summary ~ "${q}")`)
}
const jql = `${jqlParts.length ? `${jqlParts.join(' AND ')} ` : ''}ORDER BY updated DESC`
const params = new URLSearchParams({
jql: `project=${projectKey} ORDER BY updated DESC`,
maxResults: String(Math.min(PAGE_SIZE, target)),
startAt: String(startAt),
jql,
fields: 'summary,key,updated',
maxResults: String(Math.min(PAGE_SIZE, target)),
})
return `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search?${params}`
if (startAt > 0) {
params.set('startAt', String(startAt))
}
return {
url: `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/search/jql?${params.toString()}`,
}
}
let startAt = 0
@@ -177,7 +178,9 @@ export async function GET(request: Request) {
let total = 0
do {
const response = await fetch(buildSearchUrl(startAt), {
const { url: apiUrl } = buildJql(startAt)
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
@@ -189,6 +192,16 @@ export async function GET(request: Request) {
response,
`Failed to fetch issues (${response.status})`
)
if (response.status === 401 || response.status === 403) {
return NextResponse.json(
{
error: errorMessage,
authRequired: true,
requiredScopes: ['read:jira-work', 'read:project:jira'],
},
{ status: response.status }
)
}
return NextResponse.json({ error: errorMessage }, { status: response.status })
}

View File

@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react'
import { JiraIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
@@ -75,7 +75,6 @@ export function JiraIssueSelector({
const [selectedIssue, setSelectedIssue] = useState<JiraIssueInfo | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [showOAuthModal, setShowOAuthModal] = useState(false)
const initialFetchRef = useRef(false)
const [error, setError] = useState<string | null>(null)
const [cloudId, setCloudId] = useState<string | null>(null)
@@ -123,17 +122,17 @@ export function JiraIssueSelector({
return getServiceIdFromScopes(provider, requiredScopes)
}
// Determine the appropriate provider ID based on service and scopes
const getProviderId = (): string => {
// Determine the appropriate provider ID based on service and scopes (stabilized)
const providerId = useMemo(() => {
const effectiveServiceId = getServiceId()
return getProviderIdFromServiceId(effectiveServiceId)
}
}, [serviceId, provider, requiredScopes])
// Fetch available credentials for this provider
const fetchCredentials = useCallback(async () => {
if (!providerId) return
setIsLoading(true)
try {
const providerId = getProviderId()
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
if (response.ok) {
@@ -145,7 +144,7 @@ export function JiraIssueSelector({
} finally {
setIsLoading(false)
}
}, [provider, getProviderId, selectedCredentialId])
}, [providerId])
// Fetch issue info when we have a selected issue ID
const fetchIssueInfo = useCallback(

View File

@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react'
import { JiraIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
@@ -114,17 +114,17 @@ export function JiraProjectSelector({
return getServiceIdFromScopes(provider, requiredScopes)
}
// Determine the appropriate provider ID based on service and scopes
const getProviderId = (): string => {
// Determine the appropriate provider ID based on service and scopes (stabilized)
const providerId = useMemo(() => {
const effectiveServiceId = getServiceId()
return getProviderIdFromServiceId(effectiveServiceId)
}
}, [serviceId, provider, requiredScopes])
// Fetch available credentials for this provider
const fetchCredentials = useCallback(async () => {
if (!providerId) return
setIsLoading(true)
try {
const providerId = getProviderId()
const response = await fetch(`/api/auth/oauth/credentials?provider=${providerId}`)
if (response.ok) {
@@ -137,7 +137,7 @@ export function JiraProjectSelector({
} finally {
setIsLoading(false)
}
}, [provider, getProviderId, selectedCredentialId])
}, [providerId])
// Fetch detailed project information
const fetchProjectInfo = useCallback(

View File

@@ -42,13 +42,7 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
request: {
url: (params: JiraRetrieveBulkParams) => {
if (params.cloudId) {
const base = `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/search`
// Don't encode JQL here - transformResponse will handle project resolution
// Initial page; transformResponse will paginate to retrieve all (with a safety cap)
return `${base}?maxResults=100&startAt=0&fields=summary,description,created,updated`
}
// If no cloudId, use the accessible resources endpoint
// Always return accessible resources endpoint; transformResponse will build search URLs
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
method: 'GET',
@@ -56,7 +50,15 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
Authorization: `Bearer ${params.accessToken}`,
Accept: 'application/json',
}),
body: (params: JiraRetrieveBulkParams) => ({}),
body: (params: JiraRetrieveBulkParams) =>
params.cloudId
? {
jql: '', // Will be set in transformResponse when we know the resolved project key
startAt: 0,
maxResults: 100,
fields: ['summary', 'description', 'created', 'updated'],
}
: {},
},
transformResponse: async (response: Response, params?: JiraRetrieveBulkParams) => {
@@ -101,20 +103,27 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
(r: any) => r.url.toLowerCase() === normalizedInput
)
const base = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/search`
const projectKey = await resolveProjectKey(
matchedResource.id,
params!.accessToken,
params!.projectId
)
const jql = encodeURIComponent(`project=${projectKey} ORDER BY updated DESC`)
const jql = `project = ${projectKey} ORDER BY updated DESC`
let startAt = 0
let collected: any[] = []
let total = 0
while (startAt < MAX_TOTAL) {
const url = `${base}?jql=${jql}&maxResults=${PAGE_SIZE}&startAt=${startAt}&fields=summary,description,created,updated`
const queryParams = new URLSearchParams({
jql,
fields: 'summary,description,created,updated',
maxResults: String(PAGE_SIZE),
})
if (startAt > 0) {
queryParams.set('startAt', String(startAt))
}
const url = `https://api.atlassian.com/ex/jira/${matchedResource.id}/rest/api/3/search/jql?${queryParams.toString()}`
const pageResponse = await fetch(url, {
method: 'GET',
headers: {
@@ -152,15 +161,22 @@ export const jiraBulkRetrieveTool: ToolConfig<JiraRetrieveBulkParams, JiraRetrie
params!.projectId
)
const base = `https://api.atlassian.com/ex/jira/${params?.cloudId}/rest/api/3/search`
const jql = encodeURIComponent(`project=${projectKey} ORDER BY updated DESC`)
const jql = `project = ${projectKey} ORDER BY updated DESC`
// Always do full pagination with resolved key
let collected: any[] = []
let total = 0
let startAt = 0
while (startAt < MAX_TOTAL) {
const url = `${base}?jql=${jql}&maxResults=${PAGE_SIZE}&startAt=${startAt}&fields=summary,description,created,updated`
const queryParams = new URLSearchParams({
jql,
fields: 'summary,description,created,updated',
maxResults: String(PAGE_SIZE),
})
if (startAt > 0) {
queryParams.set('startAt', String(startAt))
}
const url = `https://api.atlassian.com/ex/jira/${params?.cloudId}/rest/api/3/search/jql?${queryParams.toString()}`
const pageResponse = await fetch(url, {
method: 'GET',
headers: {