Files
LazyPM/pkg/tui/views/dashboard/operations.go
2026-03-24 19:57:45 +01:00

492 lines
15 KiB
Go

package dashboard
import (
"context"
"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 {
allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
if err != nil {
return nil
}
setItemsCmd := m.issueList.SetIssues(components.SortedIssues(allIssues))
for _, issue := range allIssues {
if issue.ID == issueID {
m.setDetailIssueWithComments(*issue)
break
}
}
return tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: issueID} })
}
func (m *Model) refreshAndSubmit(issueID string) tea.Cmd {
refreshCmd := m.refreshIssueListsAndSelectIssue(issueID)
m.submitValidation()
return refreshCmd
}
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) 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
}
func (m *Model) startManageDependencies(selected ListIssue) tea.Cmd {
m.currentIssueID = selected.ID
depModal := modal.NewDependenciesModal(m.app, selected.ID)
m.dependenciesModal = depModal
return m.modalManager.PushModal(depModal)
}
// 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 {
m.logAction("tui submitted issue title edit")
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 != "" {
m.logAction("tui submitted new issue")
cmd := msgs.CreateIssueCmd(m.app, r.Value)
return func() tea.Msg { return cmd() }
}
case modal.ModalEditAssignee:
if r, ok := msg.Value.(modal.TextInputResult); ok {
m.logAction("tui submitted assignee edit")
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 {
m.logAction("tui submitted issue description edit")
cmd := msgs.UpdateIssueDescriptionCmd(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() }
}
case modal.ModalCloseReason:
if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" {
m.logAction("tui submitted custom close reason")
cmd := msgs.CloseIssueCmd(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 {
m.logAction("tui confirmed issue deletion")
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 {
m.logAction("tui selected issue status")
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 {
m.logAction("tui selected issue priority")
priority, _ := strconv.Atoi(r.SelectedValue)
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 {
m.logAction("tui selected issue type")
issueType := models.IssueType(r.SelectedValue)
cmd := msgs.UpdateIssueTypeCmd(m.app, m.currentIssueID, issueType)
return func() tea.Msg { return cmd() }
}
case modal.ModalSelectDependency:
if r, ok := msg.Value.(modal.SelectResult); ok && r.SelectedValue != "" {
m.logAction("tui added dependency")
m.modalManager.PopModal()
return msgs.AddDependencyCmd(m.app, m.currentIssueID, r.SelectedValue)
}
case modal.ModalSelectRemoveDependency:
if r, ok := msg.Value.(modal.SelectResult); ok && r.SelectedValue != "" {
m.logAction("tui removed dependency")
m.modalManager.PopModal()
return msgs.RemoveDependencyCmd(m.app, m.currentIssueID, r.SelectedValue)
}
}
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 = ""
m.logAction("tui canceled issue title edit")
case modal.ModalCreateIssue:
m.logAction("tui canceled issue creation")
case modal.ModalEditAssignee:
m.currentIssueID = ""
m.logAction("tui canceled assignee edit")
case modal.ModalEditDescription:
m.currentIssueID = ""
m.logAction("tui canceled issue description edit")
case modal.ModalAddComment:
m.currentIssueID = ""
case modal.ModalCloseReason:
m.currentIssueID = ""
m.logAction("tui canceled close reason")
case modal.ModalConfirmDelete:
m.deleteIndex = -1
m.currentIssueID = ""
m.logAction("tui canceled issue deletion")
case modal.ModalSelectStatus:
m.currentIssueID = ""
m.logAction("tui canceled status picker")
case modal.ModalSelectCloseReason:
m.currentIssueID = ""
m.logAction("tui canceled close reason")
case modal.ModalSelectPriority:
m.currentIssueID = ""
m.logAction("tui canceled priority picker")
case modal.ModalSelectType:
m.currentIssueID = ""
m.logAction("tui canceled type picker")
case modal.ModalManageDependencies:
m.dependenciesModal = nil
m.modalManager.PopModal()
if selected := m.issueList.SelectedItem(); selected.ID != "" {
m.setDetailIssueWithComments(selected.Issue)
}
m.logAction("tui closed dependencies management")
case modal.ModalSelectDependency:
m.modalManager.PopModal()
m.logAction("tui canceled add dependency")
case modal.ModalSelectRemoveDependency:
m.modalManager.PopModal()
m.logAction("tui canceled remove dependency")
}
}
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.DependencyAddRequestedMsg:
issues := modal.EligibleDependencyIssues(m.app, msg.IssueID)
if len(issues) == 0 {
m.logAction("tui no issues available to add as dependency")
return m, nil
}
listModal := modal.NewDependencyListModal(issues, m.width, m.height)
return m, m.modalManager.PushModal(listModal)
case msgs.DependencyRemoveRequestedMsg:
deps, _ := m.app.Issues.GetDependencies(context.Background(), msg.IssueID)
if len(deps) == 0 {
m.logAction("tui no dependencies to remove")
return m, nil
}
listModal := modal.NewDependencyRemoveListModal(deps, m.width, m.height)
return m, m.modalManager.PushModal(listModal)
case msgs.DependencyAddedMsg:
if m.dependenciesModal != nil {
m.dependenciesModal.RefreshDeps()
}
if msg.Err != nil {
m.logAction("tui failed to add dependency")
return m, nil
}
m.logAction("tui added dependency")
m.submitValidation()
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case msgs.DependencyRemovedMsg:
if m.dependenciesModal != nil {
m.dependenciesModal.RefreshDeps()
}
if msg.Err != nil {
m.logAction("tui failed to remove dependency")
return m, nil
}
m.logAction("tui removed dependency")
m.submitValidation()
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case msgs.TitleUpdatedMsg:
m.modalManager.GetTextInputModal(modal.ModalEditTitle).Reset()
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue title")
return m, nil
}
m.logAction("tui updated issue title")
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.DescriptionUpdatedMsg:
m.modalManager.GetTextAreaModal(modal.ModalEditDescription).Reset()
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue description")
return m, nil
}
m.logAction("tui updated issue description")
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.StatusUpdatedMsg:
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue status")
return m, nil
}
m.logAction("tui updated issue status")
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.PriorityUpdatedMsg:
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue priority")
return m, nil
}
m.logAction("tui updated issue priority")
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.TypeUpdatedMsg:
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue type")
return m, nil
}
m.logAction("tui updated issue type")
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.AssigneeUpdatedMsg:
m.modalManager.GetTextInputModal(modal.ModalEditAssignee).Reset()
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to update issue assignee")
return m, nil
}
m.logAction("tui updated issue assignee")
return m, m.refreshAndSubmit(msg.IssueID)
case msgs.SelectIssueMsg:
m.issueList.SelectIssueID(msg.IssueID)
m.closedIssueList.SelectIssueID(msg.IssueID)
return m, nil
case msgs.CreatedMsg:
m.modalManager.GetTextInputModal(modal.ModalCreateIssue).Reset()
if msg.Err != nil || msg.Issue == nil {
m.logAction("tui failed to create issue")
return m, nil
}
allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
if err != nil {
return m, nil
}
setItemsCmd := m.issueList.SetIssues(components.SortedIssues(allIssues))
selectedIssue := msg.Issue
if selectedIssue.ID == "" {
for _, issue := range allIssues {
if issue.Title == msg.Issue.Title {
selectedIssue = issue
break
}
}
}
m.setDetailIssueWithComments(*selectedIssue)
m.logAction("tui created issue")
m.submitValidation()
return m, tea.Sequence(setItemsCmd, func() tea.Msg { return msgs.SelectIssueMsg{IssueID: selectedIssue.ID} })
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.DeletedMsg:
m.deleteIndex = -1
m.currentIssueID = ""
if msg.Err != nil {
m.logAction("tui failed to delete issue")
return m, nil
}
m.logAction("tui deleted issue")
m.submitValidation()
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case tea.KeyPressMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
if m.issueList.FilterState() == list.Filtering {
cmd, _ := m.issueList.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
}
if m.focusManager.IsFocused(modal.FocusList) {
cmd, changed := m.issueList.Update(msg)
if changed {
if selected := m.issueList.SelectedItem(); selected.ID != "" {
m.setDetailIssueWithComments(selected.Issue)
}
}
return m, cmd
}
cmd, changed := m.closedIssueList.Update(msg)
if changed {
if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
m.setDetailIssueWithComments(selected.Issue)
}
}
return m, cmd
}