fix(tasks): reset description checklist when a recurring task recurs
When a recurring task with inline checklist items in its description (TipTap `- [ ]` checkboxes) was marked done, Vikunja advanced the due date and reopened it for the next occurrence, but the description's checkboxes stayed CHECKED. The recurrence path in updateDone advances dates/reminders and resets Done, but nothing touched the Description field, so the checked checklist HTML carried over verbatim. Reset the checklist checked-state on the recurrence transition only, covering the TipTap form (`data-checked="true"` and `checked` on `<input type="checkbox">`) and Markdown `- [x]`/`- [X]` defensively. Fixes #2709
This commit is contained in:
parent
f3c6312a9e
commit
409747eca0
|
|
@ -1747,6 +1747,22 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
|
|||
newTask.Done = false
|
||||
}
|
||||
|
||||
var (
|
||||
checklistTiptapCheckedRegex = regexp.MustCompile(`(data-checked=")true(")`)
|
||||
checklistInputCheckedRegex = regexp.MustCompile(`(<input[^>]*type=["']checkbox["'][^>]*?)\s+checked(?:=["'][^"']*["'])?`)
|
||||
checklistMarkdownCheckedRegex = regexp.MustCompile(`(?m)^(\s*[-*]\s+\[)[xX](\])`)
|
||||
)
|
||||
|
||||
// resetDescriptionChecklist unchecks every checklist item in a description without
|
||||
// touching any other content. A recurring task carries its description verbatim into
|
||||
// the next occurrence, so checked checklist items would otherwise stay checked.
|
||||
func resetDescriptionChecklist(description string) string {
|
||||
description = checklistTiptapCheckedRegex.ReplaceAllString(description, "${1}false${2}")
|
||||
description = checklistInputCheckedRegex.ReplaceAllString(description, "$1")
|
||||
description = checklistMarkdownCheckedRegex.ReplaceAllString(description, "$1 $2")
|
||||
return description
|
||||
}
|
||||
|
||||
// This helper function updates the reminders, doneAt, start, end and due dates of the *old* task
|
||||
// and saves the new values in the newTask object.
|
||||
// We make a few assumptions here:
|
||||
|
|
@ -1766,6 +1782,11 @@ func updateDone(oldTask *Task, newTask *Task) (updateDoneAt bool) {
|
|||
setTaskDatesDefault(oldTask, newTask)
|
||||
}
|
||||
|
||||
// A recurring task reopens for its next occurrence, so its checklist starts fresh.
|
||||
if oldTask.isRepeating() && !newTask.Done {
|
||||
newTask.Description = resetDescriptionChecklist(newTask.Description)
|
||||
}
|
||||
|
||||
newTask.DoneAt = time.Now()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -986,6 +986,60 @@ func TestUpdateDone(t *testing.T) {
|
|||
assert.False(t, newTask.Done)
|
||||
})
|
||||
})
|
||||
t.Run("reset checklist on recurrence", func(t *testing.T) {
|
||||
const checked = `before<ul data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p>Item</p></li></ul>after`
|
||||
const unchecked = `before<ul data-type="taskList"><li data-checked="false" data-type="taskItem"><label><input type="checkbox"><span></span></label><div><p>Item</p></li></ul>after`
|
||||
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
DueDate: time.Unix(1550000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
Description: checked,
|
||||
}
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.False(t, newTask.Done)
|
||||
assert.True(t, newTask.DueDate.After(oldTask.DueDate))
|
||||
assert.Equal(t, unchecked, newTask.Description)
|
||||
})
|
||||
t.Run("reset markdown checklist on recurrence", func(t *testing.T) {
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
DueDate: time.Unix(1550000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
Description: "- [x] one\n- [X] two\n- [ ] three\nnot a [x] checklist",
|
||||
}
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.Equal(t, "- [ ] one\n- [ ] two\n- [ ] three\nnot a [x] checklist", newTask.Description)
|
||||
})
|
||||
t.Run("non-recurring description untouched", func(t *testing.T) {
|
||||
const checked = `before<ul data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p>Item</p></li></ul>after`
|
||||
|
||||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 0,
|
||||
RepeatMode: TaskRepeatModeDefault,
|
||||
DueDate: time.Unix(1550000000, 0),
|
||||
}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
Description: checked,
|
||||
}
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.True(t, newTask.Done)
|
||||
assert.Equal(t, checked, newTask.Description)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue