UNPKG

@fromsvenwithlove/devops-issues-cli

Version:

AI-powered CLI tool and library for Azure DevOps work item management with Claude agents

280 lines (235 loc) 7.97 kB
import chalk from 'chalk'; import { canHaveChildren } from '../utils/work-item-types.js'; import { keypressManager } from '../utils/keypress-manager.js'; export class TreeSelector { constructor(workItems) { this.originalItems = workItems; this.tree = this.buildSelectableTree(workItems); this.flatList = this.buildFlatList(this.tree); this.selectedIndex = 0; this.running = false; this.selectedItem = null; this.handlerId = null; } buildSelectableTree(workItems) { // Filter only items that can have children const eligibleItems = workItems.filter(item => canHaveChildren(item.type)); // Create a map for quick lookup const itemMap = new Map(); eligibleItems.forEach(item => { const id = item.id; const title = item.title || item.fields?.['System.Title'] || 'No Title'; const type = item.type || item.fields?.['System.WorkItemType'] || 'Unknown'; const state = item.state || item.fields?.['System.State'] || 'Unknown'; const assignedTo = item.assignedTo || item.fields?.['System.AssignedTo']?.displayName || 'Unassigned'; itemMap.set(id, { id: id, title: title, type: type, state: state, assignedTo: assignedTo, children: [], expanded: false, level: 0, canSelect: true }); }); // Build parent-child relationships const rootItems = []; eligibleItems.forEach(item => { const parentId = item.parentId || item.fields?.['System.Parent']; const currentItem = itemMap.get(item.id); if (parentId && itemMap.has(parentId)) { const parent = itemMap.get(parentId); parent.children.push(currentItem); currentItem.parent = parent; } else { rootItems.push(currentItem); } }); // Set levels after all relationships are built function setLevels(items, level = 0) { items.forEach(item => { item.level = level; if (item.children.length > 0) { setLevels(item.children, level + 1); } }); } setLevels(rootItems); // Expand root nodes by default rootItems.forEach(item => { item.expanded = true; }); return rootItems; } buildFlatList(tree) { const list = []; function addToList(items, level = 0) { items.forEach(item => { item.level = level; list.push(item); if (item.expanded && item.children.length > 0) { addToList(item.children, level + 1); } }); } addToList(tree); // Add cancel option list.push({ id: null, title: 'Cancel', type: 'cancel', level: 0, canSelect: true, isCancel: true }); return list; } rebuildFlatList() { this.flatList = this.buildFlatList(this.tree); // Ensure selected index is still valid if (this.selectedIndex >= this.flatList.length) { this.selectedIndex = Math.max(0, this.flatList.length - 1); } } getTypeIcon(type) { const icons = { 'Epic': '📋', 'Feature': '🔹', 'User Story': '📝', 'Bug': '🐛', 'Task': '⚙️', 'cancel': '❌' }; return icons[type] || '📄'; } getStateColor(state) { const colors = { 'Active': chalk.green, 'New': chalk.blue, 'Resolved': chalk.gray, 'Closed': chalk.gray, 'Done': chalk.gray }; return colors[state] || chalk.white; } countChildren(item) { return item.children ? item.children.reduce((count, child) => count + 1 + this.countChildren(child), 0) : 0; } renderTree() { console.clear(); console.log(chalk.bold.blue('Select Parent Work Item\n')); if (this.flatList.length <= 1) { // Only cancel option console.log(chalk.yellow('No eligible parent work items found.')); console.log(chalk.dim('Supported parent types: Epic, Feature, User Story, Task, Bug\n')); console.log(chalk.dim('Press q to cancel')); return; } this.flatList.forEach((item, index) => { const isSelected = index === this.selectedIndex; const prefix = isSelected ? chalk.cyan('> ') : ' '; if (item.isCancel) { const line = `${prefix}${this.getTypeIcon(item.type)} ${chalk.red(item.title)}`; if (isSelected) { console.log(chalk.inverse(line)); } else { console.log(line); } return; } const indent = ' '.repeat(item.level); const icon = this.getTypeIcon(item.type); const stateColor = this.getStateColor(item.state); const expandIcon = item.children.length > 0 ? (item.expanded ? '▼ ' : '▶ ') : ''; const childrenInfo = item.children.length > 0 && !item.expanded ? chalk.dim(` (${item.children.length} children)`) : ''; const title = `${icon} ${expandIcon}${item.title} (#${item.id})${childrenInfo}`; const state = stateColor(`[${item.state}]`); const line = `${prefix}${indent}${title} ${state}`; if (isSelected) { console.log(chalk.inverse(line)); } else { console.log(line); } }); console.log(chalk.dim('\nNavigation: ↑↓ select, → expand, ← collapse, Enter confirm, q cancel')); } async selectParent() { return new Promise((resolve) => { this.running = true; // Generate unique handler ID this.handlerId = `tree-selector-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; // Try to register with keypress manager if (!keypressManager.addHandler(this.handlerId, this.createKeypressHandler(resolve))) { console.log(chalk.red('Tree selector requires an interactive terminal. Please run in a proper terminal/console.')); resolve(null); return; } this.renderTree(); }); } createKeypressHandler(resolve) { return async (ch, key) => { if (!this.running) return; if (key && (key.name === 'q' || (key.ctrl && key.name === 'c'))) { this.cleanup().then(() => resolve(null)); return; } if (key && key.name === 'up') { this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.renderTree(); } if (key && key.name === 'down') { this.selectedIndex = Math.min(this.flatList.length - 1, this.selectedIndex + 1); this.renderTree(); } if (key && key.name === 'right') { const item = this.flatList[this.selectedIndex]; if (item && item.children && item.children.length > 0 && !item.expanded) { item.expanded = true; this.rebuildFlatList(); this.renderTree(); } } if (key && key.name === 'left') { const item = this.flatList[this.selectedIndex]; if (item && item.expanded) { item.expanded = false; this.rebuildFlatList(); this.renderTree(); } } if (key && key.name === 'return') { const selectedItem = this.flatList[this.selectedIndex]; // Wait for cleanup to complete before resolving this.cleanup().then(() => { if (selectedItem.isCancel) { resolve(null); } else { resolve(selectedItem.id); } }); return; } }; } cleanup() { this.running = false; // Remove this handler from the global keypress manager if (this.handlerId) { keypressManager.removeHandler(this.handlerId); this.handlerId = null; } // Return promise to allow waiting for cleanup completion return new Promise(resolve => { // Small delay to ensure cleanup completes setTimeout(() => { resolve(); }, 50); }); } }