LPM-140 The Sprinting Update Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer. Changes: Introduces sprint entities in storage and exposes sprint operations via IssueService. Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes. Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
635 lines
19 KiB
Go
635 lines
19 KiB
Go
package kanban
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"charm.land/bubbles/v2/list"
|
|
tea "charm.land/bubbletea/v2"
|
|
"github.com/LazyBachelor/LazyPM/internal/models"
|
|
"github.com/LazyBachelor/LazyPM/internal/utils/user"
|
|
"github.com/LazyBachelor/LazyPM/pkg/tui/components"
|
|
"github.com/LazyBachelor/LazyPM/pkg/tui/modal"
|
|
"github.com/LazyBachelor/LazyPM/pkg/tui/msgs"
|
|
)
|
|
|
|
func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
|
|
ctx := context.Background()
|
|
|
|
backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(ctx)
|
|
backlogCmd := m.backlogList.SetIssues(backlogIssues)
|
|
|
|
sprintIssues, _ := m.app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum)
|
|
todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen)
|
|
inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress)
|
|
blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked)
|
|
doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed)
|
|
|
|
todoCmd := m.todoList.SetIssues(todoIssues)
|
|
inProgCmd := m.inProgList.SetIssues(inProgIssues)
|
|
blockedCmd := m.blockedList.SetIssues(blockedIssues)
|
|
doneCmd := m.doneList.SetIssues(doneIssues)
|
|
|
|
// Find the issue to determine which column it belongs to
|
|
var targetStatus models.Status
|
|
var inBacklog bool
|
|
for _, issue := range sprintIssues {
|
|
if issue == nil {
|
|
continue
|
|
}
|
|
if issue.ID == issueID {
|
|
targetStatus = issue.Status
|
|
m.setDetailIssueWithComments(*issue)
|
|
break
|
|
}
|
|
}
|
|
// Check if issue is in backlog
|
|
if targetStatus == "" {
|
|
for _, issue := range backlogIssues {
|
|
if issue == nil {
|
|
continue
|
|
}
|
|
if issue.ID == issueID {
|
|
inBacklog = true
|
|
m.setDetailIssueWithComments(*issue)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case inBacklog:
|
|
m.focusManager.SetCurrent(modal.FocusColumn0)
|
|
case targetStatus == models.StatusOpen:
|
|
m.focusManager.SetCurrent(modal.FocusColumn1)
|
|
case targetStatus == models.StatusInProgress:
|
|
m.focusManager.SetCurrent(modal.FocusColumn2)
|
|
case targetStatus == models.StatusBlocked:
|
|
m.focusManager.SetCurrent(modal.FocusColumn3)
|
|
case targetStatus == models.StatusClosed:
|
|
m.focusManager.SetCurrent(modal.FocusColumn4)
|
|
}
|
|
|
|
return tea.Sequence(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
|
|
return msgs.SelectIssueMsg{IssueID: issueID}
|
|
})
|
|
}
|
|
|
|
func (m *Model) refreshAndSubmit(issueID string) tea.Cmd {
|
|
refreshCmd := m.refreshIssueListsAndSelectIssue(issueID)
|
|
m.submitValidation()
|
|
return refreshCmd
|
|
}
|
|
|
|
// refreshAllIssueLists refreshes all issue lists without selecting a specific issue
|
|
func (m *Model) refreshAllIssueLists() tea.Cmd {
|
|
ctx := context.Background()
|
|
|
|
backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(ctx)
|
|
backlogCmd := m.backlogList.SetIssues(backlogIssues)
|
|
|
|
sprintIssues, _ := m.app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum)
|
|
todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen)
|
|
inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress)
|
|
blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked)
|
|
doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed)
|
|
|
|
todoCmd := m.todoList.SetIssues(todoIssues)
|
|
inProgCmd := m.inProgList.SetIssues(inProgIssues)
|
|
blockedCmd := m.blockedList.SetIssues(blockedIssues)
|
|
doneCmd := m.doneList.SetIssues(doneIssues)
|
|
|
|
return tea.Batch(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd)
|
|
}
|
|
|
|
// Modal action handlers
|
|
|
|
func (m *Model) startConfirmExit() tea.Cmd {
|
|
return m.modalManager.ShowModal(modal.ModalConfirmExit)
|
|
}
|
|
|
|
func (m *Model) startEditTitle(selected ListIssue) tea.Cmd {
|
|
m.currentIssueID = selected.ID
|
|
titleModal := m.modalManager.GetTextInputModal(modal.ModalEditTitle)
|
|
if titleModal != nil {
|
|
titleModal.SetValue(selected.Issue.Title)
|
|
titleModal.CursorEnd()
|
|
return m.modalManager.ShowModal(modal.ModalEditTitle)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) startEditDescription(selected ListIssue) tea.Cmd {
|
|
m.currentIssueID = selected.ID
|
|
descModal := m.modalManager.GetTextAreaModal(modal.ModalEditDescription)
|
|
if descModal != nil {
|
|
descModal.SetValue(selected.Issue.Description)
|
|
return m.modalManager.ShowModal(modal.ModalEditDescription)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) startCreateIssue() tea.Cmd {
|
|
createModal := m.modalManager.GetTextInputModal(modal.ModalCreateIssue)
|
|
if createModal != nil {
|
|
createModal.Reset()
|
|
return m.modalManager.ShowModal(modal.ModalCreateIssue)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) startConfirmDelete(issueID string, index int) tea.Cmd {
|
|
m.currentIssueID = issueID
|
|
m.deleteIndex = index
|
|
deleteModal := m.modalManager.GetConfirmModal(modal.ModalConfirmDelete)
|
|
if deleteModal != nil {
|
|
return m.modalManager.ShowModal(modal.ModalConfirmDelete)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) startChooseStatus(selected ListIssue) tea.Cmd {
|
|
m.currentIssueID = selected.ID
|
|
return m.modalManager.ShowModal(modal.ModalSelectStatus)
|
|
}
|
|
|
|
func (m *Model) startChoosePriority(selected ListIssue) tea.Cmd {
|
|
m.currentIssueID = selected.ID
|
|
return m.modalManager.ShowModal(modal.ModalSelectPriority)
|
|
}
|
|
|
|
func (m *Model) startChooseType(selected ListIssue) tea.Cmd {
|
|
m.currentIssueID = selected.ID
|
|
return m.modalManager.ShowModal(modal.ModalSelectType)
|
|
}
|
|
|
|
func (m *Model) startSelectSprint() tea.Cmd {
|
|
sprints, err := m.app.Issues.GetSprints(context.Background())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
options := make([]modal.SelectOption, 0, len(sprints))
|
|
for _, sprintNum := range sprints {
|
|
if sprintNum == m.backlogNum {
|
|
continue
|
|
}
|
|
label := fmt.Sprintf("Sprint %d", sprintNum)
|
|
key := fmt.Sprintf("%d", sprintNum)
|
|
if sprintNum > 9 {
|
|
continue
|
|
}
|
|
options = append(options, modal.SelectOption{
|
|
Key: key,
|
|
Label: label,
|
|
Value: fmt.Sprintf("%d", sprintNum),
|
|
})
|
|
}
|
|
|
|
if len(options) == 0 {
|
|
return nil
|
|
}
|
|
|
|
sprintModal := modal.NewSelectModal(modal.SelectConfig{
|
|
ID: "dynamic-sprint-select",
|
|
Label: "Select sprint to view:",
|
|
Options: options,
|
|
CancelKey: "esc",
|
|
})
|
|
|
|
return m.modalManager.PushModal(sprintModal)
|
|
}
|
|
|
|
func (m *Model) startCreateSprint() tea.Cmd {
|
|
return tea.Sequence(
|
|
func() tea.Msg {
|
|
ctx := context.Background()
|
|
newSprintNum, err := m.app.Issues.AddSprint(ctx)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
m.currentSprintNum = newSprintNum
|
|
return nil
|
|
},
|
|
m.refreshAllIssueLists(),
|
|
)
|
|
}
|
|
|
|
func (m *Model) startEditAssignee(selected ListIssue) tea.Cmd {
|
|
m.currentIssueID = selected.ID
|
|
assigneeModal := m.modalManager.GetTextInputModal(modal.ModalEditAssignee)
|
|
if assigneeModal != nil {
|
|
assigneeModal.SetValue(selected.Assignee)
|
|
assigneeModal.CursorEnd()
|
|
return m.modalManager.ShowModal(modal.ModalEditAssignee)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) startAddComment(selected ListIssue) tea.Cmd {
|
|
m.currentIssueID = selected.ID
|
|
commentModal := m.modalManager.GetTextAreaModal(modal.ModalAddComment)
|
|
if commentModal != nil {
|
|
commentModal.Reset()
|
|
return m.modalManager.ShowModal(modal.ModalAddComment)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// handleModalCompleted handles all modal completion messages
|
|
func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd {
|
|
switch msg.ModalID {
|
|
case modal.ModalConfirmExit:
|
|
if r, ok := msg.Value.(modal.ConfirmResult); ok && r.Confirmed {
|
|
m.logAction("tui confirmed exit")
|
|
return tea.Quit
|
|
}
|
|
case modal.ModalEditTitle:
|
|
if r, ok := msg.Value.(modal.TextInputResult); ok {
|
|
cmd := msgs.UpdateIssueTitleCmd(m.app, m.currentIssueID, r.Value)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalCreateIssue:
|
|
if r, ok := msg.Value.(modal.TextInputResult); ok && r.Value != "" {
|
|
cmd := msgs.CreateIssueCmd(m.app, r.Value)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalEditAssignee:
|
|
if r, ok := msg.Value.(modal.TextInputResult); ok {
|
|
cmd := msgs.UpdateIssueAssigneeCmd(m.app, m.currentIssueID, r.Value)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalEditDescription:
|
|
if r, ok := msg.Value.(modal.TextAreaResult); ok {
|
|
cmd := msgs.UpdateIssueDescriptionCmd(m.app, m.currentIssueID, r.Value)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalConfirmDelete:
|
|
if r, ok := msg.Value.(modal.ConfirmResult); ok && r.Confirmed {
|
|
idx := m.deleteIndex
|
|
issueID := m.currentIssueID
|
|
m.deleteIndex = -1
|
|
m.currentIssueID = ""
|
|
cmd := msgs.DeleteIssueCmd(m.app, issueID, idx)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalSelectStatus:
|
|
if r, ok := msg.Value.(modal.SelectResult); ok {
|
|
if r.SelectedValue == "closing" {
|
|
return m.modalManager.ShowModal(modal.ModalSelectCloseReason)
|
|
}
|
|
cmd := msgs.UpdateIssueStatusCmd(m.app, m.currentIssueID, r.SelectedValue)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalSelectCloseReason:
|
|
if r, ok := msg.Value.(modal.SelectResult); ok {
|
|
if r.SelectedValue == "other" {
|
|
return m.modalManager.ShowModal(modal.ModalCloseReason)
|
|
}
|
|
cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.SelectedValue)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalSelectPriority:
|
|
if r, ok := msg.Value.(modal.SelectResult); ok {
|
|
priority, err := strconv.Atoi(r.SelectedValue)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
cmd := msgs.UpdateIssuePriorityCmd(m.app, m.currentIssueID, priority)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalSelectType:
|
|
if r, ok := msg.Value.(modal.SelectResult); ok {
|
|
issueType := models.IssueType(r.SelectedValue)
|
|
cmd := msgs.UpdateIssueTypeCmd(m.app, m.currentIssueID, issueType)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalSelectSprint, "dynamic-sprint-select":
|
|
if r, ok := msg.Value.(modal.SelectResult); ok {
|
|
sprintNum, err := strconv.Atoi(r.SelectedValue)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
m.currentSprintNum = sprintNum
|
|
return m.refreshAllIssueLists()
|
|
}
|
|
case modal.ModalCloseReason:
|
|
if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" {
|
|
cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.Value)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
case modal.ModalAddComment:
|
|
if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" {
|
|
cmd := msgs.AddIssueCommentCmd(m.app, m.currentIssueID, user.GetOsUsername(), r.Value)
|
|
return func() tea.Msg { return cmd() }
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// handleModalCancelled handles all modal cancellation messages
|
|
func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) {
|
|
switch msg.ModalID {
|
|
case modal.ModalConfirmExit:
|
|
m.logAction("tui canceled exit")
|
|
case modal.ModalEditTitle:
|
|
m.currentIssueID = ""
|
|
case modal.ModalCreateIssue:
|
|
// No cleanup needed
|
|
case modal.ModalEditAssignee:
|
|
m.currentIssueID = ""
|
|
case modal.ModalEditDescription:
|
|
m.currentIssueID = ""
|
|
case modal.ModalConfirmDelete:
|
|
m.deleteIndex = -1
|
|
m.currentIssueID = ""
|
|
case modal.ModalSelectStatus:
|
|
m.currentIssueID = ""
|
|
case modal.ModalSelectCloseReason:
|
|
m.currentIssueID = ""
|
|
case modal.ModalSelectPriority:
|
|
m.currentIssueID = ""
|
|
case modal.ModalSelectType:
|
|
m.currentIssueID = ""
|
|
case modal.ModalSelectSprint, "dynamic-sprint-select":
|
|
case modal.ModalCloseReason:
|
|
m.currentIssueID = ""
|
|
case modal.ModalAddComment:
|
|
m.currentIssueID = ""
|
|
}
|
|
}
|
|
|
|
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
if cmd, handled := m.modalManager.Update(msg); handled {
|
|
return m, cmd
|
|
}
|
|
|
|
switch msg := msg.(type) {
|
|
case modal.ModalCompletedMsg:
|
|
return m, m.handleModalCompleted(msg)
|
|
|
|
case modal.ModalCancelledMsg:
|
|
m.handleModalCancelled(msg)
|
|
return m, nil
|
|
case msgs.TitleUpdatedMsg:
|
|
m.modalManager.GetTextInputModal(modal.ModalEditTitle).Reset()
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
return m, m.refreshAndSubmit(msg.IssueID)
|
|
|
|
case msgs.DescriptionUpdatedMsg:
|
|
m.modalManager.GetTextAreaModal(modal.ModalEditDescription).Reset()
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
return m, m.refreshAndSubmit(msg.IssueID)
|
|
|
|
case msgs.StatusUpdatedMsg:
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
return m, m.refreshAndSubmit(msg.IssueID)
|
|
|
|
case msgs.PriorityUpdatedMsg:
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
return m, m.refreshAndSubmit(msg.IssueID)
|
|
|
|
case msgs.TypeUpdatedMsg:
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
return m, m.refreshAndSubmit(msg.IssueID)
|
|
|
|
case msgs.AssigneeUpdatedMsg:
|
|
m.modalManager.GetTextInputModal(modal.ModalEditAssignee).Reset()
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
return m, m.refreshAndSubmit(msg.IssueID)
|
|
|
|
case msgs.IssueCommentAddedMsg:
|
|
m.modalManager.GetTextAreaModal(modal.ModalAddComment).Reset()
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
m.submitValidation()
|
|
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
|
|
|
|
case msgs.SelectIssueMsg:
|
|
m.backlogList.SelectIssueID(msg.IssueID)
|
|
m.todoList.SelectIssueID(msg.IssueID)
|
|
m.inProgList.SelectIssueID(msg.IssueID)
|
|
m.blockedList.SelectIssueID(msg.IssueID)
|
|
m.doneList.SelectIssueID(msg.IssueID)
|
|
return m, nil
|
|
|
|
case msgs.CreatedMsg:
|
|
m.modalManager.GetTextInputModal(modal.ModalCreateIssue).Reset()
|
|
if msg.Err != nil || msg.Issue == nil {
|
|
return m, nil
|
|
}
|
|
|
|
backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(context.Background())
|
|
backlogCmd := m.backlogList.SetIssues(backlogIssues)
|
|
|
|
sprintIssues, _ := m.app.Issues.GetIssuesBySprint(context.Background(), m.currentSprintNum)
|
|
todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen)
|
|
inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress)
|
|
blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked)
|
|
doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed)
|
|
|
|
todoCmd := m.todoList.SetIssues(todoIssues)
|
|
inProgCmd := m.inProgList.SetIssues(inProgIssues)
|
|
blockedCmd := m.blockedList.SetIssues(blockedIssues)
|
|
doneCmd := m.doneList.SetIssues(doneIssues)
|
|
|
|
allIssues := append(backlogIssues, sprintIssues...)
|
|
|
|
selectedIssue := msg.Issue
|
|
if selectedIssue.ID == "" {
|
|
for _, issue := range allIssues {
|
|
if issue.Title == msg.Issue.Title {
|
|
selectedIssue = issue
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
m.setDetailIssueWithComments(*selectedIssue)
|
|
m.submitValidation()
|
|
return m, tea.Sequence(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
|
|
return msgs.SelectIssueMsg{IssueID: selectedIssue.ID}
|
|
})
|
|
|
|
case msgs.DeletedMsg:
|
|
m.deleteIndex = -1
|
|
m.currentIssueID = ""
|
|
if msg.Err != nil {
|
|
return m, nil
|
|
}
|
|
allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
|
if err != nil {
|
|
return m, nil
|
|
}
|
|
|
|
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 len(todoIssues) == 0 && len(inProgIssues) == 0 && len(blockedIssues) == 0 && len(doneIssues) == 0 {
|
|
m.setDetailIssueWithComments(models.Issue{})
|
|
m.submitValidation()
|
|
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd)
|
|
}
|
|
|
|
var targetIssues []*models.Issue
|
|
switch m.focusManager.Current() {
|
|
case modal.FocusColumn1:
|
|
targetIssues = todoIssues
|
|
if len(targetIssues) == 0 {
|
|
if len(inProgIssues) > 0 {
|
|
targetIssues = inProgIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn2)
|
|
} else if len(blockedIssues) > 0 {
|
|
targetIssues = blockedIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn3)
|
|
} else if len(doneIssues) > 0 {
|
|
targetIssues = doneIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn4)
|
|
}
|
|
}
|
|
case modal.FocusColumn2:
|
|
targetIssues = inProgIssues
|
|
if len(targetIssues) == 0 {
|
|
if len(todoIssues) > 0 {
|
|
targetIssues = todoIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn1)
|
|
} else if len(blockedIssues) > 0 {
|
|
targetIssues = blockedIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn3)
|
|
} else if len(doneIssues) > 0 {
|
|
targetIssues = doneIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn4)
|
|
}
|
|
}
|
|
case modal.FocusColumn3:
|
|
targetIssues = blockedIssues
|
|
if len(targetIssues) == 0 {
|
|
if len(inProgIssues) > 0 {
|
|
targetIssues = inProgIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn2)
|
|
} else if len(todoIssues) > 0 {
|
|
targetIssues = todoIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn1)
|
|
} else if len(doneIssues) > 0 {
|
|
targetIssues = doneIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn4)
|
|
}
|
|
}
|
|
case modal.FocusColumn4:
|
|
targetIssues = doneIssues
|
|
if len(targetIssues) == 0 {
|
|
if len(blockedIssues) > 0 {
|
|
targetIssues = blockedIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn3)
|
|
} else if len(inProgIssues) > 0 {
|
|
targetIssues = inProgIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn2)
|
|
} else if len(todoIssues) > 0 {
|
|
targetIssues = todoIssues
|
|
m.focusManager.SetCurrent(modal.FocusColumn1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(targetIssues) == 0 {
|
|
m.setDetailIssueWithComments(models.Issue{})
|
|
m.submitValidation()
|
|
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd)
|
|
}
|
|
|
|
newIndex := msg.PreviousIndex
|
|
if newIndex >= len(targetIssues) {
|
|
newIndex = len(targetIssues) - 1
|
|
}
|
|
selectedIssue := targetIssues[newIndex]
|
|
m.setDetailIssueWithComments(*selectedIssue)
|
|
m.submitValidation()
|
|
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
|
|
return msgs.SelectIssueMsg{IssueID: selectedIssue.ID}
|
|
})
|
|
|
|
case tea.KeyPressMsg:
|
|
if msg.String() == "ctrl+c" {
|
|
return m, tea.Quit
|
|
}
|
|
|
|
fl := m.FocusedIssueList()
|
|
if fl.FilterState() == list.Filtering {
|
|
cmd, _ := fl.Update(msg)
|
|
return m, cmd
|
|
}
|
|
|
|
if msg.String() == "esc" {
|
|
return m, nil
|
|
}
|
|
|
|
cmd := m.handleKeyPressMsg(msg)
|
|
if cmd != nil || m.IsInModal() {
|
|
return m, cmd
|
|
}
|
|
|
|
case components.ValidationFeedbackMsg:
|
|
m.currentFeedback = msg.Feedback
|
|
if msg.Feedback.Success {
|
|
m.showComplete = true
|
|
return m, tea.Quit
|
|
}
|
|
return m, components.ListenForValidation(m.feedbackChan)
|
|
|
|
case tea.WindowSizeMsg:
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
m.modalManager.SetSize(msg.Width, msg.Height)
|
|
return m, nil
|
|
}
|
|
|
|
fl := m.FocusedIssueList()
|
|
cmd, changed := fl.Update(msg)
|
|
if changed {
|
|
if selected := fl.SelectedItem(); selected.ID != "" {
|
|
m.setDetailIssueWithComments(selected.Issue)
|
|
}
|
|
}
|
|
|
|
// Only propagate non-key messages to other lists (SetItems, etc.)
|
|
// Key messages should only affect the focused list
|
|
if _, isKeyMsg := msg.(tea.KeyPressMsg); !isKeyMsg {
|
|
// Update all lists to ensure they receive commands like SetItems
|
|
todoCmd, _ := m.todoList.Update(msg)
|
|
inProgCmd, _ := m.inProgList.Update(msg)
|
|
blockedCmd, _ := m.blockedList.Update(msg)
|
|
doneCmd, _ := m.doneList.Update(msg)
|
|
return m, tea.Sequence(cmd, todoCmd, inProgCmd, blockedCmd, doneCmd)
|
|
}
|
|
|
|
return m, cmd
|
|
}
|