From 41f8f18fcb75c6f8cf0fed5c0f34dfb370360bbb Mon Sep 17 00:00:00 2001 From: Isaac Johnson Date: Mon, 1 Jun 2026 19:15:30 -0500 Subject: [PATCH] open and close issue --- src/api.ts | 24 +++++++++++++++ src/tui.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/types.ts | 2 +- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/api.ts b/src/api.ts index 3f978f3..db78552 100644 --- a/src/api.ts +++ b/src/api.ts @@ -448,6 +448,30 @@ export async function editIssue( } } +/** + * Changes the state of an issue (open/closed). + */ +export async function changeIssueState( + config: Config, + issueNumber: number, + state: 'open' | 'closed' +): Promise { + const client = createAxiosInstance(config); + try { + await client.patch(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}`, { + state, + }); + } catch (error: any) { + if (error.response) { + if (error.response.status === 401) { + throw new Error('Unauthorized: You must be logged in with a token to change issue state.'); + } + throw new Error(`Failed to change issue state: ${error.response.data?.message || error.message}`); + } + throw new Error(`Network Error: ${error.message}`); + } +} + /** * Adds time to an issue. * @param time Time in seconds diff --git a/src/tui.ts b/src/tui.ts index d56ef3c..1af8876 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, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime } from './api.js'; +import { fetchIssues, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime, changeIssueState } from './api.js'; import { saveGlobalConfig } from './config.js'; // Setup readline for stdin keypress events @@ -291,6 +291,8 @@ export class TuiEngine { this.handleEditIssueKeypress(str, key); } else if (this.state.screen === 'add-time') { this.handleAddTimeKeypress(str, key); + } else if (this.state.screen === 'confirm-state-change') { + this.handleConfirmStateChangeKeypress(str, key); } } @@ -747,6 +749,15 @@ export class TuiEngine { return; } + if (str === 'x' || str === 'X') { + if (this.state.selectedIssue) { + this.state.screen = 'confirm-state-change'; + 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; @@ -781,6 +792,8 @@ export class TuiEngine { this.renderEditIssueScreen(cols, rows); } else if (this.state.screen === 'add-time') { this.renderAddTimeScreen(cols, rows); + } else if (this.state.screen === 'confirm-state-change') { + this.renderConfirmStateChangeScreen(cols, rows); } } @@ -1198,7 +1211,8 @@ export class TuiEngine { scrollHelp += chalk.yellow(` (${pct}%)`); } - const helpLine = chalk.gray(` [Esc/Backspace] Back to List [C] Add Comment [E] Edit [T] Add Time [R] Reload [O] Settings ${scrollHelp}`); + const actionKey = issue.state === 'open' ? 'Close' : 'Reopen'; + const helpLine = chalk.gray(` [Esc/Backspace] Back to List [C] Add Comment [E] Edit [T] Add Time [X] ${actionKey} [R] Reload [O] Settings ${scrollHelp}`); process.stdout.write(helpLine); } @@ -1880,5 +1894,73 @@ export class TuiEngine { this.render(); } } + + private renderConfirmStateChangeScreen(cols: number, rows: number) { + const borderCh = chalk.bold.hex('#4A90E2')('│'); + + const issue = this.state.selectedIssue; + if (!issue) return; + + const action = issue.state === 'open' ? 'Close' : 'Reopen'; + const title = ` Confirm ${action} `; + 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 question = `Are you sure you want to ${action.toLowerCase()} issue #${issue.number}?`; + const promptPlain = ` ${question} [y/N] `; + + const paddingRows = Math.floor((rows - 8) / 2); + for (let i = 0; i < paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + + const contentLine = (' '.repeat(Math.max(0, Math.floor((cols - 2 - promptPlain.length) / 2))) + chalk.yellow.bold(question) + ' [y/N] ').padEnd(cols - 2 + (chalk.yellow.bold(question).length - question.length)); + console.log(borderCh + contentLine + borderCh); + + for (let i = 0; i < (rows - 8) - paddingRows; i++) console.log(borderCh + ' '.repeat(cols - 2) + borderCh); + + console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘')); + + const helpLinePlain = ' [y] Yes [n/Esc] No'; + process.stdout.write(chalk.gray(helpLinePlain)); + } + + private async handleConfirmStateChangeKeypress(str: string, key: any) { + if ((key && (key.name === 'escape' || key.name === 'n')) || str === 'n' || str === 'N') { + this.state.screen = 'details'; + this.state.error = null; + this.render(); + return; + } + + if (str === 'y' || str === 'Y') { + const issue = this.state.selectedIssue; + if (!issue) return; + + this.state.error = null; + this.state.loading = true; + this.render(); + + try { + const newState = issue.state === 'open' ? 'closed' : 'open'; + await changeIssueState(this.state.config, issue.number, newState); + this.state.screen = 'details'; + this.state.loading = false; + // Reload to show the updated state + this.reloadSingleIssue(); + } catch (err: any) { + this.state.error = err.message; + this.state.loading = false; + this.render(); + } + return; + } + } } diff --git a/src/types.ts b/src/types.ts index a34d193..77f8973 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,7 +69,7 @@ export interface AddTimeForm { timeInput: string; } -export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time'; +export type ScreenType = 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time' | 'confirm-state-change'; export interface RepoItem {