Initial commit: Azure DevOps TUI client
A terminal-based user interface for browsing and managing Azure DevOps work items. Features include: - Browse work items with filtering by area, iteration, state, and type - View work item details with markdown rendering - Open work items in browser - Create git branches from work items - Update work item state - Keyboard-driven navigation 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/samuelenocsson/devops-tui/internal/models"
|
||||
)
|
||||
|
||||
// classificationNode represents an area node from the API
|
||||
type classificationNode struct {
|
||||
ID int `json:"id"`
|
||||
Identifier string `json:"identifier"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
HasChildren bool `json:"hasChildren"`
|
||||
Children []classificationNode `json:"children"`
|
||||
}
|
||||
|
||||
// GetAreas fetches all areas for the project
|
||||
func (c *Client) GetAreas() ([]models.Area, error) {
|
||||
// Use the classification nodes API with depth to get area hierarchy
|
||||
resp, err := c.get("/wit/classificationnodes/areas?$depth=10")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rootNode classificationNode
|
||||
if err := decode(resp, &rootNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Flatten the tree into a list
|
||||
areas := flattenAreas(rootNode, "")
|
||||
|
||||
// Sort areas by path for consistent ordering
|
||||
sort.Slice(areas, func(i, j int) bool {
|
||||
return areas[i].Path < areas[j].Path
|
||||
})
|
||||
|
||||
return areas, nil
|
||||
}
|
||||
|
||||
// flattenAreas recursively flattens the area tree
|
||||
func flattenAreas(node classificationNode, parentPath string) []models.Area {
|
||||
var areas []models.Area
|
||||
|
||||
// Build the path
|
||||
path := node.Path
|
||||
if path == "" {
|
||||
path = node.Name
|
||||
}
|
||||
|
||||
// Clean up the path:
|
||||
// 1. Remove leading backslash
|
||||
// 2. Remove "\Area" from path (API returns \Project\Area\Team but work items use Project\Team)
|
||||
path = strings.TrimPrefix(path, "\\")
|
||||
path = strings.TrimSuffix(path, "\\")
|
||||
|
||||
// Remove the "Area" segment from the path (e.g., "Project\Area\Team" -> "Project\Team")
|
||||
parts := strings.Split(path, "\\")
|
||||
if len(parts) >= 2 && parts[1] == "Area" {
|
||||
// Remove the "Area" part
|
||||
newParts := []string{parts[0]}
|
||||
if len(parts) > 2 {
|
||||
newParts = append(newParts, parts[2:]...)
|
||||
}
|
||||
path = strings.Join(newParts, "\\")
|
||||
}
|
||||
|
||||
// Add this node
|
||||
areas = append(areas, models.Area{
|
||||
ID: node.ID,
|
||||
Name: node.Name,
|
||||
Path: path,
|
||||
})
|
||||
|
||||
// Recursively add children
|
||||
for _, child := range node.Children {
|
||||
childAreas := flattenAreas(child, path)
|
||||
areas = append(areas, childAreas...)
|
||||
}
|
||||
|
||||
return areas
|
||||
}
|
||||
|
||||
// GetAreaDisplayName returns a shortened display name for an area
|
||||
func GetAreaDisplayName(path string) string {
|
||||
parts := strings.Split(path, "\\")
|
||||
if len(parts) > 1 {
|
||||
// Return the last part for brevity
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return path
|
||||
}
|
||||
Reference in New Issue
Block a user