From 0c0b605610367c3e416095eead1d912e025cb151 Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Mon, 1 Jun 2026 18:15:01 -0500 Subject: [PATCH] edit issue --- src/api.ts | 49 +++++++++++++ src/index.ts | 5 ++ src/tui.ts | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/types.ts | 11 ++- 4 files changed, 251 insertions(+), 3 deletions(-) diff --git a/src/api.ts b/src/api.ts index ea01723..f6f1fd2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -332,3 +332,52 @@ export async function createIssueComment( throw new Error(`Network Error: ${error.message}`); } } + +/** + * Edits an existing issue. + */ +export async function editIssue( + config: Config, + issueNumber: number, + title: string, + body: string +): Promise { + const client = createAxiosInstance(config); + try { + const response = await client.patch(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}`, { + title, + body, + }); + + const item = response.data; + return { + id: item.id, + number: item.number, + title: item.title, + state: item.state, + body: item.body || '', + user: { + id: item.user?.id || 0, + login: item.user?.login || 'unknown', + full_name: item.user?.full_name || '', + }, + created_at: item.created_at, + updated_at: item.updated_at, + comments: item.comments_count || item.comments || 0, + labels: (item.labels || []).map((l: any) => ({ + id: l.id, + name: l.name, + color: l.color, + })), + pull_request: item.pull_request, + }; + } catch (error: any) { + if (error.response) { + if (error.response.status === 401) { + throw new Error('Unauthorized: You must be logged in with a token to edit an issue.'); + } + throw new Error(`Failed to edit issue: ${error.response.data?.message || error.message}`); + } + throw new Error(`Network Error: ${error.message}`); + } +} diff --git a/src/index.ts b/src/index.ts index 9bbf445..7a1fabe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,6 +76,11 @@ async function bootstrap() { addCommentForm: { body: '', }, + editIssueForm: { + title: '', + body: '', + activeField: 'title', + }, }; // If parameters are provided, try direct connection first diff --git a/src/tui.ts b/src/tui.ts index c16b995..8b51c21 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -1,7 +1,7 @@ import readline from 'readline'; import chalk from 'chalk'; import { AppState, Issue, Comment } from './types.js'; -import { fetchIssues, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment } from './api.js'; +import { fetchIssues, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue } from './api.js'; import { saveGlobalConfig } from './config.js'; // Setup readline for stdin keypress events @@ -250,6 +250,8 @@ export class TuiEngine { this.handleCreateIssueKeypress(str, key); } else if (this.state.screen === 'add-comment') { this.handleAddCommentKeypress(str, key); + } else if (this.state.screen === 'edit-issue') { + this.handleEditIssueKeypress(str, key); } } @@ -677,6 +679,18 @@ export class TuiEngine { return; } + if (str === 'e' || str === 'E') { + if (this.state.selectedIssue) { + this.state.screen = 'edit-issue'; + this.state.editIssueForm.title = this.state.selectedIssue.title; + this.state.editIssueForm.body = this.state.selectedIssue.body; + this.state.editIssueForm.activeField = 'title'; + this.state.error = null; + this.render(); + } + return; + } + if ((key && (key.name === 'escape' || key.name === 'backspace')) || str === '\u001b' || str === 'q' || str === 'Q') { this.state.screen = 'list'; this.state.selectedIssue = null; @@ -707,6 +721,8 @@ export class TuiEngine { this.renderCreateIssueScreen(cols, rows); } else if (this.state.screen === 'add-comment') { this.renderAddCommentScreen(cols, rows); + } else if (this.state.screen === 'edit-issue') { + this.renderEditIssueScreen(cols, rows); } } @@ -1108,7 +1124,7 @@ export class TuiEngine { scrollHelp += chalk.yellow(` (${pct}%)`); } - const helpLine = chalk.gray(` [Esc/Backspace] Back to List [C] Add Comment [O] Settings ${scrollHelp}`); + const helpLine = chalk.gray(` [Esc/Backspace] Back to List [C] Add Comment [E] Edit [O] Settings ${scrollHelp}`); process.stdout.write(helpLine); } @@ -1487,5 +1503,174 @@ export class TuiEngine { this.render(); } } + + private renderEditIssueScreen(cols: number, rows: number) { + const borderCh = chalk.bold.hex('#4A90E2')('│'); + + // Header + const title = ' Edit Issue '; + const leftLen = stripAnsi(title).length; + let leftHeader = chalk.bold.hex('#4A90E2')(title); + + console.log(leftHeader + ' '.repeat(Math.max(1, cols - leftLen - 1))); + console.log(chalk.bold.hex('#4A90E2')('┌' + '─'.repeat(cols - 2) + '┐')); + + if (this.state.error) { + console.log(borderCh + chalk.red(` Error: ${this.state.error}`).padEnd(cols - 2) + borderCh); + console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + } + + const form = this.state.editIssueForm; + + // Title Field + let titleLabel = ' Title: '; + let titleVisible = form.title; + if (titleVisible.length > cols - 13) { + titleVisible = titleVisible.substring(titleVisible.length - (cols - 13)); + } + let titleContent = titleVisible.padEnd(cols - 12); + if (form.activeField === 'title') { + titleLabel = chalk.yellow(' Title: '); + titleContent = chalk.inverse(titleVisible + ' ') + ' '.repeat(Math.max(0, cols - 13 - titleVisible.length)); + } + console.log(borderCh + titleLabel + titleContent + borderCh); + console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + + // Body Field + let bodyLabel = chalk.bold(' Body (markdown supported): '); + if (form.activeField === 'body') { + bodyLabel = chalk.yellow.bold(' Body (markdown supported): '); + } + console.log(borderCh + bodyLabel + ' '.repeat(cols - stripAnsi(bodyLabel).length - 2) + borderCh); + console.log(chalk.bold.hex('#4A90E2')('├' + '─'.repeat(cols - 2) + '┤')); + + // Body Content (multiline) + const errorOffset = this.state.error ? 2 : 0; + const bodyRows = rows - 10 - errorOffset; // available space for body + const lines = form.body.split('\n'); + + // Pagination for body if it gets too long + const startLine = Math.max(0, lines.length - bodyRows); + + for (let i = 0; i < bodyRows; i++) { + const lineIdx = startLine + i; + let lineContent = ''; + if (lineIdx < lines.length) { + lineContent = lines[lineIdx]; + } + + let visLine = lineContent; + if (visLine.length > cols - 5) { + visLine = visLine.substring(visLine.length - (cols - 5)); + } + + if (form.activeField === 'body' && lineIdx === lines.length - 1) { + visLine = visLine + chalk.inverse(' ') + ' '.repeat(Math.max(0, cols - 5 - stripAnsi(visLine).length)); + } else { + visLine = visLine.padEnd(cols - 4); + } + + console.log(borderCh + ' ' + visLine + ' ' + borderCh); + } + + console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); + + // Help line + const helpLine = chalk.gray(' [Tab] Switch Fields [Ctrl+S] Save [Esc] Cancel'); + const issueStr = this.state.selectedIssue ? chalk.bold.cyan(` Issue #${this.state.selectedIssue.number} `) : ''; + const issueLen = stripAnsi(issueStr).length; + + const maxHelpLen = cols - issueLen - 1; + let helpLinePlain = helpLine; + if (stripAnsi(helpLinePlain).length > maxHelpLen) { + helpLinePlain = chalk.gray(stripAnsi(helpLinePlain).substring(0, maxHelpLen - 3) + '...'); + } + + const spaces = cols - stripAnsi(helpLinePlain).length - issueLen - 1; + process.stdout.write(helpLinePlain + ' '.repeat(Math.max(0, spaces)) + issueStr); + } + + private async handleEditIssueKeypress(str: string, key: any) { + if (key && key.name === 'escape') { + this.state.screen = 'details'; + this.state.error = null; + this.render(); + return; + } + + if (key && key.ctrl && key.name === 's') { + // Save issue + if (!this.state.editIssueForm.title.trim()) { + this.state.error = 'Title is required.'; + this.render(); + return; + } + + if (!this.state.selectedIssue) { + this.state.error = 'No issue selected.'; + this.render(); + return; + } + + this.state.error = null; + this.state.loading = true; + this.render(); + try { + const updatedIssue = await editIssue(this.state.config, this.state.selectedIssue.number, this.state.editIssueForm.title, this.state.editIssueForm.body); + + // Update the list and selected issue with the new data + this.state.selectedIssue = updatedIssue; + const listIndex = this.state.issues.findIndex((i) => i.number === updatedIssue.number); + if (listIndex !== -1) { + this.state.issues[listIndex] = updatedIssue; + } + + this.state.screen = 'details'; + this.state.loading = false; + this.render(); + } catch (err: any) { + this.state.error = err.message; + this.state.loading = false; + this.render(); + } + return; + } + + if (key && key.name === 'tab') { + this.state.editIssueForm.activeField = this.state.editIssueForm.activeField === 'title' ? 'body' : 'title'; + this.render(); + return; + } + + // Text input handling + const activeField = this.state.editIssueForm.activeField; + let val = this.state.editIssueForm[activeField]; + + if (key && key.name === 'backspace') { + if (val.length > 0) { + this.state.editIssueForm[activeField] = val.slice(0, -1); + this.render(); + } + return; + } + + if (key && key.name === 'return') { + if (activeField === 'body') { + this.state.editIssueForm.body += '\n'; + this.render(); + } else { + // In title, return switches to body + this.state.editIssueForm.activeField = 'body'; + this.render(); + } + return; + } + + if (str && str.length === 1 && !key.ctrl && !key.meta) { + this.state.error = null; + this.state.editIssueForm[activeField] += str; + this.render(); + } + } } diff --git a/src/types.ts b/src/types.ts index 5cad291..d82c92f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,7 +58,13 @@ export interface AddCommentForm { body: string; } -export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment'; +export interface EditIssueForm { + title: string; + body: string; + activeField: 'title' | 'body'; +} + +export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue'; export interface RepoItem { @@ -107,5 +113,8 @@ export interface AppState { // Add Comment Form State addCommentForm: AddCommentForm; + + // Edit Issue Form State + editIssueForm: EditIssueForm; }