listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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:
MListless/Models/TaskStore.swift | 9+++++++++
MTests/Unit/TaskStoreCompletionTests.swift | 41+++++++++++++++++++++++++++++++++++++++++
MTests/Unit/TaskStoreEdgeCaseTests.swift | 21+++++++++++++++++++++
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) + } }