M4: percent-complete, conflict-safe saves & subtask reparent
All checks were successful
CI / ci (push) Successful in 6m49s
All checks were successful
CI / ci (push) Successful in 6m49s
M3 detail/edit polish: - "Progress" slider on the edit form writes Tasks.PERCENT_COMPLETE (clamped 0-100, 5% detents); status stays owned by the complete toggle. - Conflict-safe saves: updateTask re-checks the provider's last_modified against the value captured when the form loaded and throws TaskConflictException; the editor offers overwrite-or-cancel instead of clobbering an external change. M4 subtasks (UI): - Reparent: a "Parent task" picker files a task under any top-level task in its list (or "None" to promote it); candidates stay top-level to keep nesting one level deep. Switching list clears the now-invalid parent. - Tapping a subtask opens its own detail (a new TaskDetail entry, own VM). Tests: percent clamping, parent-id write, populatedFields reveal logic. Docs: ROADMAP M2/M3/M4 reconciled to the codebase; CHANGELOG [Unreleased]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,50 @@ class TaskWriteMapperTest {
|
||||
assertThat(values[Tasks.TZ]).isEqualTo("Europe/Berlin")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `percent complete is written and clamped to 0-100`() {
|
||||
val values = TaskWriteMapper.taskValues(
|
||||
TaskForm(title = "x", listId = 1L, percentComplete = 140),
|
||||
tzId = "UTC",
|
||||
)
|
||||
assertThat(values[Tasks.PERCENT_COMPLETE]).isEqualTo(100)
|
||||
|
||||
val unset = TaskWriteMapper.taskValues(TaskForm(title = "x", listId = 1L), tzId = "UTC")
|
||||
assertThat(unset[Tasks.PERCENT_COMPLETE]).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progress keeps completion in sync both ways`() {
|
||||
// 100% completes the task (timestamp left to the provider / existing value).
|
||||
val done = TaskWriteMapper.taskValues(
|
||||
TaskForm(title = "x", listId = 1L, percentComplete = 100),
|
||||
tzId = "UTC",
|
||||
)
|
||||
assertThat(done[Tasks.STATUS]).isEqualTo(TasksContract.STATUS_COMPLETED)
|
||||
assertThat(done.containsKey(Tasks.COMPLETED)).isFalse()
|
||||
|
||||
// Below 100% reopens it and clears the completion timestamp.
|
||||
val reopened = TaskWriteMapper.taskValues(
|
||||
TaskForm(title = "x", listId = 1L, percentComplete = 75),
|
||||
tzId = "UTC",
|
||||
)
|
||||
assertThat(reopened[Tasks.STATUS]).isEqualTo(TasksContract.STATUS_IN_PROCESS)
|
||||
assertThat(reopened[Tasks.COMPLETED]).isNull()
|
||||
|
||||
// No percent in the form ⇒ status is left to the complete toggle.
|
||||
val untouched = TaskWriteMapper.taskValues(TaskForm(title = "x", listId = 1L), tzId = "UTC")
|
||||
assertThat(untouched.containsKey(Tasks.STATUS)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parent id is written so a task can be filed under another`() {
|
||||
val values = TaskWriteMapper.taskValues(
|
||||
TaskForm(title = "x", listId = 1L, parentId = 7L),
|
||||
tzId = "UTC",
|
||||
)
|
||||
assertThat(values[Tasks.PARENT_ID]).isEqualTo(7L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all-day task clears the timezone`() {
|
||||
val form = TaskForm(
|
||||
|
||||
@@ -35,4 +35,13 @@ class TaskFormTest {
|
||||
val errors = TaskForm(title = "x", listId = 1, reminderMinutesBeforeDue = 10).validate()
|
||||
assertThat(errors).contains(TaskFormError.REMINDER_WITHOUT_DUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progress and parent auto-reveal only when they carry a value`() {
|
||||
assertThat(TaskForm(title = "x", listId = 1).populatedFields()).isEmpty()
|
||||
assertThat(TaskForm(title = "x", listId = 1, percentComplete = 0, parentId = 0).populatedFields())
|
||||
.isEmpty()
|
||||
assertThat(TaskForm(title = "x", listId = 1, percentComplete = 30, parentId = 9).populatedFields())
|
||||
.containsExactly(TaskFormField.Progress, TaskFormField.Parent)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user