move operations to msgs

This commit is contained in:
Robin Olsen
2026-03-20 12:21:53 +01:00
parent aa9de18c99
commit d4bd9877d0
3 changed files with 177 additions and 167 deletions

View File

@@ -1,130 +0,0 @@
package issues
import (
"context"
"charm.land/bubbletea/v2"
"github.com/LazyBachelor/LazyPM/internal/app"
"github.com/LazyBachelor/LazyPM/internal/models"
)
// Msg types used by both dashboard and kanban TUI views.
type (
TitleUpdatedMsg struct {
IssueID string
Err error
}
DescriptionUpdatedMsg struct {
IssueID string
Err error
}
StatusUpdatedMsg struct {
IssueID string
Err error
}
PriorityUpdatedMsg struct {
IssueID string
Err error
}
TypeUpdatedMsg struct {
IssueID string
Err error
}
AssigneeUpdatedMsg struct {
IssueID string
Err error
}
SelectIssueMsg struct{ IssueID string }
CreatedMsg struct {
Issue *models.Issue
Err error
}
DeletedMsg struct {
IssueID string
Err error
PreviousIndex int
}
)
// UpdateIssueTitleCmd returns a command that updates an issue's title.
func UpdateIssueTitleCmd(app *app.App, issueID, newTitle string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"title": newTitle}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return TitleUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueDescriptionCmd returns a command that updates an issue's description.
func UpdateIssueDescriptionCmd(app *app.App, issueID, newDescription string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"description": newDescription}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return DescriptionUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueStatusCmd returns a command that updates an issue's status.
func UpdateIssueStatusCmd(app *app.App, issueID, status string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"status": status}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return StatusUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssuePriorityCmd returns a command that updates an issue's priority.
func UpdateIssuePriorityCmd(app *app.App, issueID string, priority int) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"priority": priority}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return PriorityUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueTypeCmd returns a command that updates an issue's type.
func UpdateIssueTypeCmd(app *app.App, issueID string, issueType models.IssueType) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"issue_type": string(issueType)}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return TypeUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueAssigneeCmd returns a command that updates an issue's assignee.
func UpdateIssueAssigneeCmd(app *app.App, issueID, assignee string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"assignee": assignee}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return AssigneeUpdatedMsg{IssueID: issueID, Err: err}
}
}
// CreateIssueCmd returns a command that creates a new issue.
func CreateIssueCmd(app *app.App, title string) tea.Cmd {
return func() tea.Msg {
issue := &models.Issue{
Title: title,
Status: models.StatusOpen,
IssueType: models.TypeTask,
Priority: 2,
}
err := app.Issues.CreateIssue(context.Background(), issue, "tui")
return CreatedMsg{Issue: issue, Err: err}
}
}
// DeleteIssueCmd returns a command that deletes an issue.
func DeleteIssueCmd(app *app.App, issueID string, currentIndex int) tea.Cmd {
return func() tea.Msg {
err := app.Issues.DeleteIssue(context.Background(), issueID)
return DeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex}
}
}
func CloseIssueCmd(app *app.App, issueID, reason string) tea.Cmd {
return func() tea.Msg {
err := app.Issues.CloseIssue(context.Background(), issueID, reason, "tui", "")
return StatusUpdatedMsg{IssueID: issueID, Err: err}
}
}

View File

@@ -1,7 +1,148 @@
package msgs
// SwitchToDashboardMsg signals to switch to the main dashboard view.
type SwitchToDashboardMsg struct{}
import (
"context"
// SwitchToKanbanBoardMsg signals to switch to the kanban board view.
type SwitchToKanbanBoardMsg struct{}
tea "charm.land/bubbletea/v2"
"github.com/LazyBachelor/LazyPM/internal/app"
"github.com/LazyBachelor/LazyPM/internal/models"
)
// Msg types used by both dashboard and kanban TUI views.
type (
TitleUpdatedMsg struct {
IssueID string
Err error
}
DescriptionUpdatedMsg struct {
IssueID string
Err error
}
StatusUpdatedMsg struct {
IssueID string
Err error
}
PriorityUpdatedMsg struct {
IssueID string
Err error
}
TypeUpdatedMsg struct {
IssueID string
Err error
}
AssigneeUpdatedMsg struct {
IssueID string
Err error
}
SelectIssueMsg struct{ IssueID string }
CreatedMsg struct {
Issue *models.Issue
Err error
}
DeletedMsg struct {
IssueID string
Err error
PreviousIndex int
}
IssueCommentAddedMsg struct {
IssueID string
Err error
}
// SwitchToDashboardMsg signals to switch to the main dashboard view.
SwitchToDashboardMsg struct{}
// SwitchToKanbanBoardMsg signals to switch to the kanban board view.
SwitchToKanbanBoardMsg struct{}
)
// UpdateIssueTitleCmd returns a command that updates an issue's title.
func UpdateIssueTitleCmd(app *app.App, issueID, newTitle string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"title": newTitle}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return TitleUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueDescriptionCmd returns a command that updates an issue's description.
func UpdateIssueDescriptionCmd(app *app.App, issueID, newDescription string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"description": newDescription}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return DescriptionUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueStatusCmd returns a command that updates an issue's status.
func UpdateIssueStatusCmd(app *app.App, issueID, status string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"status": status}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return StatusUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssuePriorityCmd returns a command that updates an issue's priority.
func UpdateIssuePriorityCmd(app *app.App, issueID string, priority int) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"priority": priority}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return PriorityUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueTypeCmd returns a command that updates an issue's type.
func UpdateIssueTypeCmd(app *app.App, issueID string, issueType models.IssueType) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"issue_type": string(issueType)}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return TypeUpdatedMsg{IssueID: issueID, Err: err}
}
}
// UpdateIssueAssigneeCmd returns a command that updates an issue's assignee.
func UpdateIssueAssigneeCmd(app *app.App, issueID, assignee string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"assignee": assignee}
err := app.Issues.UpdateIssue(context.Background(), issueID, updates, "tui")
return AssigneeUpdatedMsg{IssueID: issueID, Err: err}
}
}
// CreateIssueCmd returns a command that creates a new issue.
func CreateIssueCmd(app *app.App, title string) tea.Cmd {
return func() tea.Msg {
issue := &models.Issue{
Title: title,
Status: models.StatusOpen,
IssueType: models.TypeTask,
Priority: 2,
}
err := app.Issues.CreateIssue(context.Background(), issue, "tui")
return CreatedMsg{Issue: issue, Err: err}
}
}
// DeleteIssueCmd returns a command that deletes an issue.
func DeleteIssueCmd(app *app.App, issueID string, currentIndex int) tea.Cmd {
return func() tea.Msg {
err := app.Issues.DeleteIssue(context.Background(), issueID)
return DeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex}
}
}
func CloseIssueCmd(app *app.App, issueID, reason string) tea.Cmd {
return func() tea.Msg {
err := app.Issues.CloseIssue(context.Background(), issueID, reason, "tui", "")
return StatusUpdatedMsg{IssueID: issueID, Err: err}
}
}
func AddIssueCommentCmd(app *app.App, issueID, author, text string) tea.Cmd {
return func() tea.Msg {
_, err := app.Issues.AddIssueComment(context.Background(), issueID, author, text)
return IssueCommentAddedMsg{IssueID: issueID, Err: err}
}
}

View File

@@ -7,13 +7,12 @@ import (
"charm.land/bubbletea/v2"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/pkg/tui/components"
"github.com/LazyBachelor/LazyPM/pkg/tui/issues"
"github.com/LazyBachelor/LazyPM/pkg/tui/msgs"
)
// 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 msgs, update both lists, set the detail view for the given issue, and return a command to select that issue. Returns nil if fetch fails.
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
@@ -68,7 +67,7 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case issues.TitleUpdatedMsg:
case msgs.TitleUpdatedMsg:
m.editingTitle = false
m.editingIssueID = ""
m.titleInput.Blur()
@@ -77,7 +76,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issues.DescriptionUpdatedMsg:
case msgs.DescriptionUpdatedMsg:
m.editingDescription = false
m.editingDescIssueID = ""
m.descriptionInput.Blur()
@@ -86,7 +85,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issues.StatusUpdatedMsg:
case msgs.StatusUpdatedMsg:
m.choosingStatus = false
m.statusIssueID = ""
if msg.Err != nil {
@@ -94,7 +93,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issues.PriorityUpdatedMsg:
case msgs.PriorityUpdatedMsg:
m.choosingPriority = false
m.priorityIssueID = ""
if msg.Err != nil {
@@ -102,7 +101,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issues.TypeUpdatedMsg:
case msgs.TypeUpdatedMsg:
m.choosingType = false
m.typeIssueID = ""
if msg.Err != nil {
@@ -110,7 +109,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issues.AssigneeUpdatedMsg:
case msgs.AssigneeUpdatedMsg:
m.editingAssignee = false
m.assigneeIssueID = ""
m.assigneeInput.Blur()
@@ -119,14 +118,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issues.SelectIssueMsg:
case msgs.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:
case msgs.CreatedMsg:
m.creatingIssue = false
m.createTitleInput.Blur()
m.createTitleInput.Reset()
@@ -161,8 +160,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.issueDetail.SetIssue(*selectedIssue)
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return issues.SelectIssueMsg{IssueID: selectedIssue.ID} })
case issues.DeletedMsg:
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} })
case msgs.DeletedMsg:
m.confirmingDelete = false
m.deleteConfirmID = ""
if msg.Err != nil {
@@ -183,7 +182,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
blockedCmd := m.blockedList.SetIssues(blockedIssues)
doneCmd := m.doneList.SetIssues(doneIssues)
// If there are no issues at all, clear the detail view and return.
// If there are no msgs 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)
@@ -263,7 +262,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, blockedCmd, doneCmd, func() tea.Msg {
return issues.SelectIssueMsg{IssueID: selectedIssue.ID}
return msgs.SelectIssueMsg{IssueID: selectedIssue.ID}
})
case tea.KeyPressMsg:
@@ -274,7 +273,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
idx := m.deleteConfirmIndex
m.confirmingDelete = false
m.deleteConfirmID = ""
return m, issues.DeleteIssueCmd(m.app, issueID, idx)
return m, msgs.DeleteIssueCmd(m.app, issueID, idx)
case "n", "N", "esc":
m.confirmingDelete = false
m.deleteConfirmID = ""
@@ -288,22 +287,22 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusOpen))
return m, msgs.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))
return m, msgs.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))
return m, msgs.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))
return m, msgs.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint))
case "c":
issueID := m.statusIssueID
m.choosingStatus = false
@@ -345,7 +344,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
issueID := m.closeReasonIssueID
m.choosingCloseReason = false
m.closeReasonIssueID = ""
return m, issues.CloseIssueCmd(m.app, issueID, reason)
return m, msgs.CloseIssueCmd(m.app, issueID, reason)
}
}
@@ -358,7 +357,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.closingOtherReason = false
m.closeReasonIssueID = ""
m.closeReasonInput.Blur()
return m, issues.CloseIssueCmd(m.app, issueID, reason)
return m, msgs.CloseIssueCmd(m.app, issueID, reason)
}
case "esc":
m.closingOtherReason = false
@@ -378,7 +377,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
priority := int(msg.String()[0] - '0')
m.choosingPriority = false
m.priorityIssueID = ""
return m, issues.UpdateIssuePriorityCmd(m.app, issueID, priority)
return m, msgs.UpdateIssuePriorityCmd(m.app, issueID, priority)
case "esc":
m.choosingPriority = false
m.priorityIssueID = ""
@@ -394,27 +393,27 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, issues.UpdateIssueTypeCmd(m.app, issueID, models.TypeBug)
return m, msgs.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)
return m, msgs.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)
return m, msgs.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)
return m, msgs.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)
return m, msgs.UpdateIssueTypeCmd(m.app, issueID, models.TypeChore)
case "esc":
m.choosingType = false
m.typeIssueID = ""
@@ -428,7 +427,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.String() == "enter" {
title := m.createTitleInput.Value()
if title != "" {
return m, issues.CreateIssueCmd(m.app, title)
return m, msgs.CreateIssueCmd(m.app, title)
}
}
if msg.String() == "esc" {
@@ -445,7 +444,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.editingAssignee {
if msg.String() == "enter" {
assignee := m.assigneeInput.Value()
return m, issues.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee)
return m, msgs.UpdateIssueAssigneeCmd(m.app, m.assigneeIssueID, assignee)
}
if msg.String() == "esc" {
m.editingAssignee = false
@@ -462,7 +461,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.String() == "enter" {
newTitle := m.titleInput.Value()
if newTitle != "" {
return m, issues.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle)
return m, msgs.UpdateIssueTitleCmd(m.app, m.editingIssueID, newTitle)
}
}
if msg.String() == "esc" {
@@ -483,7 +482,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.editingDescription = false
m.editingDescIssueID = ""
m.descriptionInput.Blur()
return m, issues.UpdateIssueDescriptionCmd(m.app, issueID, newDesc)
return m, msgs.UpdateIssueDescriptionCmd(m.app, issueID, newDesc)
}
if msg.String() == "esc" {
m.editingDescription = false