edit issue
This commit is contained in:
parent
cfb845f1dd
commit
0c0b605610
49
src/api.ts
49
src/api.ts
|
|
@ -332,3 +332,52 @@ export async function createIssueComment(
|
||||||
throw new Error(`Network Error: ${error.message}`);
|
throw new Error(`Network Error: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits an existing issue.
|
||||||
|
*/
|
||||||
|
export async function editIssue(
|
||||||
|
config: Config,
|
||||||
|
issueNumber: number,
|
||||||
|
title: string,
|
||||||
|
body: string
|
||||||
|
): Promise<Issue> {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,11 @@ async function bootstrap() {
|
||||||
addCommentForm: {
|
addCommentForm: {
|
||||||
body: '',
|
body: '',
|
||||||
},
|
},
|
||||||
|
editIssueForm: {
|
||||||
|
title: '',
|
||||||
|
body: '',
|
||||||
|
activeField: 'title',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// If parameters are provided, try direct connection first
|
// If parameters are provided, try direct connection first
|
||||||
|
|
|
||||||
189
src/tui.ts
189
src/tui.ts
|
|
@ -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, 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';
|
import { saveGlobalConfig } from './config.js';
|
||||||
|
|
||||||
// Setup readline for stdin keypress events
|
// Setup readline for stdin keypress events
|
||||||
|
|
@ -250,6 +250,8 @@ export class TuiEngine {
|
||||||
this.handleCreateIssueKeypress(str, key);
|
this.handleCreateIssueKeypress(str, key);
|
||||||
} else if (this.state.screen === 'add-comment') {
|
} else if (this.state.screen === 'add-comment') {
|
||||||
this.handleAddCommentKeypress(str, key);
|
this.handleAddCommentKeypress(str, key);
|
||||||
|
} else if (this.state.screen === 'edit-issue') {
|
||||||
|
this.handleEditIssueKeypress(str, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -677,6 +679,18 @@ export class TuiEngine {
|
||||||
return;
|
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') {
|
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;
|
||||||
|
|
@ -707,6 +721,8 @@ export class TuiEngine {
|
||||||
this.renderCreateIssueScreen(cols, rows);
|
this.renderCreateIssueScreen(cols, rows);
|
||||||
} else if (this.state.screen === 'add-comment') {
|
} else if (this.state.screen === 'add-comment') {
|
||||||
this.renderAddCommentScreen(cols, rows);
|
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}%)`);
|
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);
|
process.stdout.write(helpLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1487,5 +1503,174 @@ export class TuiEngine {
|
||||||
this.render();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
src/types.ts
11
src/types.ts
|
|
@ -58,7 +58,13 @@ export interface AddCommentForm {
|
||||||
body: string;
|
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 {
|
export interface RepoItem {
|
||||||
|
|
@ -107,5 +113,8 @@ export interface AppState {
|
||||||
|
|
||||||
// Add Comment Form State
|
// Add Comment Form State
|
||||||
addCommentForm: AddCommentForm;
|
addCommentForm: AddCommentForm;
|
||||||
|
|
||||||
|
// Edit Issue Form State
|
||||||
|
editIssueForm: EditIssueForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue