package kanban import ( "context" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/components" "github.com/LazyBachelor/LazyPM/pkg/tui/issues" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" ) func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { /* update handler for issueTitleUpdatedMsg, issueDescriptionUpdatedMsg, and issueStatusUpdatedMsg to avoid using nearly identical code for refreshing the issue lists and updating the detail view Fetch all issues, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails. */ allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return 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) var targetStatus models.Status for _, issue := range allIssues { if issue.ID == issueID { m.issueDetail.SetIssue(*issue) targetStatus = issue.Status break } } switch targetStatus { case models.StatusOpen: m.focusedColumn = 0 case models.StatusInProgress: m.focusedColumn = 1 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, blockedCmd, doneCmd) } func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case issues.TitleUpdatedMsg: m.editingTitle = false m.editingIssueID = "" m.titleInput.Blur() if msg.Err != nil { return m, nil } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issues.DescriptionUpdatedMsg: m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() if msg.Err != nil { return m, nil } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issues.StatusUpdatedMsg: m.choosingStatus = false m.statusIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issues.PriorityUpdatedMsg: m.choosingPriority = false m.priorityIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issues.TypeUpdatedMsg: m.choosingType = false m.typeIssueID = "" if msg.Err != nil { return m, nil } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issues.AssigneeUpdatedMsg: m.editingAssignee = false m.assigneeIssueID = "" m.assigneeInput.Blur() if msg.Err != nil { return m, nil } return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) 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 case issues.CreatedMsg: m.creatingIssue = false m.createTitleInput.Blur() m.createTitleInput.Reset() if msg.Err != nil || msg.Issue == 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) // Determine the created issue from the refreshed list to ensure all fields (like ID) are populated. selectedIssue := msg.Issue if selectedIssue.ID == "" { for _, issue := range allIssues { // Prefer an issue that matches the created issue's title when ID is not yet known. if issue.Title == msg.Issue.Title { selectedIssue = issue break } } } m.issueDetail.SetIssue(*selectedIssue) 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 = "" 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 there are no issues at all, clear the detail view and return. if len(todoIssues) == 0 && len(inProgIssues) == 0 && len(blockedIssues) == 0 && len(doneIssues) == 0 { m.issueDetail.SetIssue(models.Issue{}) return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd) } // Determine which column to use for the next selection based on the current focus. var targetIssues []*models.Issue switch m.focusedColumn { case 0: targetIssues = todoIssues if len(targetIssues) == 0 { 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 = 3 } } case 1: targetIssues = inProgIssues if len(targetIssues) == 0 { 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 = 3 } } case 2: targetIssues = blockedIssues if len(targetIssues) == 0 { if len(inProgIssues) > 0 { targetIssues = inProgIssues m.focusedColumn = 1 } 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 } } } // 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, blockedCmd, doneCmd) } newIndex := msg.PreviousIndex if newIndex >= len(targetIssues) { newIndex = len(targetIssues) - 1 } selectedIssue := targetIssues[newIndex] m.issueDetail.SetIssue(*selectedIssue) return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} }) case tea.KeyMsg: if m.confirmingDelete { switch msg.String() { case "y", "Y": issueID := m.deleteConfirmID idx := m.deleteConfirmIndex m.confirmingDelete = false m.deleteConfirmID = "" return m, issues.DeleteIssueCmd(m.app, issueID, idx) case "n", "N", "esc": m.confirmingDelete = false m.deleteConfirmID = "" return m, nil } } if m.choosingStatus { switch msg.String() { case "o": issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) case "i": issueID := m.statusIssueID 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 m.statusIssueID = "" return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) case "c": issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" m.choosingCloseReason = true m.closeReasonIssueID = issueID return m, nil case "esc": m.choosingStatus = false m.statusIssueID = "" return m, nil } } if m.choosingCloseReason { var reason string switch msg.String() { case "d": reason = "Done" case "u": reason = "Duplicate issue" case "w": reason = "Won't fix" case "o": reason = "Obsolete" case "h": m.choosingCloseReason = false m.closingOtherReason = true m.closeReasonInput.SetValue("") m.closeReasonInput.Focus() return m, nil case "esc": m.choosingCloseReason = false m.closeReasonIssueID = "" return m, nil } if reason != "" { issueID := m.closeReasonIssueID m.choosingCloseReason = false m.closeReasonIssueID = "" return m, issues.CloseIssueCmd(m.app, issueID, reason) } } if m.closingOtherReason { switch msg.String() { case "enter", "ctrl+s": reason := m.closeReasonInput.Value() if reason != "" { issueID := m.closeReasonIssueID m.closingOtherReason = false m.closeReasonIssueID = "" m.closeReasonInput.Blur() return m, issues.CloseIssueCmd(m.app, issueID, reason) } case "esc": m.closingOtherReason = false m.closeReasonIssueID = "" m.closeReasonInput.Blur() return m, nil } var cmd tea.Cmd m.closeReasonInput, cmd = m.closeReasonInput.Update(msg) return m, cmd } if m.choosingPriority { switch msg.String() { case "0", "1", "2", "3", "4": issueID := m.priorityIssueID priority := int(msg.String()[0] - '0') m.choosingPriority = false m.priorityIssueID = "" return m, issues.UpdateIssuePriorityCmd(m.app, issueID, priority) case "esc": m.choosingPriority = false m.priorityIssueID = "" return m, nil default: return m, nil } } if m.choosingType { switch msg.String() { case "b": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug) case "f": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeFeature) case "t": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeTask) case "e": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeEpic) case "c": issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore) case "esc": m.choosingType = false m.typeIssueID = "" return m, nil default: return m, nil } } if m.creatingIssue { if msg.String() == "enter" { title := m.createTitleInput.Value() if title != "" { return m, issues.CreateIssueCmd(m.app, title) } } if msg.String() == "esc" { m.creatingIssue = false m.createTitleInput.Blur() m.createTitleInput.Reset() return m, nil } var cmd tea.Cmd m.createTitleInput, cmd = m.createTitleInput.Update(msg) return m, cmd } if m.editingAssignee { if msg.String() == "enter" { assignee := m.assigneeInput.Value() return m, issues.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee) } if msg.String() == "esc" { m.editingAssignee = false m.assigneeIssueID = "" m.assigneeInput.Blur() return m, nil } var cmd tea.Cmd m.assigneeInput, cmd = m.assigneeInput.Update(msg) return m, cmd } if m.editingTitle { if msg.String() == "enter" { newTitle := m.titleInput.Value() if newTitle != "" { return m, issues.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle) } } if msg.String() == "esc" { m.editingTitle = false m.editingIssueID = "" m.titleInput.Blur() return m, nil } var cmd tea.Cmd m.titleInput, cmd = m.titleInput.Update(msg) return m, cmd } if m.editingDescription { if msg.String() == "ctrl+s" { issueID := m.editingDescIssueID newDesc := m.descriptionInput.Value() m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() return m, issues.UpdateIssueDescriptionCmd(m.app, issueID, newDesc) } if msg.String() == "esc" { m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() return m, nil } var cmd tea.Cmd m.descriptionInput, cmd = m.descriptionInput.Update(msg) return m, cmd } focusedList := m.FocusedIssueList() if focusedList.FilterState() == list.Filtering { cmd, _ := focusedList.Update(msg) return m, cmd } // On main dashboard, ESC does nothing; only q quits; like in lazybeads. if msg.String() == "esc" { return m, nil } cmd := m.handleKeyMsg(msg) if cmd != nil { 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 return m, nil } fl := m.FocusedIssueList() cmd, changed := fl.Update(msg) if changed { if selected := fl.SelectedItem(); selected.ID != "" { m.issueDetail.SetIssue(selected.Issue) } } return m, cmd }