TASK BACKLOG REFINEMENT fix for closing reason

This commit is contained in:
Ine Maria Nilssen Aanonsen
2026-03-12 11:46:58 +01:00
parent 9aa940fcca
commit 2ce04896ee
7 changed files with 229 additions and 28 deletions

View File

@@ -46,23 +46,27 @@ type Model struct {
deleteConfirmID string
deleteConfirmIndex int
choosingStatus bool // true while choosing a status
statusIssueID string
choosingPriority bool // true while choosing a priority
priorityIssueID string
choosingType bool // true while choosing a type
typeIssueID string
editingAssignee bool // true while editing assignee
assigneeInput textinput.Model
assigneeIssueID string
addingComment bool // true while adding a comment
commentInput textarea.Model
commentIssueID string
feedbackChan chan models.ValidationFeedback
quitChan chan bool
currentFeedback models.ValidationFeedback
showComplete bool
submitChan chan<- struct{}
choosingStatus bool
statusIssueID string
choosingPriority bool
priorityIssueID string
choosingType bool
typeIssueID string
editingAssignee bool
assigneeInput textinput.Model
assigneeIssueID string
addingComment bool
commentInput textarea.Model
commentIssueID string
choosingCloseReason bool
closeReasonIssueID string
closingOtherReason bool
closeReasonInput textarea.Model
feedbackChan chan models.ValidationFeedback
quitChan chan bool
currentFeedback models.ValidationFeedback
showComplete bool
submitChan chan<- struct{}
}
func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model {
@@ -92,6 +96,12 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
m.descriptionInput = inputs.Description
m.assigneeInput = inputs.Assignee
closeReasonTa := textarea.New()
closeReasonTa.Placeholder = "Enter closing reason..."
closeReasonTa.SetWidth(56)
closeReasonTa.SetHeight(4)
m.closeReasonInput = closeReasonTa
commentTa := textarea.New()
commentTa.Placeholder = "Write your comment..."
commentTa.SetWidth(56)
@@ -189,7 +199,9 @@ func (m *Model) Init() tea.Cmd {
// IsInModal returns true when a modal (edit, create, delete confirm, choose status/priority/type) is active.
func (m *Model) IsInModal() bool {
return m.editingTitle || m.creatingIssue || m.editingDescription ||
m.choosingStatus || m.choosingPriority || m.confirmingDelete || m.choosingType || m.editingAssignee
m.choosingStatus || m.choosingPriority || m.confirmingDelete ||
m.choosingType || m.editingAssignee ||
m.choosingCloseReason || m.closingOtherReason
}
func (m *Model) IsFocusedOnList() bool {

View File

@@ -361,11 +361,13 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.statusIssueID = ""
return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint))
case "c":
m.logAction("tui selected issue status closed")
m.logAction("tui selected issue status closing")
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusClosed))
m.choosingCloseReason = true
m.closeReasonIssueID = issueID
return m, nil
case "esc":
m.logAction("tui canceled status picker")
m.choosingStatus = false
@@ -374,6 +376,64 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if m.choosingCloseReason {
var reason string
switch msg.String() {
case "d":
m.logAction("tui selected close reason done")
reason = "Done"
case "u":
m.logAction("tui selected close reason duplicate issue")
reason = "Duplicate issue"
case "w":
m.logAction("tui selected close reason won't fix")
reason = "Won't fix"
case "o":
m.logAction("tui selected close reason obsolete")
reason = "Obsolete"
case "h":
m.logAction("tui selected close reason other")
m.choosingCloseReason = false
m.closingOtherReason = true
m.closeReasonInput.SetValue("")
return m, nil
case "esc":
m.logAction("tui canceled close reason picker")
m.choosingCloseReason = false
m.closeReasonIssueID = ""
return m, nil
}
if reason != "" {
issueID := m.closeReasonIssueID
m.choosingCloseReason = false
m.closeReasonIssueID = ""
return m, issues.CloseIssueCmd(m.app, issueID, reason)
}
}
if m.closingOtherReason {
switch msg.String() {
case "enter":
reason := m.closeReasonInput.Value()
if reason != "" {
m.logAction("tui submitted custom close reason")
issueID := m.closeReasonIssueID
m.closingOtherReason = false
m.closeReasonIssueID = ""
return m, issues.CloseIssueCmd(m.app, issueID, reason)
}
case "esc":
m.logAction("tui canceled custom close reason")
m.closingOtherReason = false
m.closeReasonIssueID = ""
return m, nil
}
var cmd tea.Cmd
m.closeReasonInput, cmd = m.closeReasonInput.Update(msg)
return m, cmd
}
if m.choosingPriority {
switch msg.String() {
case "0", "1", "2", "3", "4":

View File

@@ -149,7 +149,7 @@ func (m *Model) View() string {
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 ready_to_sprint = r"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress r = ready_to_sprint c = closing (choose reason)"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
statusBoxWidth := min(50, m.width-4)
@@ -174,6 +174,35 @@ func (m *Model) View() string {
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, priorityBox)
}
if m.choosingCloseReason {
reasonContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Choose closing reason for "+m.closeReasonIssueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("d = Done u = Duplicate issue w = Won't fix o = Obsolete h = Other"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
reasonBoxWidth := min(70, m.width-4)
reasonBox := styles.ContainerStyle.
Width(reasonBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(reasonContent)
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox)
}
if m.closingOtherReason {
editBoxWidth := min(60, m.width-4)
m.closeReasonInput.SetWidth(editBoxWidth - 2)
m.closeReasonInput.SetHeight(4)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Enter closing reason for "+m.closeReasonIssueID+" (Enter to save, Esc to cancel):"),
m.closeReasonInput.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.choosingType {
typeContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Change type for "+m.typeIssueID+":"),

View File

@@ -56,11 +56,15 @@ type Model struct {
editingAssignee bool // true while editing assignee
assigneeInput textinput.Model
assigneeIssueID string
feedbackChan chan models.ValidationFeedback
quitChan chan bool
submitChan chan<- struct{}
currentFeedback models.ValidationFeedback
showComplete bool
choosingCloseReason bool // true while choosing a close reason
closeReasonIssueID string
closingOtherReason bool // true while entering a custom close reason
closeReasonInput textarea.Model
feedbackChan chan models.ValidationFeedback
quitChan chan bool
submitChan chan<- struct{}
currentFeedback models.ValidationFeedback
showComplete bool
}
func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model {
@@ -94,6 +98,12 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui
m.descriptionInput = inputs.Description
m.assigneeInput = inputs.Assignee
closeReasonTa := textarea.New()
closeReasonTa.Placeholder = "Enter closing reason..."
closeReasonTa.SetWidth(56)
closeReasonTa.SetHeight(4)
m.closeReasonInput = closeReasonTa
if selected := m.todoList.SelectedItem(); selected.ID != "" {
m.issueDetail.SetIssue(selected.Issue)
} else if selected := m.inProgList.SelectedItem(); selected.ID != "" {
@@ -160,7 +170,9 @@ func (m *Model) Init() tea.Cmd {
// IsInModal returns true when a modal (edit, create, delete confirm, choose status/priority/type) is active.
func (m *Model) IsInModal() bool {
return m.editingTitle || m.creatingIssue || m.editingDescription ||
m.choosingStatus || m.choosingPriority || m.confirmingDelete || m.choosingType || m.editingAssignee
m.choosingStatus || m.choosingPriority || m.confirmingDelete ||
m.choosingType || m.editingAssignee ||
m.choosingCloseReason || m.closingOtherReason
}
func (m *Model) IsFocusedOnList() bool {

View File

@@ -262,7 +262,9 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
issueID := m.statusIssueID
m.choosingStatus = false
m.statusIssueID = ""
return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusClosed))
m.choosingCloseReason = true
m.closeReasonIssueID = issueID
return m, nil
case "esc":
m.choosingStatus = false
m.statusIssueID = ""
@@ -270,6 +272,56 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if m.choosingCloseReason {
var reason string
switch msg.String() {
case "d":
reason = "Done"
case "u":
reason = "Duplicate issue"
case "w":
reason = "Won't fix"
case "o":
reason = "Obsolete"
case "h":
m.choosingCloseReason = false
m.closingOtherReason = true
m.closeReasonInput.SetValue("")
return m, nil
case "esc":
m.choosingCloseReason = false
m.closeReasonIssueID = ""
return m, nil
}
if reason != "" {
issueID := m.closeReasonIssueID
m.choosingCloseReason = false
m.closeReasonIssueID = ""
return m, issues.CloseIssueCmd(m.app, issueID, reason)
}
}
if m.closingOtherReason {
switch msg.String() {
case "enter":
reason := m.closeReasonInput.Value()
if reason != "" {
issueID := m.closeReasonIssueID
m.closingOtherReason = false
m.closeReasonIssueID = ""
return m, issues.CloseIssueCmd(m.app, issueID, reason)
}
case "esc":
m.closingOtherReason = false
m.closeReasonIssueID = ""
return m, nil
}
var cmd tea.Cmd
m.closeReasonInput, cmd = m.closeReasonInput.Update(msg)
return m, cmd
}
if m.choosingPriority {
switch msg.String() {
case "0", "1", "2", "3", "4":

View File

@@ -76,6 +76,35 @@ func (m *Model) View() string {
mainView = lipgloss.JoinVertical(lipgloss.Left, header, content, spacer, footer)
}
if m.choosingCloseReason {
reasonContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Choose closing reason for "+m.closeReasonIssueID+":"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("d = Done u = Duplicate issue w = Won't fix o = Obsolete h = Other"),
lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"),
)
reasonBoxWidth := min(70, m.width-4)
reasonBox := styles.ContainerStyle.
Width(reasonBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(reasonContent)
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, reasonBox)
}
if m.closingOtherReason {
editBoxWidth := min(60, m.width-4)
m.closeReasonInput.SetWidth(editBoxWidth - 2)
m.closeReasonInput.SetHeight(4)
editContent := lipgloss.JoinVertical(lipgloss.Left,
styles.LabelStyle.Render("Enter closing reason for "+m.closeReasonIssueID+" (Enter to save, Esc to cancel):"),
m.closeReasonInput.View(),
)
editBox := styles.ContainerStyle.
Width(editBoxWidth).
BorderForeground(styles.PrimaryBorder).
Render(editContent)
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox)
}
return components.RenderModals(
m.width,
m.height,