add issue lables
This commit is contained in:
parent
616685c9b2
commit
e70afe6c05
104
src/api.ts
104
src/api.ts
|
|
@ -1,5 +1,5 @@
|
|||
import axios from 'axios';
|
||||
import { Config, Issue, Comment, RepoItem } from './types.js';
|
||||
import { Config, Issue, Comment, RepoItem, Label } from './types.js';
|
||||
|
||||
/**
|
||||
* Normalizes the Gitea/Forgejo URL by stripping trailing slashes
|
||||
|
|
@ -598,3 +598,105 @@ export async function setIssueAssignees(
|
|||
throw new Error(`Network Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all labels for a specific repository.
|
||||
*/
|
||||
export async function fetchLabels(config: Config): Promise<Label[]> {
|
||||
const client = createAxiosInstance(config);
|
||||
try {
|
||||
const response = await client.get(`/repos/${config.owner}/${config.repo}/labels`);
|
||||
return response.data.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
color: item.color,
|
||||
description: item.description,
|
||||
exclusive: item.exclusive,
|
||||
}));
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
throw new Error(`Failed to fetch labels: ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`Network Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new label in the specified repository.
|
||||
*/
|
||||
export async function createLabel(config: Config, label: Partial<Label>): Promise<Label> {
|
||||
const client = createAxiosInstance(config);
|
||||
try {
|
||||
const response = await client.post(`/repos/${config.owner}/${config.repo}/labels`, {
|
||||
name: label.name,
|
||||
color: label.color, // expects hex without #
|
||||
description: label.description,
|
||||
exclusive: label.exclusive,
|
||||
});
|
||||
const item = response.data;
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
color: item.color,
|
||||
description: item.description,
|
||||
exclusive: item.exclusive,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
throw new Error('Unauthorized: You must be logged in with a token to create a label.');
|
||||
}
|
||||
throw new Error(`Failed to create label: ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`Network Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing label.
|
||||
*/
|
||||
export async function updateLabel(config: Config, id: number, label: Partial<Label>): Promise<Label> {
|
||||
const client = createAxiosInstance(config);
|
||||
try {
|
||||
const response = await client.patch(`/repos/${config.owner}/${config.repo}/labels/${id}`, {
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description,
|
||||
exclusive: label.exclusive,
|
||||
});
|
||||
const item = response.data;
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
color: item.color,
|
||||
description: item.description,
|
||||
exclusive: item.exclusive,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
throw new Error('Unauthorized: You must be logged in with a token to update a label.');
|
||||
}
|
||||
throw new Error(`Failed to update label: ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`Network Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a label.
|
||||
*/
|
||||
export async function deleteLabel(config: Config, id: number): Promise<void> {
|
||||
const client = createAxiosInstance(config);
|
||||
try {
|
||||
await client.delete(`/repos/${config.owner}/${config.repo}/labels/${id}`);
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
if (error.response.status === 401) {
|
||||
throw new Error('Unauthorized: You must be logged in with a token to delete a label.');
|
||||
}
|
||||
throw new Error(`Failed to delete label: ${error.response.data?.message || error.message}`);
|
||||
}
|
||||
throw new Error(`Network Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
src/index.ts
10
src/index.ts
|
|
@ -85,6 +85,16 @@ async function bootstrap() {
|
|||
addTimeForm: {
|
||||
timeInput: '',
|
||||
},
|
||||
labels: [],
|
||||
selectedLabelIndex: 0,
|
||||
labelsLoading: false,
|
||||
labelForm: {
|
||||
name: '',
|
||||
color: '',
|
||||
description: '',
|
||||
exclusive: false,
|
||||
activeField: 'name',
|
||||
},
|
||||
};
|
||||
|
||||
// If parameters are provided, try direct connection first
|
||||
|
|
|
|||
337
src/tui.ts
337
src/tui.ts
|
|
@ -4,7 +4,7 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { AppState, Issue, Comment } from './types.js';
|
||||
import { fetchIssues, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime, changeIssueState, setIssueAssignees } from './api.js';
|
||||
import { fetchIssues, fetchIssue, fetchIssueComments, validateConnection, normalizeUrl, authenticateAndFetchRepos, createIssue, createIssueComment, editIssue, addIssueTime, changeIssueState, setIssueAssignees, fetchLabels, createLabel, updateLabel, deleteLabel } from './api.js';
|
||||
import { saveGlobalConfig } from './config.js';
|
||||
|
||||
// Setup readline for stdin keypress events
|
||||
|
|
@ -400,10 +400,12 @@ 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);
|
||||
} else if (this.state.screen === 'set-assignees') {
|
||||
this.handleSetAssigneesKeypress(str, key);
|
||||
} else if (this.state.screen === 'labels-list') {
|
||||
this.handleLabelsListKeypress(str, key);
|
||||
} else if (this.state.screen === 'create-label' || this.state.screen === 'edit-label') {
|
||||
this.handleLabelFormKeypress(str, key);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -778,6 +780,13 @@ export class TuiEngine {
|
|||
return;
|
||||
}
|
||||
|
||||
if (str === 'l' || str === 'L') {
|
||||
this.state.screen = 'labels-list';
|
||||
this.state.selectedLabelIndex = 0;
|
||||
this.loadLabels();
|
||||
return;
|
||||
}
|
||||
|
||||
if (str === 'q' || str === 'Q') {
|
||||
this.stop();
|
||||
}
|
||||
|
|
@ -923,6 +932,10 @@ export class TuiEngine {
|
|||
this.renderAnimationScreen(cols, rows, ZOMBIE_FRAMES);
|
||||
} else if (this.state.screen === 'set-assignees') {
|
||||
this.renderSetAssigneesScreen(cols, rows);
|
||||
} else if (this.state.screen === 'labels-list') {
|
||||
this.renderLabelsList(cols, rows);
|
||||
} else if (this.state.screen === 'create-label' || this.state.screen === 'edit-label') {
|
||||
this.renderLabelForm(cols, rows);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1122,11 +1135,12 @@ export class TuiEngine {
|
|||
const stateWidth = 7;
|
||||
const authorWidth = 14;
|
||||
const assigneesWidth = 14;
|
||||
const labelsWidth = 16;
|
||||
const createdWidth = 12;
|
||||
const commentsWidth = 6;
|
||||
const timeWidth = 7;
|
||||
// Title takes all remaining space. There are 9 columns + 10 borders + 9 spaces = 19 extra chars
|
||||
const titleWidth = Math.max(10, cols - idWidth - typeWidth - stateWidth - authorWidth - assigneesWidth - createdWidth - commentsWidth - timeWidth - 19);
|
||||
// Title takes all remaining space. There are 10 columns + 11 borders + 10 spaces = 21 extra chars
|
||||
const titleWidth = Math.max(10, cols - idWidth - typeWidth - stateWidth - authorWidth - assigneesWidth - labelsWidth - createdWidth - commentsWidth - timeWidth - 21);
|
||||
|
||||
// Render Table Header
|
||||
const padHeader = (title: string, w: number) => chalk.bold.white(title.padEnd(w));
|
||||
|
|
@ -1140,6 +1154,7 @@ export class TuiEngine {
|
|||
padHeader('Title', titleWidth) + borderCh + ' ' +
|
||||
padHeader('Author', authorWidth) + borderCh + ' ' +
|
||||
padHeader('Assignees', assigneesWidth) + borderCh + ' ' +
|
||||
padHeader('Labels', labelsWidth) + borderCh + ' ' +
|
||||
padHeader('Created', createdWidth) + borderCh + ' ' +
|
||||
padHeader('Coms', commentsWidth) + borderCh + ' ' +
|
||||
padHeader('Time', timeWidth) + borderCh
|
||||
|
|
@ -1179,6 +1194,26 @@ export class TuiEngine {
|
|||
const authorStr = truncate(issue.user.login, authorWidth).padEnd(authorWidth);
|
||||
const assigneesNames = (issue.assignees || []).map(u => u.login).join(',');
|
||||
const assigneesStr = truncate(assigneesNames, assigneesWidth).padEnd(assigneesWidth);
|
||||
let plainLen = 0;
|
||||
let issueLabelsStr = '';
|
||||
for (let idx = 0; idx < issue.labels.length; idx++) {
|
||||
const l = issue.labels[idx];
|
||||
let name = l.name;
|
||||
if (name.length + 2 > labelsWidth) {
|
||||
name = name.substring(0, labelsWidth - 3) + '…';
|
||||
}
|
||||
const lText = ` ${name} `;
|
||||
if (plainLen + lText.length > labelsWidth) break;
|
||||
issueLabelsStr += chalk.bgHex('#' + l.color).black(lText);
|
||||
plainLen += lText.length;
|
||||
if (idx < issue.labels.length - 1 && plainLen + 1 <= labelsWidth) {
|
||||
issueLabelsStr += ' ';
|
||||
plainLen += 1;
|
||||
} else if (idx < issue.labels.length - 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
issueLabelsStr += ' '.repeat(Math.max(0, labelsWidth - plainLen));
|
||||
const createdStr = formatDate(issue.created_at).substring(0, 10).padEnd(createdWidth);
|
||||
const commentsStr = String(issue.comments).padEnd(commentsWidth);
|
||||
const timeStr = formatTime(issue.total_tracked_time).padEnd(timeWidth);
|
||||
|
|
@ -1190,6 +1225,7 @@ export class TuiEngine {
|
|||
' ' + (issue.pull_request ? chalk.magenta(titleStr) : titleStr) + borderCh +
|
||||
' ' + authorStr + borderCh +
|
||||
' ' + assigneesStr + borderCh +
|
||||
' ' + issueLabelsStr + borderCh +
|
||||
' ' + createdStr + borderCh +
|
||||
' ' + commentsStr + borderCh +
|
||||
' ' + timeStr + borderCh;
|
||||
|
|
@ -1224,7 +1260,7 @@ export class TuiEngine {
|
|||
console.log(chalk.bold.hex('#4A90E2')('└' + '─'.repeat(cols - 2) + '┘'));
|
||||
|
||||
// Keyboard controls help line
|
||||
const fullHelpLine = ' [↑/↓] Navigate [Enter] View [C] Create [/] Search [S] Sort [F] State [T] Type [N/P] Page [R] Reload [O] Settings [Esc] Quit';
|
||||
const fullHelpLine = ' [↑/↓] Navigate [Enter] View [C] Create [/] Search [S] Sort [F] State [T] Type [L] Labels [N/P] Page [R] Reload [O] Settings [Esc] Quit';
|
||||
const repoStr = chalk.bold.cyan(` repo: ${this.state.config.owner}/${this.state.config.repo} `);
|
||||
const repoLen = stripAnsi(repoStr).length;
|
||||
|
||||
|
|
@ -2395,8 +2431,295 @@ export class TuiEngine {
|
|||
|
||||
const printedRows = H + padTopCount + 5;
|
||||
const remainingRows = Math.max(0, rows - printedRows - 1);
|
||||
for (let i = 0; i < remainingRows; i++) {
|
||||
}
|
||||
|
||||
// --- Labels Logic ---
|
||||
private async loadLabels() {
|
||||
this.state.labelsLoading = true;
|
||||
this.state.error = null;
|
||||
this.render();
|
||||
try {
|
||||
this.state.labels = await fetchLabels(this.state.config);
|
||||
} catch (err: any) {
|
||||
this.state.error = `Failed to load labels: ${err.message}`;
|
||||
} finally {
|
||||
this.state.labelsLoading = false;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLabelsListKeypress(str: string, key: any) {
|
||||
if (this.state.labelsLoading) return;
|
||||
|
||||
if ((key && key.name === 'escape') || str === '\u001b' || str === 'q' || str === 'Q') {
|
||||
this.state.screen = 'list';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'up') {
|
||||
if (this.state.selectedLabelIndex > 0) {
|
||||
this.state.selectedLabelIndex--;
|
||||
this.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'down') {
|
||||
if (this.state.selectedLabelIndex < this.state.labels.length - 1) {
|
||||
this.state.selectedLabelIndex++;
|
||||
this.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (str === 'c' || str === 'C') {
|
||||
this.state.screen = 'create-label';
|
||||
this.state.labelForm = {
|
||||
name: '',
|
||||
color: '000000',
|
||||
description: '',
|
||||
exclusive: false,
|
||||
activeField: 'name'
|
||||
};
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (str === 'e' || str === 'E') {
|
||||
if (this.state.labels.length > 0) {
|
||||
const lbl = this.state.labels[this.state.selectedLabelIndex];
|
||||
this.state.screen = 'edit-label';
|
||||
this.state.labelForm = {
|
||||
name: lbl.name,
|
||||
color: lbl.color,
|
||||
description: lbl.description || '',
|
||||
exclusive: lbl.exclusive || false,
|
||||
activeField: 'name'
|
||||
};
|
||||
this.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ((key && key.name === 'delete') || str === 'd' || str === 'D') {
|
||||
if (this.state.labels.length > 0) {
|
||||
const lbl = this.state.labels[this.state.selectedLabelIndex];
|
||||
this.state.labelsLoading = true;
|
||||
this.render();
|
||||
try {
|
||||
await deleteLabel(this.state.config, lbl.id);
|
||||
await this.loadLabels();
|
||||
} catch (err: any) {
|
||||
this.state.error = `Failed to delete label: ${err.message}`;
|
||||
this.state.labelsLoading = false;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLabelFormKeypress(str: string, key: any) {
|
||||
if ((key && key.name === 'escape') || str === '\u001b') {
|
||||
this.state.screen = 'labels-list';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const form = this.state.labelForm;
|
||||
const fields: Array<'name' | 'color' | 'description' | 'exclusive'> = ['name', 'color', 'description', 'exclusive'];
|
||||
const currentIdx = fields.indexOf(form.activeField);
|
||||
|
||||
if (key && key.name === 'up') {
|
||||
form.activeField = fields[(currentIdx - 1 + fields.length) % fields.length];
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'down') {
|
||||
form.activeField = fields[(currentIdx + 1) % fields.length];
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'tab') {
|
||||
form.activeField = fields[(currentIdx + 1) % fields.length];
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (str === ' ' && form.activeField === 'exclusive') {
|
||||
form.exclusive = !form.exclusive;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((key && key.name === 'return') || str === '\r' || str === '\n') {
|
||||
if (form.activeField === 'exclusive') {
|
||||
form.exclusive = !form.exclusive;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
if (!form.name.trim() || !form.color.trim()) {
|
||||
this.state.error = 'Name and Color are required.';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
this.state.labelsLoading = true;
|
||||
this.state.error = null;
|
||||
this.render();
|
||||
|
||||
try {
|
||||
if (this.state.screen === 'create-label') {
|
||||
await createLabel(this.state.config, {
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim(),
|
||||
description: form.description.trim(),
|
||||
exclusive: form.exclusive
|
||||
});
|
||||
} else {
|
||||
const lbl = this.state.labels[this.state.selectedLabelIndex];
|
||||
await updateLabel(this.state.config, lbl.id, {
|
||||
name: form.name.trim(),
|
||||
color: form.color.trim(),
|
||||
description: form.description.trim(),
|
||||
exclusive: form.exclusive
|
||||
});
|
||||
}
|
||||
this.state.screen = 'labels-list';
|
||||
await this.loadLabels();
|
||||
} catch (err: any) {
|
||||
this.state.error = err.message;
|
||||
this.state.labelsLoading = false;
|
||||
this.render();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && key.name === 'backspace') {
|
||||
if (form.activeField === 'name') form.name = form.name.slice(0, -1);
|
||||
if (form.activeField === 'color') form.color = form.color.slice(0, -1);
|
||||
if (form.activeField === 'description') form.description = form.description.slice(0, -1);
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (str && !key.ctrl && !key.meta && str.length === 1 && str.charCodeAt(0) >= 32) {
|
||||
if (form.activeField === 'name') form.name += str;
|
||||
if (form.activeField === 'color') form.color += str;
|
||||
if (form.activeField === 'description') form.description += str;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
private renderLabelsList(cols: number, rows: number) {
|
||||
const header = chalk.bgBlue.white.bold(` Repository Labels - ${this.state.config.owner}/${this.state.config.repo} `);
|
||||
const headerPad = Math.max(0, Math.floor((cols - stripAnsi(header).length) / 2));
|
||||
console.log(' '.repeat(headerPad) + header);
|
||||
console.log(chalk.gray('─'.repeat(cols)));
|
||||
|
||||
if (this.state.labelsLoading) {
|
||||
console.log(chalk.yellow(` Loading labels... ${SPINNER_FRAMES[spinnerIndex]}`));
|
||||
} else if (this.state.labels.length === 0) {
|
||||
console.log(chalk.gray(' No labels found.'));
|
||||
} else {
|
||||
const listHeight = rows - 6; // header(2), footer(2), errors etc.
|
||||
let startIdx = 0;
|
||||
if (this.state.selectedLabelIndex >= listHeight) {
|
||||
startIdx = this.state.selectedLabelIndex - listHeight + 1;
|
||||
}
|
||||
|
||||
for (let i = 0; i < listHeight; i++) {
|
||||
const itemIdx = startIdx + i;
|
||||
if (itemIdx >= this.state.labels.length) break;
|
||||
|
||||
const lbl = this.state.labels[itemIdx];
|
||||
const isSelected = itemIdx === this.state.selectedLabelIndex;
|
||||
|
||||
let prefix = isSelected ? chalk.green(' > ') : ' ';
|
||||
let lblDisplay = chalk.bgHex('#' + lbl.color).black(` ${lbl.name} `);
|
||||
if (lbl.exclusive) lblDisplay += ' (exclusive)';
|
||||
|
||||
let line = prefix + lblDisplay;
|
||||
if (lbl.description) {
|
||||
line += chalk.gray(` - ${truncate(lbl.description, cols - 30)}`);
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
console.log(chalk.bgGray(line + ' '.repeat(Math.max(0, cols - stripAnsi(line).length))));
|
||||
} else {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill remaining space
|
||||
const currentLine = this.state.labelsLoading ? 3 : Math.min(this.state.labels.length, rows - 6) + 2;
|
||||
for (let i = currentLine; i < rows - 3; i++) console.log('');
|
||||
|
||||
if (this.state.error) {
|
||||
console.log(chalk.red(` Error: ${this.state.error}`));
|
||||
} else {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(chalk.gray('─'.repeat(cols)));
|
||||
console.log(chalk.gray(' [c] Create [e] Edit [d] Delete [esc] Back to Issues'));
|
||||
}
|
||||
|
||||
private renderLabelForm(cols: number, rows: number) {
|
||||
const isEdit = this.state.screen === 'edit-label';
|
||||
const title = isEdit ? 'Edit Label' : 'Create Label';
|
||||
const header = chalk.bgBlue.white.bold(` ${title} `);
|
||||
const headerPad = Math.max(0, Math.floor((cols - stripAnsi(header).length) / 2));
|
||||
console.log(' '.repeat(headerPad) + header);
|
||||
console.log(chalk.gray('─'.repeat(cols)));
|
||||
|
||||
const form = this.state.labelForm;
|
||||
const width = Math.min(60, cols - 4);
|
||||
const leftPadStr = ' '.repeat(Math.max(0, Math.floor((cols - width) / 2)));
|
||||
|
||||
console.log('');
|
||||
|
||||
// Name
|
||||
const nameLabel = form.activeField === 'name' ? chalk.cyan.bold('> Name: ') : ' Name: ';
|
||||
console.log(leftPadStr + nameLabel + (form.name || chalk.gray('(empty)')));
|
||||
console.log('');
|
||||
|
||||
// Color
|
||||
const colorLabel = form.activeField === 'color' ? chalk.cyan.bold('> Color (hex without #): ') : ' Color: ';
|
||||
console.log(leftPadStr + colorLabel + (form.color || chalk.gray('e.g. ff0000')));
|
||||
if (form.color && form.color.length >= 3) {
|
||||
try {
|
||||
console.log(leftPadStr + ' Preview: ' + chalk.bgHex('#' + form.color).black(` ${form.name || 'Label'} `));
|
||||
} catch (e) {
|
||||
console.log(leftPadStr + ' Preview: ' + chalk.red('Invalid color'));
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Description
|
||||
const descLabel = form.activeField === 'description' ? chalk.cyan.bold('> Description: ') : ' Description: ';
|
||||
console.log(leftPadStr + descLabel + (form.description || chalk.gray('(empty)')));
|
||||
console.log('');
|
||||
|
||||
// Exclusive
|
||||
const exclLabel = form.activeField === 'exclusive' ? chalk.cyan.bold('> Exclusive: ') : ' Exclusive: ';
|
||||
console.log(leftPadStr + exclLabel + (form.exclusive ? chalk.green('[x]') : '[ ]'));
|
||||
console.log('');
|
||||
|
||||
for (let i = 13; i < rows - 3; i++) console.log('');
|
||||
|
||||
if (this.state.error) {
|
||||
console.log(leftPadStr + chalk.red(` Error: ${this.state.error}`));
|
||||
} else if (this.state.labelsLoading) {
|
||||
console.log(leftPadStr + chalk.yellow(` Saving... ${SPINNER_FRAMES[spinnerIndex]}`));
|
||||
} else {
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(chalk.gray('─'.repeat(cols)));
|
||||
console.log(chalk.gray(' [↑/↓/Tab] Navigate [Enter] Save [Esc] Cancel'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
src/types.ts
20
src/types.ts
|
|
@ -16,6 +16,8 @@ export interface Label {
|
|||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
description?: string;
|
||||
exclusive?: boolean;
|
||||
}
|
||||
|
||||
export interface Issue {
|
||||
|
|
@ -70,7 +72,15 @@ export interface AddTimeForm {
|
|||
timeInput: string;
|
||||
}
|
||||
|
||||
export type ScreenType = 'launch' | 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time' | 'confirm-state-change' | 'animating-close' | 'animating-reopen' | 'set-assignees';
|
||||
export interface LabelForm {
|
||||
name: string;
|
||||
color: string;
|
||||
description: string;
|
||||
exclusive: boolean;
|
||||
activeField: 'name' | 'color' | 'description' | 'exclusive';
|
||||
}
|
||||
|
||||
export type ScreenType = 'launch' | 'setup' | 'repo-picker' | 'list' | 'details' | 'create-issue' | 'add-comment' | 'edit-issue' | 'add-time' | 'confirm-state-change' | 'animating-close' | 'animating-reopen' | 'set-assignees' | 'labels-list' | 'create-label' | 'edit-label';
|
||||
|
||||
|
||||
export interface RepoItem {
|
||||
|
|
@ -126,5 +136,13 @@ export interface AppState {
|
|||
|
||||
// Add Time Form State
|
||||
addTimeForm: AddTimeForm;
|
||||
|
||||
// Labels List State
|
||||
labels: Label[];
|
||||
selectedLabelIndex: number;
|
||||
labelsLoading: boolean;
|
||||
|
||||
// Label Form State
|
||||
labelForm: LabelForm;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue