Files
LazyPM/pkg/tui/views/dashboard/operations.go
Moira Daniella A Sebastian 63631fc42f Implemented add comments for tui
2026-03-02 20:17:08 +01:00

517 lines
14 KiB
Go

package dashboard
import (
"context"
"os"
"os/user"
"github.com/LazyBachelor/LazyPM/internal/app"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
)
func defaultCommentAuthor() string {
if u, err := user.Current(); err == nil && u.Username != "" {
return u.Username
}
if s := os.Getenv("USER"); s != "" {
return s
}
if s := os.Getenv("USERNAME"); s != "" {
return s
}
return "user"
}
type issueTitleUpdatedMsg struct {
IssueID string
Err error
}
type issueDescriptionUpdatedMsg struct {
IssueID string
Err error
}
type issueStatusUpdatedMsg struct {
IssueID string
Err error
}
type issuePriorityUpdatedMsg struct {
IssueID string
Err error
}
type issueTypeUpdatedMsg struct {
IssueID string
Err error
}
type selectIssueMsg struct {
IssueID string
}
type issueCreatedMsg struct {
Issue *models.Issue
Err error
}
type issueDeletedMsg struct {
IssueID string
Err error
PreviousIndex int
}
type issueCommentAddedMsg struct {
IssueID string
Err error
}
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}
}
}
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 issueTitleUpdatedMsg{IssueID: issueID, Err: err}
}
}
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 issueDescriptionUpdatedMsg{IssueID: issueID, Err: err}
}
}
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 issueStatusUpdatedMsg{IssueID: issueID, Err: err}
}
}
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 issuePriorityUpdatedMsg{IssueID: issueID, Err: err}
}
}
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 issueTypeUpdatedMsg{IssueID: issueID, Err: err}
}
}
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 issueCreatedMsg{Issue: issue, Err: err}
}
}
func deleteIssueCmd(app *app.App, issueID string, currentIndex int) tea.Cmd {
return func() tea.Msg {
err := app.Issues.DeleteIssue(context.Background(), issueID)
return issueDeletedMsg{IssueID: issueID, Err: err, PreviousIndex: currentIndex}
}
}
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.
*/
issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
if err != nil {
return nil
}
setItemsCmd := m.issueList.SetIssues(OpenAndInProgressOnly(issues))
closedSetCmd := m.closedIssueList.SetIssues(ClosedOnly(issues))
for _, issue := range issues {
if issue.ID == issueID {
m.setDetailIssueWithComments(*issue)
break
}
}
return tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: issueID} })
}
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case issueTitleUpdatedMsg:
m.editingTitle = false
m.editingIssueID = ""
m.titleInput.Blur()
if msg.Err != nil {
return m, nil
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issueDescriptionUpdatedMsg:
m.editingDescription = false
m.editingDescIssueID = ""
m.descriptionInput.Blur()
if msg.Err != nil {
return m, nil
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issueStatusUpdatedMsg:
m.choosingStatus = false
m.statusIssueID = ""
if msg.Err != nil {
return m, nil
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issuePriorityUpdatedMsg:
m.choosingPriority = false
m.priorityIssueID = ""
if msg.Err != nil {
return m, nil
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issueTypeUpdatedMsg:
m.choosingType = false
m.typeIssueID = ""
if msg.Err != nil {
return m, nil
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case selectIssueMsg:
m.issueList.SelectIssueID(msg.IssueID)
m.closedIssueList.SelectIssueID(msg.IssueID)
return m, nil
case issueCreatedMsg:
m.creatingIssue = false
m.createTitleInput.Blur()
m.createTitleInput.Reset()
if msg.Err != nil || msg.Issue == nil {
return m, nil
}
issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
if err != nil {
return m, nil
}
setItemsCmd := m.issueList.SetIssues(OpenAndInProgressOnly(issues))
closedSetCmd := m.closedIssueList.SetIssues(ClosedOnly(issues))
// 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 issues {
// 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.setDetailIssueWithComments(*selectedIssue)
return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: selectedIssue.ID} })
case issueCommentAddedMsg:
m.addingComment = false
m.commentIssueID = ""
m.commentInput.Blur()
m.commentInput.Reset()
if msg.Err != nil {
return m, nil
}
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
case issueDeletedMsg:
m.confirmingDelete = false
m.deleteConfirmID = ""
if msg.Err != nil {
return m, nil
}
issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
if err != nil {
return m, nil
}
openIssues := OpenAndInProgressOnly(issues)
closedIssues := ClosedOnly(issues)
setItemsCmd := m.issueList.SetIssues(openIssues)
closedSetCmd := m.closedIssueList.SetIssues(closedIssues)
// If there are no issues at all, clear the detail view and return.
if len(openIssues) == 0 && len(closedIssues) == 0 {
m.setDetailIssueWithComments(models.Issue{})
return m, tea.Sequence(setItemsCmd, closedSetCmd)
}
// Determine which list to use for the next selection.
var targetIssues []*models.Issue
if m.focusedWindow == 0 {
targetIssues = openIssues
if len(targetIssues) == 0 && len(closedIssues) > 0 {
// The open list became empty; fall back to closed issues.
targetIssues = closedIssues
m.focusedWindow = 1
}
} else {
targetIssues = closedIssues
if len(targetIssues) == 0 && len(openIssues) > 0 {
// The closed list became empty; fall back to open/in-progress issues.
targetIssues = openIssues
m.focusedWindow = 0
}
}
// Safety: if targetIssues is still empty here, just clear detail and return.
if len(targetIssues) == 0 {
m.setDetailIssueWithComments(models.Issue{})
return m, tea.Sequence(setItemsCmd, closedSetCmd)
}
newIndex := msg.PreviousIndex
if newIndex >= len(targetIssues) {
newIndex = len(targetIssues) - 1
}
selectedIssue := targetIssues[newIndex]
m.setDetailIssueWithComments(*selectedIssue)
return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg {
return 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, 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, updateIssueStatusCmd(m.app, issueID, string(models.StatusOpen))
case "i":
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress))
case "c":
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusClosed))
case "esc":
m.choosingStatus = false
m.statusIssueID = ""
return m, nil
}
}
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, 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, updateIssueTypeCmd(m.app, issueID, models.TypeBug)
case "f":
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, updateIssueTypeCmd(m.app, issueID, models.TypeFeature)
case "t":
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, updateIssueTypeCmd(m.app, issueID, models.TypeTask)
case "e":
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, updateIssueTypeCmd(m.app, issueID, models.TypeEpic)
case "c":
issueID := m.typeIssueID
m.choosingType = false
m.typeIssueID = ""
return m, 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, 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.editingTitle {
if msg.String() == "enter" {
newTitle := m.titleInput.Value()
if newTitle != "" {
return m, 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.addingComment {
if msg.String() == "ctrl+s" || msg.String() == "enter" {
text := m.commentInput.Value()
if text != "" {
issueID := m.commentIssueID
m.addingComment = false
m.commentIssueID = ""
m.commentInput.Blur()
m.commentInput.Reset()
return m, addIssueCommentCmd(m.app, issueID, defaultCommentAuthor(), text)
}
}
if msg.String() == "esc" {
m.addingComment = false
m.commentIssueID = ""
m.commentInput.Blur()
m.commentInput.Reset()
return m, nil
}
var cmd tea.Cmd
m.commentInput, cmd = m.commentInput.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, 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 ValidationFeedbackMsg:
m.currentFeedback = msg.Feedback
if msg.Feedback.Success {
m.showComplete = true
return m, tea.Quit
}
return m, m.listenForValidation()
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
}
if m.focusedWindow == 0 {
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
}