update description and status, and replicating help message from lazybeads
This commit is contained in:
@@ -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"}
|
||||||
|
|||||||
@@ -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", "", ""),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++ {
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user