update description and status, and replicating help message from lazybeads

This commit is contained in:
viljarb
2026-02-14 15:29:08 +01:00
parent 3ed3863947
commit 501c2a71e5
7 changed files with 197 additions and 17 deletions

View File

@@ -1 +1 @@
{"id":"LazyPM-LPM-32-dko","title":"231","status":"open","priority":2,"issue_type":"feature","owner":"viljarb@tutanota.com","created_at":"2026-02-13T12:35:13.465758325+01:00","created_by":"vb","updated_at":"2026-02-13T12:35:13.465758325+01:00"} {"id":"LazyPM-LPM-32-dko","title":"231","status":"open","priority":2,"issue_type":"chore","owner":"viljarb@tutanota.com","created_at":"2026-02-13T12:35:13.465758325+01:00","created_by":"vb","updated_at":"2026-02-14T11:40:22.72004373+01:00"}

View File

@@ -34,7 +34,7 @@ func (h HelpBar) shortHelp() string {
styles.HighlightKey("↑/k") + " up", styles.HighlightKey("↑/k") + " up",
styles.HighlightKey("↓/j") + " down", styles.HighlightKey("↓/j") + " down",
styles.HighlightKey("a") + " add", styles.HighlightKey("a") + " add",
styles.HighlightKey("e") + " edit", styles.HighlightKey("e/d/s") + " edit",
styles.HighlightKey("x") + " delete", styles.HighlightKey("x") + " delete",
styles.HighlightKey("q") + " quit", styles.HighlightKey("q") + " quit",
styles.HighlightKey("?") + " help", styles.HighlightKey("?") + " help",
@@ -71,6 +71,7 @@ func (h HelpBar) fullHelp() string {
renderRow("↑/k", "up", "enter", "view issue"), renderRow("↑/k", "up", "enter", "view issue"),
renderRow("↓/j", "down", "b", "back to list"), renderRow("↓/j", "down", "b", "back to list"),
renderRow("a", "add issue", "e", "edit title"), renderRow("a", "add issue", "e", "edit title"),
renderRow("d", "edit desc", "s", "change status"),
renderRow("x", "delete issue", "?", "help"), renderRow("x", "delete issue", "?", "help"),
renderRow("q", "quit", "", ""), renderRow("q", "quit", "", ""),
} }

View File

@@ -201,7 +201,6 @@ func (l *IssueList) SetIssues(issues []models.Issue) tea.Cmd {
return l.list.SetItems(listIssues) return l.list.SetItems(listIssues)
} }
// SelectIssueID sets the list selection to the issue with the given ID, if present.
func (l *IssueList) SelectIssueID(issueID string) { func (l *IssueList) SelectIssueID(issueID string) {
items := l.list.Items() items := l.list.Items()
for i := 0; i < len(items); i++ { for i := 0; i < len(items); i++ {

View File

@@ -12,9 +12,11 @@ type DashboardKeyMap struct {
BackToList key.Binding BackToList key.Binding
ScrollUp key.Binding ScrollUp key.Binding
ScrollDown key.Binding ScrollDown key.Binding
EditTitle key.Binding EditTitle key.Binding
AddIssue key.Binding EditDescription key.Binding
DeleteIssue key.Binding ChangeStatus key.Binding
AddIssue key.Binding
DeleteIssue key.Binding
} }
var defaultDashboardKeyMap = DashboardKeyMap{ var defaultDashboardKeyMap = DashboardKeyMap{
@@ -46,6 +48,14 @@ var defaultDashboardKeyMap = DashboardKeyMap{
key.WithKeys("e"), key.WithKeys("e"),
key.WithHelp("e", "edit title"), key.WithHelp("e", "edit title"),
), ),
EditDescription: key.NewBinding(
key.WithKeys("d"),
key.WithHelp("d", "edit description"),
),
ChangeStatus: key.NewBinding(
key.WithKeys("s"),
key.WithHelp("s", "change status"),
),
AddIssue: key.NewBinding( AddIssue: key.NewBinding(
key.WithKeys("a"), key.WithKeys("a"),
key.WithHelp("a", "add issue"), key.WithHelp("a", "add issue"),
@@ -72,16 +82,24 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd {
d.issueDetail.ScrollUp(1) d.issueDetail.ScrollUp(1)
case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown):
d.issueDetail.ScrollDown(1) d.issueDetail.ScrollDown(1)
case !d.editingTitle && !d.creatingIssue && key.Matches(msg, d.keyMap.EditTitle): case !d.editingTitle && !d.creatingIssue && !d.editingDescription && key.Matches(msg, d.keyMap.EditTitle):
// just to check if an issue is selected before trying to edit
if selected := d.issueList.SelectedItem(); selected.ID != "" { if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startEditTitle(selected) d.startEditTitle(selected)
cmd = d.titleInput.Focus() cmd = d.titleInput.Focus()
} }
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && key.Matches(msg, d.keyMap.EditDescription):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startEditDescription(selected)
cmd = d.descriptionInput.Focus()
}
case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && key.Matches(msg, d.keyMap.ChangeStatus):
if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startChooseStatus(selected)
}
case !d.editingTitle && !d.creatingIssue && key.Matches(msg, d.keyMap.AddIssue): case !d.editingTitle && !d.creatingIssue && key.Matches(msg, d.keyMap.AddIssue):
d.startCreateIssue() d.startCreateIssue()
cmd = d.createTitleInput.Focus() cmd = d.createTitleInput.Focus()
case !d.editingTitle && !d.creatingIssue && !d.confirmingDelete && key.Matches(msg, d.keyMap.DeleteIssue): case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.confirmingDelete && key.Matches(msg, d.keyMap.DeleteIssue):
if selected := d.issueList.SelectedItem(); selected.ID != "" { if selected := d.issueList.SelectedItem(); selected.ID != "" {
d.startConfirmDelete(selected.ID, d.issueList.Index()) d.startConfirmDelete(selected.ID, d.issueList.Index())
} }

View File

@@ -3,6 +3,7 @@ package dashboard
import ( import (
"github.com/LazyBachelor/LazyPM/internal/service" "github.com/LazyBachelor/LazyPM/internal/service"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -17,15 +18,22 @@ type Model struct {
height int height int
focusedPane int // 0 = list, 1 = detail focusedPane int // 0 = list, 1 = detail
editingTitle bool // should be true while we are editing a title editingTitle bool // true while we are editing a title
titleInput textinput.Model titleInput textinput.Model
editingIssueID string editingIssueID string
creatingIssue bool // should be true while we are creating a new issue
createTitleInput textinput.Model
confirmingDelete bool editingDescription bool // true while editing a description
descriptionInput textarea.Model
editingDescIssueID string
creatingIssue bool // true while creating a new issue
createTitleInput textinput.Model
confirmingDelete bool // true while confirming a delete
deleteConfirmID string deleteConfirmID string
deleteConfirmIndex int deleteConfirmIndex int
choosingStatus bool
statusIssueID string
} }
func NewDashboard(svc *service.Services) *Model { func NewDashboard(svc *service.Services) *Model {
@@ -52,6 +60,12 @@ func NewDashboard(svc *service.Services) *Model {
createTi.CharLimit = 256 createTi.CharLimit = 256
m.createTitleInput = createTi m.createTitleInput = createTi
descTa := textarea.New()
descTa.Placeholder = "Issue description..."
descTa.SetWidth(56)
descTa.SetHeight(8)
m.descriptionInput = descTa
if selected := m.issueList.SelectedItem(); selected.ID != "" { if selected := m.issueList.SelectedItem(); selected.ID != "" {
m.issueDetail.SetIssue(selected.Issue) m.issueDetail.SetIssue(selected.Issue)
} }
@@ -66,6 +80,13 @@ func (m *Model) startEditTitle(selected ListIssue) {
m.titleInput.CursorEnd() m.titleInput.CursorEnd()
} }
func (m *Model) startEditDescription(selected ListIssue) {
m.editingDescription = true
m.editingDescIssueID = selected.ID
m.descriptionInput.SetValue(selected.Issue.Description)
m.descriptionInput.CursorEnd()
}
func (m *Model) startCreateIssue() { func (m *Model) startCreateIssue() {
m.creatingIssue = true m.creatingIssue = true
m.createTitleInput.SetValue("") m.createTitleInput.SetValue("")
@@ -78,6 +99,11 @@ func (m *Model) startConfirmDelete(issueID string, index int) {
m.deleteConfirmIndex = index m.deleteConfirmIndex = index
} }
func (m *Model) startChooseStatus(selected ListIssue) {
m.choosingStatus = true
m.statusIssueID = selected.ID
}
func (m *Model) Init() tea.Cmd { func (m *Model) Init() tea.Cmd {
return nil return nil
} }

View File

@@ -14,6 +14,16 @@ type issueTitleUpdatedMsg struct {
Err error Err error
} }
type issueDescriptionUpdatedMsg struct {
IssueID string
Err error
}
type issueStatusUpdatedMsg struct {
IssueID string
Err error
}
type selectIssueMsg struct { type selectIssueMsg struct {
IssueID string IssueID string
} }
@@ -37,6 +47,22 @@ func updateIssueTitleCmd(svc *service.Services, issueID, newTitle string) tea.Cm
} }
} }
func updateIssueDescriptionCmd(svc *service.Services, issueID, newDescription string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"description": newDescription}
err := svc.Beads.UpdateIssue(context.Background(), issueID, updates, "tui")
return issueDescriptionUpdatedMsg{IssueID: issueID, Err: err}
}
}
func updateIssueStatusCmd(svc *service.Services, issueID, status string) tea.Cmd {
return func() tea.Msg {
updates := map[string]interface{}{"status": status}
err := svc.Beads.UpdateIssue(context.Background(), issueID, updates, "tui")
return issueStatusUpdatedMsg{IssueID: issueID, Err: err}
}
}
func createIssueCmd(svc *service.Services, title string) tea.Cmd { func createIssueCmd(svc *service.Services, title string) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
issue := &models.Issue{ issue := &models.Issue{
@@ -65,7 +91,26 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.Err != nil { if msg.Err != nil {
return m, nil return m, nil
} }
// reload issues and keep selection on the updated issue issues, err := m.svc.Beads.AllIssues(context.Background())
if err != nil {
return m, nil
}
setItemsCmd := m.issueList.SetIssues(issues)
for _, issue := range issues {
if issue.ID == msg.IssueID {
m.issueDetail.SetIssue(issue)
break
}
}
return m, tea.Sequence(setItemsCmd, func() tea.Msg { return selectIssueMsg{IssueID: msg.IssueID} })
case issueDescriptionUpdatedMsg:
m.editingDescription = false
m.editingDescIssueID = ""
m.descriptionInput.Blur()
if msg.Err != nil {
return m, nil
}
issues, err := m.svc.Beads.AllIssues(context.Background()) issues, err := m.svc.Beads.AllIssues(context.Background())
if err != nil { if err != nil {
return m, nil return m, nil
@@ -80,6 +125,25 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// after list is updated, select the edited issue again // after list is updated, select the edited issue again
return m, tea.Sequence(setItemsCmd, func() tea.Msg { return selectIssueMsg{IssueID: msg.IssueID} }) return m, tea.Sequence(setItemsCmd, func() tea.Msg { return selectIssueMsg{IssueID: msg.IssueID} })
case issueStatusUpdatedMsg:
m.choosingStatus = false
m.statusIssueID = ""
if msg.Err != nil {
return m, nil
}
issues, err := m.svc.Beads.AllIssues(context.Background())
if err != nil {
return m, nil
}
setItemsCmd := m.issueList.SetIssues(issues)
for _, issue := range issues {
if issue.ID == msg.IssueID {
m.issueDetail.SetIssue(issue)
break
}
}
return m, tea.Sequence(setItemsCmd, func() tea.Msg { return selectIssueMsg{IssueID: msg.IssueID} })
case selectIssueMsg: case selectIssueMsg:
m.issueList.SelectIssueID(msg.IssueID) m.issueList.SelectIssueID(msg.IssueID)
return m, nil return m, nil
@@ -138,6 +202,30 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
if m.choosingStatus {
switch msg.String() {
case "o":
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, updateIssueStatusCmd(m.svc, issueID, "open")
case "i":
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, updateIssueStatusCmd(m.svc, issueID, "in_progress")
case "c":
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, updateIssueStatusCmd(m.svc, issueID, "closed")
case "esc":
m.choosingStatus = false
m.statusIssueID = ""
return m, nil
}
}
if m.creatingIssue { if m.creatingIssue {
if msg.String() == "enter" { if msg.String() == "enter" {
title := m.createTitleInput.Value() title := m.createTitleInput.Value()
@@ -159,7 +247,6 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.editingTitle { if m.editingTitle {
if msg.String() == "enter" { if msg.String() == "enter" {
newTitle := m.titleInput.Value() newTitle := m.titleInput.Value()
// dont update if its empty or same as the original
if newTitle != "" { if newTitle != "" {
return m, updateIssueTitleCmd(m.svc, m.editingIssueID, newTitle) return m, updateIssueTitleCmd(m.svc, m.editingIssueID, newTitle)
} }
@@ -175,6 +262,26 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd 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.svc, 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
}
if m.issueList.FilterState() == list.Filtering { if m.issueList.FilterState() == list.Filtering {
cmd, _ := m.issueList.Update(msg) cmd, _ := m.issueList.Update(msg)
return m, cmd return m, cmd

View File

@@ -49,6 +49,21 @@ func (m *Model) View() string {
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) 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)
m.descriptionInput.SetHeight(10)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"),
m.descriptionInput.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.creatingIssue { if m.creatingIssue {
createBoxWidth := min(60, m.width-4) createBoxWidth := min(60, m.width-4)
m.createTitleInput.Width = createBoxWidth - 2 m.createTitleInput.Width = createBoxWidth - 2
@@ -76,5 +91,19 @@ func (m *Model) View() string {
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox) return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox)
} }
if m.choosingStatus {
statusContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change status for "+m.statusIssueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress c = closed"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
statusBoxWidth := min(50, m.width-4)
statusBox := styles.ContainerStyle.
Width(statusBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(statusContent)
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, statusBox)
}
return mainView return mainView
} }