adding blocked status to tui

This commit is contained in:
viljarb
2026-03-12 13:18:15 +01:00
parent 5b0c69e671
commit 1f98308d9b
9 changed files with 80 additions and 18 deletions

View File

@@ -70,7 +70,7 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
d.updateDetailFromSelection()
}
case !d.IsInModal() && key.Matches(msg, d.keyMap.MoveColumnRight):
if d.focusedColumn < 2 {
if d.focusedColumn < 3 {
d.focusedColumn++
d.updateDetailFromSelection()
}

View File

@@ -22,6 +22,7 @@ type Model struct {
header Header
todoList IssueList
inProgList IssueList
blockedList IssueList
doneList IssueList
issueDetail IssueDetail
helpBar components.HelpBar
@@ -30,7 +31,7 @@ type Model struct {
width int
height int
focusedColumn int // 0 = To Do, 1 = In Progress, 2 = Done
focusedColumn int // 0 = To Do, 1 = In Progress, 2 = Blocked, 3 = Done
focusOnDetail bool // true when detail pane is focused
editingTitle bool // true while we are editing a title
@@ -80,10 +81,12 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
todoIssues := components.StatusOnly(allIssues, models.StatusOpen)
inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress)
blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked)
doneIssues := components.StatusOnly(allIssues, models.StatusClosed)
m.todoList = components.NewIssueListFromIssues(app, todoIssues, 0, 0)
m.inProgList = components.NewIssueListFromIssues(app, inProgIssues, 0, 0)
m.blockedList = components.NewIssueListFromIssues(app, blockedIssues, 0, 0)
m.doneList = components.NewIssueListFromIssues(app, doneIssues, 0, 0)
m.issueDetail = components.NewIssueDetail()
m.helpBar = components.NewHelpBar(components.ViewKanban)
@@ -98,6 +101,8 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
m.issueDetail.SetIssue(selected.Issue)
} else if selected := m.inProgList.SelectedItem(); selected.ID != "" {
m.issueDetail.SetIssue(selected.Issue)
} else if selected := m.blockedList.SelectedItem(); selected.ID != "" {
m.issueDetail.SetIssue(selected.Issue)
} else if selected := m.doneList.SelectedItem(); selected.ID != "" {
m.issueDetail.SetIssue(selected.Issue)
}
@@ -196,6 +201,8 @@ func (m *Model) FocusedIssueList() *IssueList {
case 1:
return &m.inProgList
case 2:
return &m.blockedList
case 3:
return &m.doneList
default:
return &m.todoList
@@ -219,6 +226,8 @@ func statusForColumn(col int) models.Status {
case 1:
return models.StatusInProgress
case 2:
return models.StatusBlocked
case 3:
return models.StatusClosed
default:
return models.StatusOpen
@@ -235,7 +244,7 @@ func (m *Model) moveIssue(delta int) tea.Cmd {
}
newCol := m.focusedColumn + delta
if newCol < 0 || newCol > 2 {
if newCol < 0 || newCol > 3 {
return nil
}

View File

@@ -21,10 +21,12 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
todoIssues := components.StatusOnly(allIssues, models.StatusOpen)
inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress)
blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked)
doneIssues := components.StatusOnly(allIssues, models.StatusClosed)
todoCmd := m.todoList.SetIssues(todoIssues)
inProgCmd := m.inProgList.SetIssues(inProgIssues)
blockedCmd := m.blockedList.SetIssues(blockedIssues)
doneCmd := m.doneList.SetIssues(doneIssues)
var targetStatus models.Status
@@ -41,16 +43,19 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
m.focusedColumn = 0
case models.StatusInProgress:
m.focusedColumn = 1
case models.StatusClosed:
case models.StatusBlocked:
m.focusedColumn = 2
case models.StatusClosed:
m.focusedColumn = 3
}
// Select the moved issue in its new column immediately so the highlight follows it.
m.todoList.SelectIssueID(issueID)
m.inProgList.SelectIssueID(issueID)
m.blockedList.SelectIssueID(issueID)
m.doneList.SelectIssueID(issueID)
return tea.Sequence(todoCmd, inProgCmd, doneCmd)
return tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd)
}
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -109,6 +114,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case issues.SelectIssueMsg:
m.todoList.SelectIssueID(msg.IssueID)
m.inProgList.SelectIssueID(msg.IssueID)
m.blockedList.SelectIssueID(msg.IssueID)
m.doneList.SelectIssueID(msg.IssueID)
return m, nil
@@ -126,10 +132,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
todoIssues := components.StatusOnly(allIssues, models.StatusOpen)
inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress)
blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked)
doneIssues := components.StatusOnly(allIssues, models.StatusClosed)
todoCmd := m.todoList.SetIssues(todoIssues)
inProgCmd := m.inProgList.SetIssues(inProgIssues)
blockedCmd := m.blockedList.SetIssues(blockedIssues)
doneCmd := m.doneList.SetIssues(doneIssues)
// Determine the created issue from the refreshed list to ensure all fields (like ID) are populated.
@@ -145,7 +153,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.issueDetail.SetIssue(*selectedIssue)
return m, tea.Sequence(todoCmd, inProgCmd, doneCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} })
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} })
case issues.DeletedMsg:
m.confirmingDelete = false
m.deleteConfirmID = ""
@@ -159,16 +167,18 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
todoIssues := components.StatusOnly(allIssues, models.StatusOpen)
inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress)
blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked)
doneIssues := components.StatusOnly(allIssues, models.StatusClosed)
todoCmd := m.todoList.SetIssues(todoIssues)
inProgCmd := m.inProgList.SetIssues(inProgIssues)
blockedCmd := m.blockedList.SetIssues(blockedIssues)
doneCmd := m.doneList.SetIssues(doneIssues)
// If there are no issues at all, clear the detail view and return.
if len(todoIssues) == 0 && len(inProgIssues) == 0 && len(doneIssues) == 0 {
if len(todoIssues) == 0 && len(inProgIssues) == 0 && len(blockedIssues) == 0 && len(doneIssues) == 0 {
m.issueDetail.SetIssue(models.Issue{})
return m, tea.Sequence(todoCmd, inProgCmd, doneCmd)
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd)
}
// Determine which column to use for the next selection based on the current focus.
@@ -180,9 +190,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if len(inProgIssues) > 0 {
targetIssues = inProgIssues
m.focusedColumn = 1
} else if len(blockedIssues) > 0 {
targetIssues = blockedIssues
m.focusedColumn = 2
} else if len(doneIssues) > 0 {
targetIssues = doneIssues
m.focusedColumn = 2
m.focusedColumn = 3
}
}
case 1:
@@ -191,13 +204,16 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if len(todoIssues) > 0 {
targetIssues = todoIssues
m.focusedColumn = 0
} else if len(blockedIssues) > 0 {
targetIssues = blockedIssues
m.focusedColumn = 2
} else if len(doneIssues) > 0 {
targetIssues = doneIssues
m.focusedColumn = 2
m.focusedColumn = 3
}
}
case 2:
targetIssues = doneIssues
targetIssues = blockedIssues
if len(targetIssues) == 0 {
if len(inProgIssues) > 0 {
targetIssues = inProgIssues
@@ -205,6 +221,23 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} else if len(todoIssues) > 0 {
targetIssues = todoIssues
m.focusedColumn = 0
} else if len(doneIssues) > 0 {
targetIssues = doneIssues
m.focusedColumn = 3
}
}
case 3:
targetIssues = doneIssues
if len(targetIssues) == 0 {
if len(blockedIssues) > 0 {
targetIssues = blockedIssues
m.focusedColumn = 2
} else if len(inProgIssues) > 0 {
targetIssues = inProgIssues
m.focusedColumn = 1
} else if len(todoIssues) > 0 {
targetIssues = todoIssues
m.focusedColumn = 0
}
}
}
@@ -212,7 +245,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Safety: if targetIssues is still empty here, just clear detail and return.
if len(targetIssues) == 0 {
m.issueDetail.SetIssue(models.Issue{})
return m, tea.Sequence(todoCmd, inProgCmd, doneCmd)
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd)
}
newIndex := msg.PreviousIndex
@@ -221,7 +254,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
selectedIssue := targetIssues[newIndex]
m.issueDetail.SetIssue(*selectedIssue)
return m, tea.Sequence(todoCmd, inProgCmd, doneCmd, func() tea.Msg {
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
return issues.SelectIssueMsg{IssueID: selectedIssue.ID}
})
@@ -253,6 +286,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.choosingStatus = false
m.statusIssueID = ""
return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress))
case "b":
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusBlocked))
case "r":
issueID := m.statusIssueID
m.choosingStatus = false

View File

@@ -22,7 +22,7 @@ func (m *Model) View() string {
contentHeight := m.height - headerHeight - footerHeight
totalContentWidth := m.width - 1
colWidth := totalContentWidth / 3
colWidth := totalContentWidth / 4
if colWidth < 20 {
colWidth = 20
}
@@ -35,18 +35,21 @@ func (m *Model) View() string {
m.todoList.SetSize(colWidth, boardHeight-1)
m.inProgList.SetSize(colWidth, boardHeight-1)
m.blockedList.SetSize(colWidth, boardHeight-1)
m.doneList.SetSize(colWidth, boardHeight-1)
// Only highlight the selected row in the focused column.
m.todoList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 0)
m.inProgList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 1)
m.doneList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 2)
m.blockedList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 2)
m.doneList.SetHighlightSelected(!m.focusOnDetail && m.focusedColumn == 3)
// Detail view takes full width below the board.
m.issueDetail.SetSize(totalContentWidth, contentHeight-boardHeight)
todoLabel := styles.LabelStyle.Render("To Do")
inProgLabel := styles.LabelStyle.Render("In Progress")
blockedLabel := styles.LabelStyle.Render("Blocked")
doneLabel := styles.LabelStyle.Render("Done")
highlight := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
@@ -56,14 +59,17 @@ func (m *Model) View() string {
case 1:
inProgLabel = highlight.Render("In Progress ▶")
case 2:
blockedLabel = highlight.Render("Blocked ▶")
case 3:
doneLabel = highlight.Render("Done ▶")
}
todoCol := lipgloss.JoinVertical(lipgloss.Left, todoLabel, m.todoList.View())
inProgCol := lipgloss.JoinVertical(lipgloss.Left, inProgLabel, m.inProgList.View())
blockedCol := lipgloss.JoinVertical(lipgloss.Left, blockedLabel, m.blockedList.View())
doneCol := lipgloss.JoinVertical(lipgloss.Left, doneLabel, m.doneList.View())
board := lipgloss.JoinHorizontal(lipgloss.Left, todoCol, inProgCol, doneCol)
board := lipgloss.JoinHorizontal(lipgloss.Left, todoCol, inProgCol, blockedCol, doneCol)
content := lipgloss.JoinVertical(lipgloss.Left, board, m.issueDetail.View())
// Add spacer to lock footer to bottom of screen when content is shorter than available space