open and close issue

This commit is contained in:
Isaac Johnson 2026-06-01 19:15:30 -05:00
parent 0702fea1ed
commit 41f8f18fcb
3 changed files with 109 additions and 3 deletions

View File

@ -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<void> {
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. * Adds time to an issue.
* @param time Time in seconds * @param time Time in seconds

View File

@ -1,7 +1,7 @@
import readline from 'readline'; import readline from 'readline';
import chalk from 'chalk'; import chalk from 'chalk';
import { AppState, Issue, Comment } from './types.js'; 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'; import { saveGlobalConfig } from './config.js';
// Setup readline for stdin keypress events // Setup readline for stdin keypress events
@ -291,6 +291,8 @@ export class TuiEngine {
this.handleEditIssueKeypress(str, key); this.handleEditIssueKeypress(str, key);
} else if (this.state.screen === 'add-time') { } else if (this.state.screen === 'add-time') {
this.handleAddTimeKeypress(str, key); this.handleAddTimeKeypress(str, key);
} else if (this.state.screen === 'confirm-state-change') {
this.handleConfirmStateChangeKeypress(str, key);
} }
} }
@ -747,6 +749,15 @@ export class TuiEngine {
return; 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') { if ((key && (key.name === 'escape' || key.name === 'backspace')) || str === '\u001b' || str === 'q' || str === 'Q') {
this.state.screen = 'list'; this.state.screen = 'list';
this.state.selectedIssue = null; this.state.selectedIssue = null;
@ -781,6 +792,8 @@ export class TuiEngine {
this.renderEditIssueScreen(cols, rows); this.renderEditIssueScreen(cols, rows);
} else if (this.state.screen === 'add-time') { } else if (this.state.screen === 'add-time') {
this.renderAddTimeScreen(cols, rows); 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}%)`); 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); process.stdout.write(helpLine);
} }
@ -1880,5 +1894,73 @@ export class TuiEngine {
this.render(); 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;
}
}
} }

View File

@ -69,7 +69,7 @@ export interface AddTimeForm {
timeInput: string; 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 { export interface RepoItem {