Add sorting, user assignment, and UI improvements

Features:
- Add work item sorting by ID, Type, State (keys 1, 2, 3)
- Add user assignment modal with team member filtering (key a)
- Add parent work item title display in details panel
- Preserve sort order and selection on panel reload

UI improvements:
- Remove zebra striping from work items list
- Remove priority column from list and details
- Align metadata fields in details panel
- Add markdown rendering for descriptions (using glamour)
- Add state colors: To Do (orange), In Progress (purple), Done (green), Testing (yellow)

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel Enocsson
2025-12-04 22:31:28 +01:00
parent 2555afce19
commit 7c488b2d83
7 changed files with 682 additions and 12 deletions
+62 -1
View File
@@ -42,6 +42,7 @@ type App struct {
helpPanel components.HelpPanel
stateModal components.StateModal
branchModal components.BranchModal
assignModal components.AssignModal
// State
activePanel Panel
@@ -55,6 +56,7 @@ type App struct {
areas []models.Area
workItems []models.WorkItem
statesByType map[string][]models.WorkItemStateInfo
teamMembers []models.TeamMember
// Services
client *api.Client
@@ -84,6 +86,7 @@ func NewApp(client *api.Client) App {
helpPanel: components.NewHelpPanel(keys, styles),
stateModal: components.NewStateModal(styles, keys),
branchModal: components.NewBranchModal(styles, keys),
assignModal: components.NewAssignModal(styles, keys),
activePanel: PanelWorkItems,
viewMode: ViewMain,
loading: true,
@@ -130,6 +133,15 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Batch(cmds...)
}
if a.assignModal.IsVisible() {
newModal, cmd := a.assignModal.Update(msg)
a.assignModal = newModal
if cmd != nil {
cmds = append(cmds, cmd)
}
return a, tea.Batch(cmds...)
}
// Global keys
if key.Matches(msg, a.keys.Quit) && !a.helpPanel.IsVisible() && a.viewMode == ViewMain {
return a, tea.Quit
@@ -195,6 +207,17 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
// Open assign modal (only when work items panel is active)
if key.Matches(msg, a.keys.Assign) && a.activePanel == PanelWorkItems {
if item := a.workItemsPanel.SelectedItem(); item != nil {
a.assignModal.SetItem(item)
a.assignModal.SetMembers(a.teamMembers)
a.assignModal.SetSize(a.width, a.height)
a.assignModal.SetVisible(true)
return a, nil
}
}
// Update active panel
switch a.activePanel {
case PanelFilter:
@@ -215,6 +238,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.iterations = msg.iterations
a.areas = msg.areas
a.statesByType = msg.statesByType
a.teamMembers = msg.teamMembers
a.stateModal.SetStatesByType(a.statesByType)
filterState := models.NewFilterState(a.iterations, a.areas, a.statesByType)
@@ -268,6 +292,7 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Modal was closed, nothing special to do
a.stateModal.SetVisible(false)
a.branchModal.SetVisible(false)
a.assignModal.SetVisible(false)
case components.StateChangeRequestMsg:
a.stateModal.SetVisible(false)
@@ -289,6 +314,17 @@ func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case components.BranchCreateErrorMsg:
a.err = msg.Err
case components.AssignRequestMsg:
a.assignModal.SetVisible(false)
a.loading = true
return a, assignWorkItemCmd(a.client, msg.Item.ID, msg.UserEmail, msg.UserName, a.filterPanel.FilterState())
case assignedMsg:
a.loading = false
a.statusMsg = fmt.Sprintf("Assigned to %s", msg.userName)
// Refresh work items to show updated assignment
return a, loadWorkItemsCmd(a.client, a.filterPanel.FilterState())
}
// Update selected item in details panel
@@ -313,6 +349,11 @@ func (a App) View() string {
return a.branchModal.View()
}
// Render assign modal if visible
if a.assignModal.IsVisible() {
return a.assignModal.View()
}
// Render help overlay if visible
if a.helpPanel.IsVisible() {
_ = a.renderMainView()
@@ -455,6 +496,7 @@ type dataLoadedMsg struct {
iterations []models.Iteration
areas []models.Area
statesByType map[string][]models.WorkItemStateInfo
teamMembers []models.TeamMember
}
type workItemsLoadedMsg struct {
@@ -469,6 +511,10 @@ type stateChangedMsg struct {
newState string
}
type assignedMsg struct {
userName string
}
// Commands
func loadDataCmd(client *api.Client) tea.Cmd {
@@ -486,7 +532,12 @@ func loadDataCmd(client *api.Client) tea.Cmd {
// Non-fatal - we can still work with hardcoded states
statesByType = make(map[string][]models.WorkItemStateInfo)
}
return dataLoadedMsg{iterations: iterations, areas: areas, statesByType: statesByType}
teamMembers, err := client.GetTeamMembers()
if err != nil {
// Non-fatal - we can still work without team members
teamMembers = []models.TeamMember{}
}
return dataLoadedMsg{iterations: iterations, areas: areas, statesByType: statesByType, teamMembers: teamMembers}
}
}
@@ -530,3 +581,13 @@ func createBranchCmd(branchName string) tea.Cmd {
return components.BranchCreatedMsg{BranchName: branchName}
}
}
func assignWorkItemCmd(client *api.Client, itemID int, userEmail, userName string, filterState *models.FilterState) tea.Cmd {
return func() tea.Msg {
err := client.AssignWorkItem(itemID, userEmail)
if err != nil {
return errMsg{err: err}
}
return assignedMsg{userName: userName}
}
}