Implemented add comments for tui
This commit is contained in:
@@ -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"),
|
||||
@@ -99,32 +104,37 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
|
||||
d.issueDetail.ScrollUp(1)
|
||||
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown):
|
||||
d.issueDetail.ScrollDown(1)
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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()
|
||||
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) startEditTitle(selected ListIssue) {
|
||||
m.editingTitle = true
|
||||
m.editingIssueID = selected.ID
|
||||
@@ -203,11 +230,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
|
||||
}
|
||||
}
|
||||
@@ -203,8 +230,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
m.issueDetail.SetIssue(*selectedIssue)
|
||||
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 = ""
|
||||
@@ -221,7 +257,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)
|
||||
}
|
||||
|
||||
@@ -245,7 +281,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)
|
||||
}
|
||||
|
||||
@@ -254,7 +290,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}
|
||||
})
|
||||
@@ -388,6 +424,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" {
|
||||
issueID := m.editingDescIssueID
|
||||
@@ -441,7 +501,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
|
||||
@@ -449,7 +509,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