mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
feat(issues): make issues page writable with inline editing (#1017)
This commit is contained in:
+171
-4
@@ -1,6 +1,6 @@
|
||||
import type { IssueInfo } from '../../shared/types'
|
||||
import type { IssueInfo, GitHubIssueUpdate } from '../../shared/types'
|
||||
import { mapIssueInfo } from './mappers'
|
||||
import { ghExecFileAsync, acquire, release, getOwnerRepo } from './gh-utils'
|
||||
import { ghExecFileAsync, acquire, release, getOwnerRepo, classifyGhError } from './gh-utils'
|
||||
|
||||
/**
|
||||
* Get a single issue by number.
|
||||
@@ -98,9 +98,9 @@ export async function createIssue(
|
||||
'-X',
|
||||
'POST',
|
||||
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues`,
|
||||
'-f',
|
||||
'--raw-field',
|
||||
`title=${trimmedTitle}`,
|
||||
'-f',
|
||||
'--raw-field',
|
||||
`body=${body}`
|
||||
],
|
||||
{ cwd: repoPath }
|
||||
@@ -121,3 +121,170 @@ export async function createIssue(
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing GitHub issue. Fans out to separate gh commands for
|
||||
* state changes vs field edits since `gh issue edit` does not support state.
|
||||
*/
|
||||
export async function updateIssue(
|
||||
repoPath: string,
|
||||
issueNumber: number,
|
||||
updates: GitHubIssueUpdate
|
||||
): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
const ownerRepo = await getOwnerRepo(repoPath)
|
||||
if (!ownerRepo) {
|
||||
return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' }
|
||||
}
|
||||
|
||||
const repo = `${ownerRepo.owner}/${ownerRepo.repo}`
|
||||
const errors: string[] = []
|
||||
|
||||
// State change requires a separate command
|
||||
if (updates.state) {
|
||||
await acquire()
|
||||
try {
|
||||
const cmd = updates.state === 'closed' ? 'close' : 'reopen'
|
||||
await ghExecFileAsync(['issue', cmd, String(issueNumber), '--repo', repo], {
|
||||
cwd: repoPath
|
||||
})
|
||||
} catch (err) {
|
||||
const stderr = err instanceof Error ? err.message : String(err)
|
||||
// Treat "already closed/open" as a no-op
|
||||
if (!stderr.toLowerCase().includes('already')) {
|
||||
errors.push(classifyGhError(stderr).message)
|
||||
}
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
// Field edits (labels, assignees, title) via gh issue edit
|
||||
const editArgs: string[] = ['issue', 'edit', String(issueNumber), '--repo', repo]
|
||||
let hasEditArgs = false
|
||||
|
||||
if (updates.title) {
|
||||
editArgs.push('--title', updates.title)
|
||||
hasEditArgs = true
|
||||
}
|
||||
for (const label of updates.addLabels ?? []) {
|
||||
editArgs.push('--add-label', label)
|
||||
hasEditArgs = true
|
||||
}
|
||||
for (const label of updates.removeLabels ?? []) {
|
||||
editArgs.push('--remove-label', label)
|
||||
hasEditArgs = true
|
||||
}
|
||||
for (const assignee of updates.addAssignees ?? []) {
|
||||
editArgs.push('--add-assignee', assignee)
|
||||
hasEditArgs = true
|
||||
}
|
||||
for (const assignee of updates.removeAssignees ?? []) {
|
||||
editArgs.push('--remove-assignee', assignee)
|
||||
hasEditArgs = true
|
||||
}
|
||||
|
||||
if (hasEditArgs) {
|
||||
await acquire()
|
||||
try {
|
||||
await ghExecFileAsync(editArgs, { cwd: repoPath })
|
||||
} catch (err) {
|
||||
const stderr = err instanceof Error ? err.message : String(err)
|
||||
errors.push(classifyGhError(stderr).message)
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { ok: false, error: errors.join('; ') }
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
export async function addIssueComment(
|
||||
repoPath: string,
|
||||
issueNumber: number,
|
||||
body: string
|
||||
): Promise<{ ok: true; id: number } | { ok: false; error: string }> {
|
||||
const ownerRepo = await getOwnerRepo(repoPath)
|
||||
if (!ownerRepo) {
|
||||
return { ok: false, error: 'Could not resolve GitHub owner/repo for this repository' }
|
||||
}
|
||||
await acquire()
|
||||
try {
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'api',
|
||||
'-X',
|
||||
'POST',
|
||||
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues/${issueNumber}/comments`,
|
||||
'--raw-field',
|
||||
`body=${body}`
|
||||
],
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
const data = JSON.parse(stdout) as { id?: number }
|
||||
return { ok: true, id: data.id ?? 0 }
|
||||
} catch (err) {
|
||||
const stderr = err instanceof Error ? err.message : String(err)
|
||||
return { ok: false, error: classifyGhError(stderr).message }
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
export async function listLabels(repoPath: string): Promise<string[]> {
|
||||
const ownerRepo = await getOwnerRepo(repoPath)
|
||||
if (!ownerRepo) {
|
||||
return []
|
||||
}
|
||||
await acquire()
|
||||
try {
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'api',
|
||||
'--paginate',
|
||||
`repos/${ownerRepo.owner}/${ownerRepo.repo}/labels`,
|
||||
'--jq',
|
||||
'.[].name'
|
||||
],
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
return stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((l) => l.length > 0)
|
||||
} catch {
|
||||
return []
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAssignableUsers(repoPath: string): Promise<string[]> {
|
||||
const ownerRepo = await getOwnerRepo(repoPath)
|
||||
if (!ownerRepo) {
|
||||
return []
|
||||
}
|
||||
await acquire()
|
||||
try {
|
||||
const { stdout } = await ghExecFileAsync(
|
||||
[
|
||||
'api',
|
||||
'--paginate',
|
||||
`repos/${ownerRepo.owner}/${ownerRepo.repo}/assignees`,
|
||||
'--jq',
|
||||
'.[].login'
|
||||
],
|
||||
{ cwd: repoPath }
|
||||
)
|
||||
return stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((l) => l.length > 0)
|
||||
} catch {
|
||||
return []
|
||||
} finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user