mirror of
https://github.com/RWejlgaard/org.git
synced 2026-09-02 01:15:35 +00:00
feat: stamp CLOSED for any configured done state
This commit is contained in:
parent
5b10f4e229
commit
517b60ecd9
2 changed files with 82 additions and 63 deletions
|
|
@ -103,7 +103,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if len(items) > 0 && m.cursor < len(items) {
|
||||
m.cycleStateBackward(items[m.cursor])
|
||||
// Auto clock out when changing to DONE
|
||||
if items[m.cursor].State == model.StateDONE && items[m.cursor].IsClockedIn() {
|
||||
if m.config.IsDoneState(string(items[m.cursor].State)) && items[m.cursor].IsClockedIn() {
|
||||
items[m.cursor].ClockOut()
|
||||
}
|
||||
m.setStatus("State changed")
|
||||
|
|
@ -114,8 +114,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if len(items) > 0 && m.cursor < len(items) {
|
||||
m.cycleStateForward(items[m.cursor])
|
||||
// Auto clock out when changing to last state (typically DONE)
|
||||
stateNames := m.config.GetStateNames()
|
||||
if len(stateNames) > 0 && string(items[m.cursor].State) == stateNames[len(stateNames)-1] && items[m.cursor].IsClockedIn() {
|
||||
if m.config.IsDoneState(string(items[m.cursor].State)) && items[m.cursor].IsClockedIn() {
|
||||
items[m.cursor].ClockOut()
|
||||
}
|
||||
m.setStatus("State changed")
|
||||
|
|
@ -138,8 +137,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
if len(items) > 0 && m.cursor < len(items) {
|
||||
m.cycleStateForward(items[m.cursor])
|
||||
// Auto clock out when changing to last state (typically DONE)
|
||||
stateNames := m.config.GetStateNames()
|
||||
if len(stateNames) > 0 && string(items[m.cursor].State) == stateNames[len(stateNames)-1] && items[m.cursor].IsClockedIn() {
|
||||
if m.config.IsDoneState(string(items[m.cursor].State)) && items[m.cursor].IsClockedIn() {
|
||||
items[m.cursor].ClockOut()
|
||||
}
|
||||
m.setStatus("State changed")
|
||||
|
|
@ -870,6 +868,30 @@ func (m uiModel) updateSetEffort(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, cmd
|
||||
}
|
||||
|
||||
// stripClosedNotes removes any note line that is a CLOSED: planning entry.
|
||||
func stripClosedNotes(notes []string) []string {
|
||||
var filtered []string
|
||||
for _, note := range notes {
|
||||
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||
filtered = append(filtered, note)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// applyClosedTransition stamps or clears the CLOSED timestamp based on whether
|
||||
// the item entered or left the set of done states.
|
||||
func applyClosedTransition(item *model.Item, wasDone, isDone bool, now time.Time) {
|
||||
switch {
|
||||
case isDone && !wasDone:
|
||||
item.Closed = &now
|
||||
item.Notes = stripClosedNotes(item.Notes)
|
||||
case wasDone && !isDone:
|
||||
item.Closed = nil
|
||||
item.Notes = stripClosedNotes(item.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *uiModel) cycleStateForward(item *model.Item) {
|
||||
stateNames := m.config.GetStateNames()
|
||||
if len(stateNames) == 0 {
|
||||
|
|
@ -879,7 +901,6 @@ func (m *uiModel) cycleStateForward(item *model.Item) {
|
|||
// Find current state index
|
||||
currentIndex := -1
|
||||
currentState := string(item.State)
|
||||
lastStateIndex := len(stateNames) - 1
|
||||
|
||||
// Handle empty state
|
||||
if currentState == "" {
|
||||
|
|
@ -911,34 +932,8 @@ func (m *uiModel) cycleStateForward(item *model.Item) {
|
|||
// Update the item state
|
||||
item.State = model.TodoState(newState)
|
||||
|
||||
// Manage CLOSED timestamp
|
||||
wasInDoneState := (oldState == stateNames[lastStateIndex])
|
||||
isInDoneState := (newState == stateNames[lastStateIndex])
|
||||
|
||||
if isInDoneState && !wasInDoneState {
|
||||
// Moving TO done state - add CLOSED timestamp
|
||||
now := time.Now()
|
||||
item.Closed = &now
|
||||
// Remove any existing CLOSED line from notes
|
||||
var filteredNotes []string
|
||||
for _, note := range item.Notes {
|
||||
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||
filteredNotes = append(filteredNotes, note)
|
||||
}
|
||||
}
|
||||
item.Notes = filteredNotes
|
||||
} else if wasInDoneState && !isInDoneState {
|
||||
// Moving FROM done state - remove CLOSED timestamp
|
||||
item.Closed = nil
|
||||
// Remove any existing CLOSED line from notes
|
||||
var filteredNotes []string
|
||||
for _, note := range item.Notes {
|
||||
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||
filteredNotes = append(filteredNotes, note)
|
||||
}
|
||||
}
|
||||
item.Notes = filteredNotes
|
||||
}
|
||||
// Manage CLOSED timestamp based on the configured done-state set.
|
||||
applyClosedTransition(item, m.config.IsDoneState(oldState), m.config.IsDoneState(newState), time.Now())
|
||||
}
|
||||
|
||||
func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||
|
|
@ -950,7 +945,6 @@ func (m *uiModel) cycleStateBackward(item *model.Item) {
|
|||
// Find current state index
|
||||
currentIndex := -1
|
||||
currentState := string(item.State)
|
||||
lastStateIndex := len(stateNames) - 1
|
||||
|
||||
// Handle empty state
|
||||
if currentState == "" {
|
||||
|
|
@ -980,34 +974,8 @@ func (m *uiModel) cycleStateBackward(item *model.Item) {
|
|||
// Update the item state
|
||||
item.State = model.TodoState(newState)
|
||||
|
||||
// Manage CLOSED timestamp
|
||||
wasInDoneState := (oldState == stateNames[lastStateIndex])
|
||||
isInDoneState := (newState == stateNames[lastStateIndex])
|
||||
|
||||
if isInDoneState && !wasInDoneState {
|
||||
// Moving TO done state - add CLOSED timestamp
|
||||
now := time.Now()
|
||||
item.Closed = &now
|
||||
// Remove any existing CLOSED line from notes
|
||||
var filteredNotes []string
|
||||
for _, note := range item.Notes {
|
||||
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||
filteredNotes = append(filteredNotes, note)
|
||||
}
|
||||
}
|
||||
item.Notes = filteredNotes
|
||||
} else if wasInDoneState && !isInDoneState {
|
||||
// Moving FROM done state - remove CLOSED timestamp
|
||||
item.Closed = nil
|
||||
// Remove any existing CLOSED line from notes
|
||||
var filteredNotes []string
|
||||
for _, note := range item.Notes {
|
||||
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||
filteredNotes = append(filteredNotes, note)
|
||||
}
|
||||
}
|
||||
item.Notes = filteredNotes
|
||||
}
|
||||
// Manage CLOSED timestamp based on the configured done-state set.
|
||||
applyClosedTransition(item, m.config.IsDoneState(oldState), m.config.IsDoneState(newState), time.Now())
|
||||
}
|
||||
|
||||
func (m *uiModel) deleteItem(item *model.Item) {
|
||||
|
|
|
|||
51
internal/ui/modes_test.go
Normal file
51
internal/ui/modes_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rwejlgaard/org/internal/model"
|
||||
)
|
||||
|
||||
func TestApplyClosedTransitionEntersDone(t *testing.T) {
|
||||
now := time.Date(2026, 7, 21, 14, 30, 0, 0, time.UTC)
|
||||
item := &model.Item{Notes: []string{"a note", "CLOSED: [old stamp]"}}
|
||||
|
||||
applyClosedTransition(item, false, true, now)
|
||||
|
||||
if item.Closed == nil || !item.Closed.Equal(now) {
|
||||
t.Fatalf("expected Closed set to now, got %v", item.Closed)
|
||||
}
|
||||
if len(item.Notes) != 1 || item.Notes[0] != "a note" {
|
||||
t.Fatalf("expected stale CLOSED note stripped, got %v", item.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClosedTransitionLeavesDone(t *testing.T) {
|
||||
now := time.Now()
|
||||
stamp := now
|
||||
item := &model.Item{Closed: &stamp, Notes: []string{" CLOSED: [x]", "keep"}}
|
||||
|
||||
applyClosedTransition(item, true, false, now)
|
||||
|
||||
if item.Closed != nil {
|
||||
t.Fatalf("expected Closed cleared, got %v", item.Closed)
|
||||
}
|
||||
if len(item.Notes) != 1 || item.Notes[0] != "keep" {
|
||||
t.Fatalf("expected CLOSED note stripped, got %v", item.Notes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClosedTransitionDoneToDoneUnchanged(t *testing.T) {
|
||||
orig := time.Date(2026, 1, 1, 9, 0, 0, 0, time.UTC)
|
||||
item := &model.Item{Closed: &orig, Notes: []string{"keep"}}
|
||||
|
||||
applyClosedTransition(item, true, true, time.Now())
|
||||
|
||||
if item.Closed == nil || !item.Closed.Equal(orig) {
|
||||
t.Fatalf("done->done must keep original Closed, got %v", item.Closed)
|
||||
}
|
||||
if len(item.Notes) != 1 || item.Notes[0] != "keep" {
|
||||
t.Fatalf("done->done must not touch notes, got %v", item.Notes)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue