Merge branch 'main' into Metrics-and-Logs
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/LazyBachelor/LazyPM/internal/utils/check"
|
||||
)
|
||||
|
||||
@@ -12,10 +13,10 @@ const description = `You are tasked with creating a new issue in the project man
|
||||
This task will test your ability to use the issue creation workflow effectively.
|
||||
|
||||
Your task:
|
||||
1. Create a new issue with a clear title
|
||||
2. Add a detailed description explaining what needs to be done
|
||||
3. Assign the issue to yourself
|
||||
4. Mark the issue as in-progress when you start working on it
|
||||
1. Create a new issue with the title "My first Issue"
|
||||
2. Add this detailed description "I need to do some coding"
|
||||
3. Assign the issue to yourself as "Me"
|
||||
4. Mark the issue as In Progress when you start working on it
|
||||
5. Close the issue once you've completed the work
|
||||
|
||||
Make sure to fill out all the necessary details to help others understand the work item.`
|
||||
@@ -60,6 +61,8 @@ func (t *CreateIssueTask) Setup(ctx context.Context) error {
|
||||
return t.app.Issues.CreateIssue(ctx, t.setupIssue, "")
|
||||
}
|
||||
|
||||
var isInProgress = false
|
||||
|
||||
func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback {
|
||||
expect := check.NewExpector()
|
||||
|
||||
@@ -68,9 +71,6 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback {
|
||||
return expect.ValidationFeedback
|
||||
}
|
||||
|
||||
expect.NotEmptyString(t.setupIssue.Assignee,
|
||||
fmt.Sprintf("%s is not assigned to anyone", t.setupIssue.ID))
|
||||
|
||||
if len(issues) == 0 {
|
||||
expect.Fail("No new issues created")
|
||||
return expect.ValidationFeedback
|
||||
@@ -78,8 +78,30 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback {
|
||||
|
||||
issue := issues[0]
|
||||
|
||||
expect.Assert(len(issues) < 2, "Multiple issues were created instead of one")
|
||||
expect.Assert(len(issues) < 2, "Multiple issues were created instead of one. Delete the extra issues and try again.")
|
||||
|
||||
expect.NotEmptyString(issue.Title, "Issue title should not be empty")
|
||||
expect.Assert(issue.Title == "My first Issue",
|
||||
fmt.Sprintf("Issue title does not match the expected value 'My first Issue', but was '%s'", issue.Title))
|
||||
|
||||
expect.NotEmptyString(issue.Description, "Issue description should not be empty")
|
||||
expect.Assert(issue.Description == "I need to do some coding",
|
||||
fmt.Sprintf("Issue description does not match the expected value 'I need to do some coding', but was '%s'", issue.Description))
|
||||
|
||||
expect.Assert(issue.Assignee == "Me",
|
||||
fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee))
|
||||
|
||||
if issue.Status == models.StatusInProgress || isInProgress {
|
||||
isInProgress = true
|
||||
} else {
|
||||
expect.Fail("Issue should be marked as in-progress when work starts")
|
||||
}
|
||||
|
||||
if !isInProgress {
|
||||
return expect.ValidationFeedback
|
||||
} else if issue.Status != models.StatusClosed {
|
||||
expect.Fail("Issue should be set to Closed once the work is completed")
|
||||
}
|
||||
|
||||
return expect.Complete()
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ func (e *Expector) Complete() ValidationFeedback {
|
||||
return e.ValidationFeedback
|
||||
}
|
||||
|
||||
func (e *Expector) CompleteWithMessage(message string) ValidationFeedback {
|
||||
e.Success = len(e.Errors()) == 0
|
||||
if !e.Success {
|
||||
e.Message = message
|
||||
}
|
||||
return e.ValidationFeedback
|
||||
}
|
||||
|
||||
func (e *Expector) Fail(message string) ValidationFeedback {
|
||||
e.Checks = append(e.Checks, NewCheck(message, false))
|
||||
return e.ValidationFeedback
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -194,3 +195,30 @@ func runQuestionnaire(t Tasker, iType InterfaceType, collector *taskRunCollector
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool) {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
feedback := t.Validate(ctx)
|
||||
if feedback.Success {
|
||||
if feedback.Message == "" {
|
||||
feedback.Message = "Task completed successfully! Going back to the survey menu..."
|
||||
}
|
||||
feedbackChan <- feedback
|
||||
time.Sleep(4 * time.Second)
|
||||
doneChan <- true
|
||||
return
|
||||
}
|
||||
feedback.Message = "Task not completed!"
|
||||
feedbackChan <- feedback
|
||||
case <-quitChan:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (h HelpBar) shortHelp() string {
|
||||
styles.HighlightKey("↓/j") + " down",
|
||||
styles.HighlightKey("pgup/pgdn") + " page",
|
||||
styles.HighlightKey("a") + " add",
|
||||
styles.HighlightKey("e/d/s/p/t") + " edit",
|
||||
styles.HighlightKey("e/d/s/p/t/c") + " edit",
|
||||
styles.HighlightKey("x") + " delete",
|
||||
styles.HighlightKey("q") + " quit",
|
||||
styles.HighlightKey("?") + " help",
|
||||
@@ -75,6 +75,7 @@ func (h HelpBar) fullHelp() string {
|
||||
renderRow("pgup", "page up", "pgdn", "page down"),
|
||||
renderRow("b", "back to list", "a", "add issue"),
|
||||
renderRow("e", "edit title", "d", "edit desc"),
|
||||
renderRow("c", "add comment", "", ""),
|
||||
renderRow("s", "change status", "p", "change priority"),
|
||||
renderRow("t", "change type", "x", "delete issue"),
|
||||
renderRow("?", "help", "q", "quit"),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
type IssueDetail struct {
|
||||
viewport viewport.Model
|
||||
issue models.Issue
|
||||
comments []*models.Comment
|
||||
focused bool
|
||||
}
|
||||
|
||||
@@ -25,6 +28,12 @@ func (i *IssueDetail) SetIssue(issue models.Issue) {
|
||||
i.refreshContent()
|
||||
}
|
||||
|
||||
// SetComments updates the list of comments displayed for the current issue.
|
||||
func (i *IssueDetail) SetComments(comments []*models.Comment) {
|
||||
i.comments = comments
|
||||
i.refreshContent()
|
||||
}
|
||||
|
||||
func (i *IssueDetail) SetSize(width, height int) {
|
||||
i.viewport.Height = height
|
||||
i.viewport.Width = width
|
||||
@@ -60,19 +69,34 @@ func (i *IssueDetail) refreshContent() {
|
||||
descLabel := styles.LabelStyle.Render("Description:")
|
||||
descContent := styles.ValueStyle.Render(i.issue.Description)
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left,
|
||||
titleRow,
|
||||
idRow,
|
||||
typeRow,
|
||||
statusRow,
|
||||
priorityRow,
|
||||
descLabel,
|
||||
descContent,
|
||||
)
|
||||
var parts []string
|
||||
parts = append(parts, titleRow, idRow, typeRow, statusRow, priorityRow, descLabel, descContent)
|
||||
|
||||
// Comments section
|
||||
commentsLabel := styles.LabelStyle.Render("Comments:")
|
||||
parts = append(parts, commentsLabel)
|
||||
if len(i.comments) == 0 {
|
||||
parts = append(parts, lipgloss.NewStyle().Foreground(styles.FaintText).Render(" No comments yet."))
|
||||
} else {
|
||||
for _, c := range i.comments {
|
||||
authorDate := lipgloss.NewStyle().Foreground(styles.Primary).Render(c.Author) + " " +
|
||||
lipgloss.NewStyle().Foreground(styles.FaintText).Render(formatCommentTime(c.CreatedAt))
|
||||
commentRow := lipgloss.JoinVertical(lipgloss.Left,
|
||||
authorDate,
|
||||
styles.ValueStyle.Render(c.Text),
|
||||
)
|
||||
parts = append(parts, commentRow)
|
||||
}
|
||||
}
|
||||
|
||||
content := lipgloss.JoinVertical(lipgloss.Left, parts...)
|
||||
i.viewport.SetContent(content)
|
||||
}
|
||||
|
||||
func formatCommentTime(t time.Time) string {
|
||||
return t.Format("Jan 2, 15:04")
|
||||
}
|
||||
|
||||
func (i IssueDetail) View() string {
|
||||
content := i.viewport.View()
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ type DashboardKeyMap struct {
|
||||
ChangeStatus key.Binding
|
||||
ChangePriority key.Binding
|
||||
ChangeType key.Binding
|
||||
AddComment key.Binding
|
||||
AddIssue key.Binding
|
||||
DeleteIssue key.Binding
|
||||
}
|
||||
@@ -71,6 +72,10 @@ var defaultDashboardKeyMap = DashboardKeyMap{
|
||||
key.WithKeys("t"),
|
||||
key.WithHelp("t", "change type"),
|
||||
),
|
||||
AddComment: key.NewBinding(
|
||||
key.WithKeys("c"),
|
||||
key.WithHelp("c", "add comment"),
|
||||
),
|
||||
AddIssue: key.NewBinding(
|
||||
key.WithKeys("a"),
|
||||
key.WithHelp("a", "add issue"),
|
||||
@@ -106,38 +111,43 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
|
||||
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown):
|
||||
d.issueDetail.ScrollDown(1)
|
||||
d.logAction("tui scrolled issue detail down")
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditTitle):
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.EditTitle):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startEditTitle(selected)
|
||||
cmd = d.titleInput.Focus()
|
||||
d.logAction("tui started editing issue title")
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditDescription):
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.EditDescription):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startEditDescription(selected)
|
||||
cmd = d.descriptionInput.Focus()
|
||||
d.logAction("tui started editing issue description")
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeStatus):
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangeStatus):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startChooseStatus(selected)
|
||||
d.logAction("tui opened status picker")
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangePriority):
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangePriority):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startChoosePriority(selected)
|
||||
d.logAction("tui opened priority picker")
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeType):
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangeType):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startChooseType(selected)
|
||||
d.logAction("tui opened type picker")
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.AddIssue):
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.AddComment):
|
||||
if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" {
|
||||
d.startAddComment(selected)
|
||||
cmd = d.commentInput.Focus()
|
||||
}
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.AddIssue):
|
||||
d.startCreateIssue()
|
||||
cmd = d.createTitleInput.Focus()
|
||||
d.logAction("tui started creating issue")
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.DeleteIssue):
|
||||
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.DeleteIssue):
|
||||
fl := d.FocusedIssueList()
|
||||
if selected := fl.SelectedItem(); selected.ID != "" {
|
||||
d.startConfirmDelete(selected.ID, fl.Index())
|
||||
|
||||
@@ -47,6 +47,9 @@ type Model struct {
|
||||
priorityIssueID string
|
||||
choosingType bool // true while choosing a type
|
||||
typeIssueID string
|
||||
addingComment bool // true while adding a comment
|
||||
commentInput textarea.Model
|
||||
commentIssueID string
|
||||
feedbackChan chan models.ValidationFeedback
|
||||
quitChan chan bool
|
||||
currentFeedback models.ValidationFeedback
|
||||
@@ -89,15 +92,39 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
|
||||
descTa.SetHeight(8)
|
||||
m.descriptionInput = descTa
|
||||
|
||||
commentTa := textarea.New()
|
||||
commentTa.Placeholder = "Write your comment..."
|
||||
commentTa.SetWidth(56)
|
||||
commentTa.SetHeight(6)
|
||||
m.commentInput = commentTa
|
||||
|
||||
if selected := m.issueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
m.setDetailIssueWithComments(selected.Issue)
|
||||
} else if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
m.setDetailIssueWithComments(selected.Issue)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// setDetailIssueWithComments sets the issue in the detail pane and loads its comments.
|
||||
func (m *Model) setDetailIssueWithComments(issue models.Issue) {
|
||||
m.issueDetail.SetIssue(issue)
|
||||
if issue.ID == "" {
|
||||
m.issueDetail.SetComments(nil)
|
||||
return
|
||||
}
|
||||
comments, _ := m.app.Issues.GetIssueComments(context.Background(), issue.ID)
|
||||
m.issueDetail.SetComments(comments)
|
||||
}
|
||||
|
||||
func (m *Model) startAddComment(selected ListIssue) {
|
||||
m.addingComment = true
|
||||
m.commentIssueID = selected.ID
|
||||
m.commentInput.SetValue("")
|
||||
m.commentInput.Reset()
|
||||
}
|
||||
|
||||
func (m *Model) logAction(action string) {
|
||||
if m.app != nil {
|
||||
m.app.LogAction(models.EncodeActionEvent(models.ActionEvent{
|
||||
@@ -212,11 +239,11 @@ func (m *Model) ToggleFocusedWindow() {
|
||||
m.focusedWindow = 1 - m.focusedWindow
|
||||
if m.focusedWindow == 0 {
|
||||
if selected := m.issueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
m.setDetailIssueWithComments(selected.Issue)
|
||||
}
|
||||
} else {
|
||||
if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
m.setDetailIssueWithComments(selected.Issue)
|
||||
}
|
||||
}
|
||||
m.issueDetail.SetFocused(m.IsFocusedOnDetail())
|
||||
|
||||
@@ -2,6 +2,8 @@ package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"github.com/LazyBachelor/LazyPM/internal/app"
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
@@ -9,6 +11,19 @@ import (
|
||||
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
|
||||
@@ -49,6 +64,18 @@ type issueDeletedMsg struct {
|
||||
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}
|
||||
@@ -121,7 +148,7 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
|
||||
closedSetCmd := m.closedIssueList.SetIssues(ClosedOnly(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.ID == issueID {
|
||||
m.issueDetail.SetIssue(*issue)
|
||||
m.setDetailIssueWithComments(*issue)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -214,9 +241,18 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
m.issueDetail.SetIssue(*selectedIssue)
|
||||
m.setDetailIssueWithComments(*selectedIssue)
|
||||
m.logAction("tui created issue")
|
||||
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 = ""
|
||||
@@ -235,7 +271,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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.issueDetail.SetIssue(models.Issue{})
|
||||
m.setDetailIssueWithComments(models.Issue{})
|
||||
return m, tea.Sequence(setItemsCmd, closedSetCmd)
|
||||
}
|
||||
|
||||
@@ -259,7 +295,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// Safety: if targetIssues is still empty here, just clear detail and return.
|
||||
if len(targetIssues) == 0 {
|
||||
m.issueDetail.SetIssue(models.Issue{})
|
||||
m.setDetailIssueWithComments(models.Issue{})
|
||||
return m, tea.Sequence(setItemsCmd, closedSetCmd)
|
||||
}
|
||||
|
||||
@@ -268,7 +304,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
newIndex = len(targetIssues) - 1
|
||||
}
|
||||
selectedIssue := targetIssues[newIndex]
|
||||
m.issueDetail.SetIssue(*selectedIssue)
|
||||
m.setDetailIssueWithComments(*selectedIssue)
|
||||
return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg {
|
||||
return selectIssueMsg{IssueID: selectedIssue.ID}
|
||||
})
|
||||
@@ -420,6 +456,30 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
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" {
|
||||
m.logAction("tui submitted issue description edit")
|
||||
@@ -475,7 +535,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmd, changed := m.issueList.Update(msg)
|
||||
if changed {
|
||||
if selected := m.issueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
m.setDetailIssueWithComments(selected.Issue)
|
||||
}
|
||||
}
|
||||
return m, cmd
|
||||
@@ -483,7 +543,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmd, changed := m.closedIssueList.Update(msg)
|
||||
if changed {
|
||||
if selected := m.closedIssueList.SelectedItem(); selected.ID != "" {
|
||||
m.issueDetail.SetIssue(selected.Issue)
|
||||
m.setDetailIssueWithComments(selected.Issue)
|
||||
}
|
||||
}
|
||||
return m, cmd
|
||||
|
||||
@@ -70,6 +70,21 @@ func (m *Model) View() string {
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)
|
||||
}
|
||||
|
||||
if m.addingComment {
|
||||
editBoxWidth := min(60, m.width-4)
|
||||
m.commentInput.SetWidth(editBoxWidth - 2)
|
||||
m.commentInput.SetHeight(8)
|
||||
editContent := lipgloss.JoinVertical(lipgloss.Left,
|
||||
styles.LabelStyle.Render("Add comment for "+m.commentIssueID+" (Ctrl+S or Enter to save, Esc to cancel):"),
|
||||
m.commentInput.View(),
|
||||
)
|
||||
editBox := styles.ContainerStyle.
|
||||
Width(editBoxWidth).
|
||||
BorderForeground(styles.PrimaryBorder).
|
||||
Render(editContent)
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)
|
||||
}
|
||||
|
||||
if m.editingDescription {
|
||||
editBoxWidth := min(60, m.width-4)
|
||||
m.descriptionInput.SetWidth(editBoxWidth - 2)
|
||||
|
||||
Reference in New Issue
Block a user