mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
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:
committed by
GitHub
parent
9132cd224d
commit
063bd610b1
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user