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) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
m.cycleStateBackward(items[m.cursor])
|
m.cycleStateBackward(items[m.cursor])
|
||||||
// Auto clock out when changing to DONE
|
// 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()
|
items[m.cursor].ClockOut()
|
||||||
}
|
}
|
||||||
m.setStatus("State changed")
|
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) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
m.cycleStateForward(items[m.cursor])
|
m.cycleStateForward(items[m.cursor])
|
||||||
// Auto clock out when changing to last state (typically DONE)
|
// Auto clock out when changing to last state (typically DONE)
|
||||||
stateNames := m.config.GetStateNames()
|
if m.config.IsDoneState(string(items[m.cursor].State)) && items[m.cursor].IsClockedIn() {
|
||||||
if len(stateNames) > 0 && string(items[m.cursor].State) == stateNames[len(stateNames)-1] && items[m.cursor].IsClockedIn() {
|
|
||||||
items[m.cursor].ClockOut()
|
items[m.cursor].ClockOut()
|
||||||
}
|
}
|
||||||
m.setStatus("State changed")
|
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) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
m.cycleStateForward(items[m.cursor])
|
m.cycleStateForward(items[m.cursor])
|
||||||
// Auto clock out when changing to last state (typically DONE)
|
// Auto clock out when changing to last state (typically DONE)
|
||||||
stateNames := m.config.GetStateNames()
|
if m.config.IsDoneState(string(items[m.cursor].State)) && items[m.cursor].IsClockedIn() {
|
||||||
if len(stateNames) > 0 && string(items[m.cursor].State) == stateNames[len(stateNames)-1] && items[m.cursor].IsClockedIn() {
|
|
||||||
items[m.cursor].ClockOut()
|
items[m.cursor].ClockOut()
|
||||||
}
|
}
|
||||||
m.setStatus("State changed")
|
m.setStatus("State changed")
|
||||||
|
|
@ -870,6 +868,30 @@ func (m uiModel) updateSetEffort(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, 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) {
|
func (m *uiModel) cycleStateForward(item *model.Item) {
|
||||||
stateNames := m.config.GetStateNames()
|
stateNames := m.config.GetStateNames()
|
||||||
if len(stateNames) == 0 {
|
if len(stateNames) == 0 {
|
||||||
|
|
@ -879,7 +901,6 @@ func (m *uiModel) cycleStateForward(item *model.Item) {
|
||||||
// Find current state index
|
// Find current state index
|
||||||
currentIndex := -1
|
currentIndex := -1
|
||||||
currentState := string(item.State)
|
currentState := string(item.State)
|
||||||
lastStateIndex := len(stateNames) - 1
|
|
||||||
|
|
||||||
// Handle empty state
|
// Handle empty state
|
||||||
if currentState == "" {
|
if currentState == "" {
|
||||||
|
|
@ -911,34 +932,8 @@ func (m *uiModel) cycleStateForward(item *model.Item) {
|
||||||
// Update the item state
|
// Update the item state
|
||||||
item.State = model.TodoState(newState)
|
item.State = model.TodoState(newState)
|
||||||
|
|
||||||
// Manage CLOSED timestamp
|
// Manage CLOSED timestamp based on the configured done-state set.
|
||||||
wasInDoneState := (oldState == stateNames[lastStateIndex])
|
applyClosedTransition(item, m.config.IsDoneState(oldState), m.config.IsDoneState(newState), time.Now())
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *uiModel) cycleStateBackward(item *model.Item) {
|
func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||||
|
|
@ -950,7 +945,6 @@ func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||||
// Find current state index
|
// Find current state index
|
||||||
currentIndex := -1
|
currentIndex := -1
|
||||||
currentState := string(item.State)
|
currentState := string(item.State)
|
||||||
lastStateIndex := len(stateNames) - 1
|
|
||||||
|
|
||||||
// Handle empty state
|
// Handle empty state
|
||||||
if currentState == "" {
|
if currentState == "" {
|
||||||
|
|
@ -980,34 +974,8 @@ func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||||
// Update the item state
|
// Update the item state
|
||||||
item.State = model.TodoState(newState)
|
item.State = model.TodoState(newState)
|
||||||
|
|
||||||
// Manage CLOSED timestamp
|
// Manage CLOSED timestamp based on the configured done-state set.
|
||||||
wasInDoneState := (oldState == stateNames[lastStateIndex])
|
applyClosedTransition(item, m.config.IsDoneState(oldState), m.config.IsDoneState(newState), time.Now())
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *uiModel) deleteItem(item *model.Item) {
|
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