Merge pull request #75 from LazyBachelor/LPM-140

LPM-140 The Sprinting Update
Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer.

Changes:

Introduces sprint entities in storage and exposes sprint operations via IssueService.
Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes.
Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
This commit is contained in:
Robin Olsen
2026-03-23 11:26:04 -07:00
committed by GitHub
parent 06404708ba
commit d900a5fcd3
33 changed files with 2378 additions and 771 deletions

View File

@@ -7,6 +7,7 @@ const (
FocusNone FocusArea = iota
FocusList
FocusDetail
FocusColumn0 // Backlog
FocusColumn1
FocusColumn2
FocusColumn3
@@ -46,12 +47,25 @@ func (f *FocusManager) IsFocused(area FocusArea) bool {
// IsListFocused returns true if any list area is focused
func (f *FocusManager) IsListFocused() bool {
return f.currentArea == FocusList ||
f.currentArea == FocusColumn0 ||
f.currentArea == FocusColumn1 ||
f.currentArea == FocusColumn2 ||
f.currentArea == FocusColumn3 ||
f.currentArea == FocusColumn4
}
// NextColumn moves focus to the next column (for kanban)
func (f *FocusManager) NextColumn() {
areas := []FocusArea{FocusColumn0, FocusColumn1, FocusColumn2, FocusColumn3, FocusColumn4}
f.cycleFocus(areas)
}
// PreviousColumn moves focus to the previous column (for kanban)
func (f *FocusManager) PreviousColumn() {
areas := []FocusArea{FocusColumn4, FocusColumn3, FocusColumn2, FocusColumn1, FocusColumn0}
f.cycleFocus(areas)
}
// IsDetailFocused returns true if the detail area is focused
func (f *FocusManager) IsDetailFocused() bool {
return f.currentArea == FocusDetail
@@ -87,18 +101,6 @@ func (f *FocusManager) Previous() {
f.cycleFocus(areas)
}
// NextColumn moves focus to the next column (for kanban)
func (f *FocusManager) NextColumn() {
areas := []FocusArea{FocusColumn1, FocusColumn2, FocusColumn3, FocusColumn4}
f.cycleFocus(areas)
}
// PreviousColumn moves focus to the previous column (for kanban)
func (f *FocusManager) PreviousColumn() {
areas := []FocusArea{FocusColumn4, FocusColumn3, FocusColumn2, FocusColumn1}
f.cycleFocus(areas)
}
// cycleFocus finds the next enabled area in the given order
func (f *FocusManager) cycleFocus(areas []FocusArea) {
// Find current position

View File

@@ -262,4 +262,11 @@ func RegisterCommonModals(m *Manager) {
Label: "Change type:",
Options: TypeOptions(),
}))
// Sprint Select Modal
m.RegisterModal(NewSelectModal(SelectConfig{
ID: ModalSelectSprint,
Label: "Select sprint:",
Options: []SelectOption{},
}))
}

View File

@@ -62,6 +62,7 @@ const (
ModalSelectCloseReason = "select-close-reason"
ModalSelectPriority = "select-priority"
ModalSelectType = "select-type"
ModalSelectSprint = "select-sprint"
)
// ModalStack manages a stack of active modals with priority handling

View File

@@ -1,6 +1,8 @@
package modal
import (
"fmt"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/LazyBachelor/LazyPM/internal/style"
@@ -176,7 +178,6 @@ func StatusOptions() []SelectOption {
{Key: "o", Label: "open", Value: "open"},
{Key: "i", Label: "in_progress", Value: "in_progress"},
{Key: "b", Label: "blocked", Value: "blocked"},
{Key: "r", Label: "ready_to_sprint", Value: "ready_to_sprint"},
{Key: "c", Label: "closing", Value: "closing"},
}
}
@@ -213,3 +214,27 @@ func CloseReasonOptions() []SelectOption {
{Key: "h", Label: "Other", Value: "other"},
}
}
// SprintOptions generates sprint selection options from available sprints
func SprintOptions(sprints []int, backlogNum int) []SelectOption {
options := make([]SelectOption, 0, len(sprints))
for _, sprintNum := range sprints {
label := fmt.Sprintf("Sprint %d", sprintNum)
if sprintNum == backlogNum {
label = "Backlog"
}
key := fmt.Sprintf("%d", sprintNum)
if sprintNum <= 9 {
key = fmt.Sprintf("%d", sprintNum)
} else {
continue
}
options = append(options, SelectOption{
Key: key,
Label: label,
Value: fmt.Sprintf("%d", sprintNum),
})
}
return options
}