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:
@@ -16,6 +16,8 @@ type KeyMap struct {
|
||||
MoveIssueRight key.Binding
|
||||
SubmitValidation key.Binding
|
||||
AddComment key.Binding
|
||||
SelectSprint key.Binding
|
||||
CreateSprint key.Binding
|
||||
}
|
||||
|
||||
var defaultKanbanKeyMap = KeyMap{
|
||||
@@ -44,6 +46,14 @@ var defaultKanbanKeyMap = KeyMap{
|
||||
key.WithKeys("c"),
|
||||
key.WithHelp("c", "add comment"),
|
||||
),
|
||||
SelectSprint: key.NewBinding(
|
||||
key.WithKeys("S"),
|
||||
key.WithHelp("S", "select sprint"),
|
||||
),
|
||||
CreateSprint: key.NewBinding(
|
||||
key.WithKeys("n"),
|
||||
key.WithHelp("n", "new sprint"),
|
||||
),
|
||||
}
|
||||
|
||||
func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
|
||||
@@ -117,6 +127,12 @@ func (m *Model) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
|
||||
if selected := fl.SelectedItem(); selected.ID != "" {
|
||||
cmd = m.startConfirmDelete(selected.ID, fl.Index())
|
||||
}
|
||||
|
||||
case m.notInModalMsgWithKey(msg, m.keyMap.SelectSprint):
|
||||
cmd = m.startSelectSprint()
|
||||
|
||||
case m.notInModalMsgWithKey(msg, m.keyMap.CreateSprint):
|
||||
cmd = m.startCreateSprint()
|
||||
}
|
||||
|
||||
return cmd
|
||||
|
||||
@@ -20,6 +20,7 @@ type (
|
||||
|
||||
type Model struct {
|
||||
header Header
|
||||
backlogList IssueList
|
||||
todoList IssueList
|
||||
inProgList IssueList
|
||||
blockedList IssueList
|
||||
@@ -31,6 +32,9 @@ type Model struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
currentSprintNum int
|
||||
backlogNum int
|
||||
|
||||
// Modal and Focus management
|
||||
modalManager *modal.Manager
|
||||
focusManager *modal.FocusManager
|
||||
@@ -61,15 +65,33 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
|
||||
deleteIndex: -1,
|
||||
}
|
||||
|
||||
// Setup lists
|
||||
m.issueDetail = components.NewIssueDetail()
|
||||
m.helpBar = components.NewHelpBar(components.ViewKanban)
|
||||
|
||||
allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
todoIssues := components.StatusOnly(allIssues, models.StatusOpen)
|
||||
inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress)
|
||||
blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked)
|
||||
doneIssues := components.StatusOnly(allIssues, models.StatusClosed)
|
||||
ctx := context.Background()
|
||||
backlogNum, _ := app.Issues.GetBacklogSprint(ctx)
|
||||
m.backlogNum = backlogNum
|
||||
|
||||
backlogIssues, _ := app.Issues.GetIssuesNotInAnySprint(ctx)
|
||||
m.backlogList = components.NewIssueListFromIssues(app, backlogIssues, 20, 10)
|
||||
|
||||
sprints, _ := app.Issues.GetSprints(ctx)
|
||||
m.currentSprintNum = 0
|
||||
for _, sprintNum := range sprints {
|
||||
if sprintNum != backlogNum {
|
||||
m.currentSprintNum = sprintNum
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var sprintIssues []*models.Issue
|
||||
if m.currentSprintNum > 0 {
|
||||
sprintIssues, _ = app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum)
|
||||
}
|
||||
todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen)
|
||||
inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress)
|
||||
blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked)
|
||||
doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed)
|
||||
|
||||
m.todoList = components.NewIssueListFromIssues(app, todoIssues, 20, 10)
|
||||
m.inProgList = components.NewIssueListFromIssues(app, inProgIssues, 20, 10)
|
||||
@@ -77,16 +99,17 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
|
||||
m.doneList = components.NewIssueListFromIssues(app, doneIssues, 20, 10)
|
||||
|
||||
// Setup focus areas for kanban columns
|
||||
m.focusManager.EnableArea(modal.FocusColumn0)
|
||||
m.focusManager.EnableArea(modal.FocusColumn1)
|
||||
m.focusManager.EnableArea(modal.FocusColumn2)
|
||||
m.focusManager.EnableArea(modal.FocusColumn3)
|
||||
m.focusManager.EnableArea(modal.FocusColumn4)
|
||||
m.focusManager.SetCurrent(modal.FocusColumn1)
|
||||
m.focusManager.SetCurrent(modal.FocusColumn0)
|
||||
|
||||
// Register modals
|
||||
m.registerModals()
|
||||
|
||||
if selected := m.todoList.SelectedItem(); selected.ID != "" {
|
||||
if selected := m.backlogList.SelectedItem(); selected.ID != "" {
|
||||
m.setDetailIssueWithComments(selected.Issue)
|
||||
}
|
||||
|
||||
@@ -138,7 +161,7 @@ func (m *Model) IsFocusedOnDetail() bool {
|
||||
|
||||
func (m *Model) ToggleFocus() {
|
||||
if m.focusManager.IsDetailFocused() {
|
||||
m.focusManager.SetCurrent(modal.FocusColumn1)
|
||||
m.focusManager.SetCurrent(modal.FocusColumn0)
|
||||
m.issueDetail.SetFocused(false)
|
||||
} else {
|
||||
m.focusManager.SetCurrent(modal.FocusDetail)
|
||||
@@ -148,6 +171,8 @@ func (m *Model) ToggleFocus() {
|
||||
|
||||
func (m *Model) FocusedIssueList() *IssueList {
|
||||
switch m.focusManager.Current() {
|
||||
case modal.FocusColumn0:
|
||||
return &m.backlogList
|
||||
case modal.FocusColumn1:
|
||||
return &m.todoList
|
||||
case modal.FocusColumn2:
|
||||
@@ -157,7 +182,7 @@ func (m *Model) FocusedIssueList() *IssueList {
|
||||
case modal.FocusColumn4:
|
||||
return &m.doneList
|
||||
default:
|
||||
return &m.todoList
|
||||
return &m.backlogList
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +206,8 @@ func (m *Model) setDetailIssueWithComments(issue models.Issue) {
|
||||
|
||||
func statusForColumn(col modal.FocusArea) models.Status {
|
||||
switch col {
|
||||
case modal.FocusColumn0:
|
||||
return models.StatusOpen
|
||||
case modal.FocusColumn1:
|
||||
return models.StatusOpen
|
||||
case modal.FocusColumn2:
|
||||
@@ -204,25 +231,31 @@ func (m *Model) moveIssue(delta int) tea.Cmd {
|
||||
currentCol := m.focusManager.Current()
|
||||
var newCol modal.FocusArea
|
||||
switch currentCol {
|
||||
case modal.FocusColumn1:
|
||||
case modal.FocusColumn0: // Backlog
|
||||
if delta > 0 {
|
||||
newCol = modal.FocusColumn2
|
||||
return m.moveIssueToSprint(selected.ID, m.currentSprintNum)
|
||||
}
|
||||
case modal.FocusColumn2:
|
||||
case modal.FocusColumn1: // To Do
|
||||
if delta > 0 {
|
||||
newCol = modal.FocusColumn3
|
||||
newCol = modal.FocusColumn2 // To In Progress
|
||||
} else if delta < 0 {
|
||||
return m.moveIssueToBacklog(selected.ID)
|
||||
}
|
||||
case modal.FocusColumn2: // In Progress
|
||||
if delta > 0 {
|
||||
newCol = modal.FocusColumn3 // To Blocked
|
||||
} else {
|
||||
newCol = modal.FocusColumn1
|
||||
newCol = modal.FocusColumn1 // To To Do
|
||||
}
|
||||
case modal.FocusColumn3:
|
||||
case modal.FocusColumn3: // Blocked
|
||||
if delta > 0 {
|
||||
newCol = modal.FocusColumn4
|
||||
newCol = modal.FocusColumn4 // To Done
|
||||
} else {
|
||||
newCol = modal.FocusColumn2
|
||||
newCol = modal.FocusColumn2 // To In Progress
|
||||
}
|
||||
case modal.FocusColumn4:
|
||||
case modal.FocusColumn4: // Done
|
||||
if delta < 0 {
|
||||
newCol = modal.FocusColumn3
|
||||
newCol = modal.FocusColumn3 // To Blocked
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,3 +266,27 @@ func (m *Model) moveIssue(delta int) tea.Cmd {
|
||||
newStatus := statusForColumn(newCol)
|
||||
return msgs.UpdateIssueStatusCmd(m.app, selected.ID, string(newStatus))
|
||||
}
|
||||
|
||||
// moveIssueToSprint moves an issue to a sprint
|
||||
func (m *Model) moveIssueToSprint(issueID string, sprintNum int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := m.app.Issues.AddIssueToSprint(context.Background(), issueID, sprintNum)
|
||||
m.submitValidation()
|
||||
if err != nil {
|
||||
return m.refreshIssueListsAndSelectIssue(issueID)()
|
||||
}
|
||||
return m.refreshIssueListsAndSelectIssue(issueID)()
|
||||
}
|
||||
}
|
||||
|
||||
// moveIssueToBacklog removes an issue from the current sprint (moves it to backlog)
|
||||
func (m *Model) moveIssueToBacklog(issueID string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := m.app.Issues.RemoveIssueFromSprint(context.Background(), issueID, m.currentSprintNum)
|
||||
m.submitValidation()
|
||||
if err != nil {
|
||||
return m.refreshIssueListsAndSelectIssue(issueID)()
|
||||
}
|
||||
return m.refreshIssueListsAndSelectIssue(issueID)()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package kanban
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"charm.land/bubbles/v2/list"
|
||||
@@ -14,42 +15,63 @@ import (
|
||||
)
|
||||
|
||||
func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd {
|
||||
allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
todoIssues := components.StatusOnly(allIssues, models.StatusOpen)
|
||||
inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress)
|
||||
blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked)
|
||||
doneIssues := components.StatusOnly(allIssues, models.StatusClosed)
|
||||
backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(ctx)
|
||||
backlogCmd := m.backlogList.SetIssues(backlogIssues)
|
||||
|
||||
sprintIssues, _ := m.app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum)
|
||||
todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen)
|
||||
inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress)
|
||||
blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked)
|
||||
doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed)
|
||||
|
||||
todoCmd := m.todoList.SetIssues(todoIssues)
|
||||
inProgCmd := m.inProgList.SetIssues(inProgIssues)
|
||||
blockedCmd := m.blockedList.SetIssues(blockedIssues)
|
||||
doneCmd := m.doneList.SetIssues(doneIssues)
|
||||
|
||||
// Find the issue to determine which column it belongs to
|
||||
var targetStatus models.Status
|
||||
for _, issue := range allIssues {
|
||||
var inBacklog bool
|
||||
for _, issue := range sprintIssues {
|
||||
if issue == nil {
|
||||
continue
|
||||
}
|
||||
if issue.ID == issueID {
|
||||
m.setDetailIssueWithComments(*issue)
|
||||
targetStatus = issue.Status
|
||||
m.setDetailIssueWithComments(*issue)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Check if issue is in backlog
|
||||
if targetStatus == "" {
|
||||
for _, issue := range backlogIssues {
|
||||
if issue == nil {
|
||||
continue
|
||||
}
|
||||
if issue.ID == issueID {
|
||||
inBacklog = true
|
||||
m.setDetailIssueWithComments(*issue)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch targetStatus {
|
||||
case models.StatusOpen:
|
||||
switch {
|
||||
case inBacklog:
|
||||
m.focusManager.SetCurrent(modal.FocusColumn0)
|
||||
case targetStatus == models.StatusOpen:
|
||||
m.focusManager.SetCurrent(modal.FocusColumn1)
|
||||
case models.StatusInProgress:
|
||||
case targetStatus == models.StatusInProgress:
|
||||
m.focusManager.SetCurrent(modal.FocusColumn2)
|
||||
case models.StatusBlocked:
|
||||
case targetStatus == models.StatusBlocked:
|
||||
m.focusManager.SetCurrent(modal.FocusColumn3)
|
||||
case models.StatusClosed:
|
||||
case targetStatus == models.StatusClosed:
|
||||
m.focusManager.SetCurrent(modal.FocusColumn4)
|
||||
}
|
||||
|
||||
return tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
|
||||
return tea.Sequence(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
|
||||
return msgs.SelectIssueMsg{IssueID: issueID}
|
||||
})
|
||||
}
|
||||
@@ -60,6 +82,27 @@ func (m *Model) refreshAndSubmit(issueID string) tea.Cmd {
|
||||
return refreshCmd
|
||||
}
|
||||
|
||||
// refreshAllIssueLists refreshes all issue lists without selecting a specific issue
|
||||
func (m *Model) refreshAllIssueLists() tea.Cmd {
|
||||
ctx := context.Background()
|
||||
|
||||
backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(ctx)
|
||||
backlogCmd := m.backlogList.SetIssues(backlogIssues)
|
||||
|
||||
sprintIssues, _ := m.app.Issues.GetIssuesBySprint(ctx, m.currentSprintNum)
|
||||
todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen)
|
||||
inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress)
|
||||
blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked)
|
||||
doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed)
|
||||
|
||||
todoCmd := m.todoList.SetIssues(todoIssues)
|
||||
inProgCmd := m.inProgList.SetIssues(inProgIssues)
|
||||
blockedCmd := m.blockedList.SetIssues(blockedIssues)
|
||||
doneCmd := m.doneList.SetIssues(doneIssues)
|
||||
|
||||
return tea.Batch(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd)
|
||||
}
|
||||
|
||||
// Modal action handlers
|
||||
|
||||
func (m *Model) startConfirmExit() tea.Cmd {
|
||||
@@ -121,6 +164,58 @@ func (m *Model) startChooseType(selected ListIssue) tea.Cmd {
|
||||
return m.modalManager.ShowModal(modal.ModalSelectType)
|
||||
}
|
||||
|
||||
func (m *Model) startSelectSprint() tea.Cmd {
|
||||
sprints, err := m.app.Issues.GetSprints(context.Background())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
options := make([]modal.SelectOption, 0, len(sprints))
|
||||
for _, sprintNum := range sprints {
|
||||
if sprintNum == m.backlogNum {
|
||||
continue
|
||||
}
|
||||
label := fmt.Sprintf("Sprint %d", sprintNum)
|
||||
key := fmt.Sprintf("%d", sprintNum)
|
||||
if sprintNum > 9 {
|
||||
continue
|
||||
}
|
||||
options = append(options, modal.SelectOption{
|
||||
Key: key,
|
||||
Label: label,
|
||||
Value: fmt.Sprintf("%d", sprintNum),
|
||||
})
|
||||
}
|
||||
|
||||
if len(options) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sprintModal := modal.NewSelectModal(modal.SelectConfig{
|
||||
ID: "dynamic-sprint-select",
|
||||
Label: "Select sprint to view:",
|
||||
Options: options,
|
||||
CancelKey: "esc",
|
||||
})
|
||||
|
||||
return m.modalManager.PushModal(sprintModal)
|
||||
}
|
||||
|
||||
func (m *Model) startCreateSprint() tea.Cmd {
|
||||
return tea.Sequence(
|
||||
func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
newSprintNum, err := m.app.Issues.AddSprint(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
m.currentSprintNum = newSprintNum
|
||||
return nil
|
||||
},
|
||||
m.refreshAllIssueLists(),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *Model) startEditAssignee(selected ListIssue) tea.Cmd {
|
||||
m.currentIssueID = selected.ID
|
||||
assigneeModal := m.modalManager.GetTextInputModal(modal.ModalEditAssignee)
|
||||
@@ -210,6 +305,15 @@ func (m *Model) handleModalCompleted(msg modal.ModalCompletedMsg) tea.Cmd {
|
||||
cmd := msgs.UpdateIssueTypeCmd(m.app, m.currentIssueID, issueType)
|
||||
return func() tea.Msg { return cmd() }
|
||||
}
|
||||
case modal.ModalSelectSprint, "dynamic-sprint-select":
|
||||
if r, ok := msg.Value.(modal.SelectResult); ok {
|
||||
sprintNum, err := strconv.Atoi(r.SelectedValue)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
m.currentSprintNum = sprintNum
|
||||
return m.refreshAllIssueLists()
|
||||
}
|
||||
case modal.ModalCloseReason:
|
||||
if r, ok := msg.Value.(modal.TextAreaResult); ok && r.Value != "" {
|
||||
cmd := msgs.CloseIssueCmd(m.app, m.currentIssueID, r.Value)
|
||||
@@ -248,6 +352,7 @@ func (m *Model) handleModalCancelled(msg modal.ModalCancelledMsg) {
|
||||
m.currentIssueID = ""
|
||||
case modal.ModalSelectType:
|
||||
m.currentIssueID = ""
|
||||
case modal.ModalSelectSprint, "dynamic-sprint-select":
|
||||
case modal.ModalCloseReason:
|
||||
m.currentIssueID = ""
|
||||
case modal.ModalAddComment:
|
||||
@@ -322,6 +427,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, m.refreshIssueListsAndSelectIssue(msg.IssueID)
|
||||
|
||||
case msgs.SelectIssueMsg:
|
||||
m.backlogList.SelectIssueID(msg.IssueID)
|
||||
m.todoList.SelectIssueID(msg.IssueID)
|
||||
m.inProgList.SelectIssueID(msg.IssueID)
|
||||
m.blockedList.SelectIssueID(msg.IssueID)
|
||||
@@ -333,21 +439,23 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg.Err != nil || msg.Issue == nil {
|
||||
return m, nil
|
||||
}
|
||||
allIssues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
if err != nil {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
todoIssues := components.StatusOnly(allIssues, models.StatusOpen)
|
||||
inProgIssues := components.StatusOnly(allIssues, models.StatusInProgress)
|
||||
blockedIssues := components.StatusOnly(allIssues, models.StatusBlocked)
|
||||
doneIssues := components.StatusOnly(allIssues, models.StatusClosed)
|
||||
backlogIssues, _ := m.app.Issues.GetIssuesNotInAnySprint(context.Background())
|
||||
backlogCmd := m.backlogList.SetIssues(backlogIssues)
|
||||
|
||||
sprintIssues, _ := m.app.Issues.GetIssuesBySprint(context.Background(), m.currentSprintNum)
|
||||
todoIssues := components.StatusOnly(sprintIssues, models.StatusOpen)
|
||||
inProgIssues := components.StatusOnly(sprintIssues, models.StatusInProgress)
|
||||
blockedIssues := components.StatusOnly(sprintIssues, models.StatusBlocked)
|
||||
doneIssues := components.StatusOnly(sprintIssues, models.StatusClosed)
|
||||
|
||||
todoCmd := m.todoList.SetIssues(todoIssues)
|
||||
inProgCmd := m.inProgList.SetIssues(inProgIssues)
|
||||
blockedCmd := m.blockedList.SetIssues(blockedIssues)
|
||||
doneCmd := m.doneList.SetIssues(doneIssues)
|
||||
|
||||
allIssues := append(backlogIssues, sprintIssues...)
|
||||
|
||||
selectedIssue := msg.Issue
|
||||
if selectedIssue.ID == "" {
|
||||
for _, issue := range allIssues {
|
||||
@@ -360,7 +468,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
m.setDetailIssueWithComments(*selectedIssue)
|
||||
m.submitValidation()
|
||||
return m, tea.Sequence(todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
|
||||
return m, tea.Sequence(backlogCmd, todoCmd, inProgCmd, blockedCmd, doneCmd, func() tea.Msg {
|
||||
return msgs.SelectIssueMsg{IssueID: selectedIssue.ID}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package kanban
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/LazyBachelor/LazyPM/internal/style"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/components"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/modal"
|
||||
"github.com/LazyBachelor/LazyPM/internal/style"
|
||||
)
|
||||
|
||||
func (m *Model) View() tea.View {
|
||||
@@ -16,6 +18,14 @@ func (m *Model) View() tea.View {
|
||||
m.helpBar.SetWidth(m.width)
|
||||
m.modalManager.SetSize(m.width, m.height)
|
||||
|
||||
var sprintName string
|
||||
if m.currentSprintNum > 0 {
|
||||
sprintName = fmt.Sprintf("Sprint %d", m.currentSprintNum)
|
||||
} else {
|
||||
sprintName = "No Sprint"
|
||||
}
|
||||
|
||||
m.header = components.NewHeader(fmt.Sprintf("Kanban Board - %s", sprintName))
|
||||
header := m.header.View(m.width)
|
||||
headerHeight := m.header.Height()
|
||||
|
||||
@@ -24,49 +34,53 @@ func (m *Model) View() tea.View {
|
||||
|
||||
contentHeight := m.height - headerHeight - footerHeight
|
||||
totalContentWidth := m.width - 1
|
||||
colWidth := max(totalContentWidth/4, 20)
|
||||
colWidth := max(totalContentWidth/5, 16)
|
||||
|
||||
// Calculate initial list height (half of content height, minimum 5 rows)
|
||||
listHeight := contentHeight / 2
|
||||
if listHeight < 5 {
|
||||
listHeight = contentHeight
|
||||
}
|
||||
|
||||
m.backlogList.SetSize(colWidth, listHeight-1)
|
||||
m.todoList.SetSize(colWidth, listHeight-1)
|
||||
m.inProgList.SetSize(colWidth, listHeight-1)
|
||||
m.blockedList.SetSize(colWidth, listHeight-1)
|
||||
m.doneList.SetSize(colWidth, listHeight-1)
|
||||
|
||||
// Only highlight the focused column's selected row
|
||||
currentFocus := m.focusManager.Current()
|
||||
m.backlogList.SetHighlightSelected(currentFocus == modal.FocusColumn0)
|
||||
m.todoList.SetHighlightSelected(currentFocus == modal.FocusColumn1)
|
||||
m.inProgList.SetHighlightSelected(currentFocus == modal.FocusColumn2)
|
||||
m.blockedList.SetHighlightSelected(currentFocus == modal.FocusColumn3)
|
||||
m.doneList.SetHighlightSelected(currentFocus == modal.FocusColumn4)
|
||||
|
||||
todoLabel := style.LabelStyle.Render("To Do")
|
||||
inProgLabel := style.LabelStyle.Render("In Progress")
|
||||
blockedLabel := style.LabelStyle.Render("Blocked")
|
||||
doneLabel := style.LabelStyle.Render("Done")
|
||||
backlogLabel := style.LabelStyle.Render("Backlog")
|
||||
todoLabel := style.LabelStyle.Render(fmt.Sprintf("%s - To Do", sprintName))
|
||||
inProgLabel := style.LabelStyle.Render(fmt.Sprintf("%s - In Progress", sprintName))
|
||||
blockedLabel := style.LabelStyle.Render(fmt.Sprintf("%s - Blocked", sprintName))
|
||||
doneLabel := style.LabelStyle.Render(fmt.Sprintf("%s - Done", sprintName))
|
||||
|
||||
highlight := lipgloss.NewStyle().Foreground(style.Primary).Bold(true)
|
||||
switch currentFocus {
|
||||
case modal.FocusColumn0:
|
||||
backlogLabel = highlight.Render("Backlog ▶")
|
||||
case modal.FocusColumn1:
|
||||
todoLabel = highlight.Render("To Do ▶")
|
||||
todoLabel = highlight.Render(fmt.Sprintf("%s - To Do ▶", sprintName))
|
||||
case modal.FocusColumn2:
|
||||
inProgLabel = highlight.Render("In Progress ▶")
|
||||
inProgLabel = highlight.Render(fmt.Sprintf("%s - In Progress ▶", sprintName))
|
||||
case modal.FocusColumn3:
|
||||
blockedLabel = highlight.Render("Blocked ▶")
|
||||
blockedLabel = highlight.Render(fmt.Sprintf("%s - Blocked ▶", sprintName))
|
||||
case modal.FocusColumn4:
|
||||
doneLabel = highlight.Render("Done ▶")
|
||||
doneLabel = highlight.Render(fmt.Sprintf("%s - Done ▶", sprintName))
|
||||
}
|
||||
|
||||
backlogCol := lipgloss.JoinVertical(lipgloss.Left, backlogLabel, m.backlogList.View())
|
||||
todoCol := lipgloss.JoinVertical(lipgloss.Left, todoLabel, m.todoList.View())
|
||||
inProgCol := lipgloss.JoinVertical(lipgloss.Left, inProgLabel, m.inProgList.View())
|
||||
blockedCol := lipgloss.JoinVertical(lipgloss.Left, blockedLabel, m.blockedList.View())
|
||||
doneCol := lipgloss.JoinVertical(lipgloss.Left, doneLabel, m.doneList.View())
|
||||
|
||||
board := lipgloss.JoinHorizontal(lipgloss.Left, todoCol, inProgCol, blockedCol, doneCol)
|
||||
board := lipgloss.JoinHorizontal(lipgloss.Left, backlogCol, todoCol, inProgCol, blockedCol, doneCol)
|
||||
boardHeight := lipgloss.Height(board)
|
||||
|
||||
detailHeight := max(contentHeight-boardHeight, 5)
|
||||
|
||||
Reference in New Issue
Block a user