commit f4be9944f86dcadc865e0bbabe46af46deef2460
parent 04939b02c703e7f8d94e4e6ed30951020088d1fe
Author: Michael Camilleri <[email protected]>
Date: Wed, 18 Feb 2026 21:45:29 +0900
Improve sort ordering on uncompletion
Co-Authored-By: Codex GPT 5.3 <[email protected]>
Diffstat:
3 files changed, 71 insertions(+), 0 deletions(-)
diff --git a/Listless/Models/TaskStore.swift b/Listless/Models/TaskStore.swift
@@ -62,6 +62,15 @@ final class TaskStore {
func uncomplete(taskID: UUID) {
guard let task = findTask(id: taskID) else { return }
+ let restoredSortOrder = task.sortOrder
+ let activeTasks = fetchTasks().filter { !$0.isCompleted && $0.id != task.id }
+ let hasSortOrderConflict = activeTasks.contains { $0.sortOrder == restoredSortOrder }
+
+ if hasSortOrderConflict {
+ let maxSortOrder = activeTasks.map(\.sortOrder).max() ?? -1000
+ task.sortOrder = maxSortOrder + 1000
+ }
+
task.isCompleted = false
save()
}
diff --git a/Tests/Unit/TaskStoreCompletionTests.swift b/Tests/Unit/TaskStoreCompletionTests.swift
@@ -145,4 +145,45 @@ struct TaskStoreCompletionTests {
#expect(tasks.allSatisfy { $0.isCompleted })
#expect(tasks.count == 5)
}
+
+ @Test("Uncomplete restores previous sortOrder when no active conflict")
+ func uncompleteRestoresPreviousSortOrderWhenNoConflict() async throws {
+ let (store, taskIDs) = makeTestStoreWithTasks(count: 3)
+ let taskToRestoreID = taskIDs[1]
+
+ let originalSortOrder = store.fetchTasks().first { $0.id == taskToRestoreID }?.sortOrder
+ #expect(originalSortOrder != nil)
+
+ store.complete(taskID: taskToRestoreID)
+ store.uncomplete(taskID: taskToRestoreID)
+
+ let activeTasks = store.fetchTasks().filter { !$0.isCompleted }
+ let restoredTask = activeTasks.first { $0.id == taskToRestoreID }
+
+ #expect(restoredTask != nil)
+ #expect(restoredTask?.sortOrder == originalSortOrder)
+ #expect(activeTasks.count == 3)
+ }
+
+ @Test("Uncomplete appends task when restored sortOrder conflicts with active task")
+ func uncompleteAppendsWhenRestoredSortOrderConflicts() async throws {
+ let store = makeTestStore()
+ let activeTask = store.createTask(title: "Active task")
+ let completedTask = store.createTask(title: "Completed task")
+
+ store.complete(taskID: completedTask.id)
+ store.moveTask(taskID: activeTask.id, toIndex: 0)
+ completedTask.sortOrder = activeTask.sortOrder
+ store.save()
+
+ store.uncomplete(taskID: completedTask.id)
+
+ let activeTasks = store.fetchTasks().filter { !$0.isCompleted }
+ .sorted { $0.sortOrder < $1.sortOrder }
+ let lastActiveTask = activeTasks.last
+
+ #expect(activeTasks.count == 2)
+ #expect(lastActiveTask?.id == completedTask.id)
+ #expect(lastActiveTask?.sortOrder ?? 0 > activeTask.sortOrder)
+ }
}
diff --git a/Tests/Unit/TaskStoreEdgeCaseTests.swift b/Tests/Unit/TaskStoreEdgeCaseTests.swift
@@ -160,4 +160,25 @@ struct TaskStoreEdgeCaseTests {
#expect(activeTasks.count == 3)
#expect(activeTasks.contains { $0.id == taskIDs[1] })
}
+
+ @Test("Uncomplete legacy sortOrder zero conflict appends to end")
+ func uncompleteLegacyZeroConflictAppendsToEnd() async throws {
+ let store = makeTestStore()
+ let activeTask = store.createTask(title: "Active")
+ let completedTask = store.createTask(title: "Completed")
+
+ activeTask.sortOrder = 0
+ store.complete(taskID: completedTask.id)
+ completedTask.sortOrder = 0
+ store.save()
+
+ store.uncomplete(taskID: completedTask.id)
+
+ let activeTasks = store.fetchTasks().filter { !$0.isCompleted }
+ .sorted { $0.sortOrder < $1.sortOrder }
+ #expect(activeTasks.count == 2)
+ #expect(activeTasks[0].id == activeTask.id)
+ #expect(activeTasks[1].id == completedTask.id)
+ #expect(activeTasks[1].sortOrder > activeTasks[0].sortOrder)
+ }
}