diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt index 6dcbaad..4319c19 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt @@ -244,13 +244,34 @@ class NoteEditorViewModel( } } + /** + * πŸ†• v1.8.1 (IMPL_15): FΓΌgt ein neues Item nach dem angegebenen Item ein. + * + * Guard: Bei MANUAL/UNCHECKED_FIRST wird sichergestellt, dass das neue (unchecked) + * Item nicht innerhalb der checked-Sektion eingefΓΌgt wird. Falls das Trigger-Item + * checked ist, wird stattdessen vor dem ersten checked Item eingefΓΌgt. + */ fun addChecklistItemAfter(afterItemId: String): String { val newItem = ChecklistItemState.createEmpty(0) _checklistItems.update { items -> val index = items.indexOfFirst { it.id == afterItemId } if (index >= 0) { + val currentSort = _lastChecklistSortOption.value + val hasSeparator = currentSort == ChecklistSortOption.MANUAL || + currentSort == ChecklistSortOption.UNCHECKED_FIRST + + // πŸ†• v1.8.1 (IMPL_15): Wenn das Trigger-Item checked ist und ein Separator + // existiert, darf das neue unchecked Item nicht in die checked-Sektion. + // β†’ Stattdessen vor dem ersten checked Item einfΓΌgen. + val effectiveIndex = if (hasSeparator && items[index].isChecked) { + val firstCheckedIndex = items.indexOfFirst { it.isChecked } + if (firstCheckedIndex >= 0) firstCheckedIndex else index + 1 + } else { + index + 1 + } + val newList = items.toMutableList() - newList.add(index + 1, newItem) + newList.add(effectiveIndex, newItem) // Update order values newList.mapIndexed { i, item -> item.copy(order = i) } } else { @@ -259,12 +280,46 @@ class NoteEditorViewModel( } return newItem.id } - + + /** + * πŸ†• v1.8.1 (IMPL_15): FΓΌgt ein neues Item an der semantisch korrekten Position ein. + * + * Bei MANUAL/UNCHECKED_FIRST: Vor dem ersten checked Item (= direkt ΓΌber dem Separator). + * Bei allen anderen Modi: Am Ende der Liste (kein Separator sichtbar). + * + * Verhindert, dass checked Items ΓΌber den Separator springen oder das neue Item + * unter dem Separator erscheint. + */ fun addChecklistItemAtEnd(): String { - val newItem = ChecklistItemState.createEmpty(_checklistItems.value.size) - _checklistItems.update { items -> items + newItem } + val newItem = ChecklistItemState.createEmpty(0) + _checklistItems.update { items -> + val insertIndex = calculateInsertIndexForNewItem(items) + val newList = items.toMutableList() + newList.add(insertIndex, newItem) + newList.mapIndexed { i, item -> item.copy(order = i) } + } return newItem.id } + + /** + * πŸ†• v1.8.1 (IMPL_15): Berechnet die korrekte Insert-Position fΓΌr ein neues unchecked Item. + * + * - MANUAL / UNCHECKED_FIRST: Vor dem ersten checked Item (direkt ΓΌber dem Separator) + * - Alle anderen Modi: Am Ende der Liste (kein Separator, kein visuelles Problem) + * + * Falls keine checked Items existieren, wird am Ende eingefΓΌgt. + */ + private fun calculateInsertIndexForNewItem(items: List): Int { + val currentSort = _lastChecklistSortOption.value + return when (currentSort) { + ChecklistSortOption.MANUAL, + ChecklistSortOption.UNCHECKED_FIRST -> { + val firstCheckedIndex = items.indexOfFirst { it.isChecked } + if (firstCheckedIndex >= 0) firstCheckedIndex else items.size + } + else -> items.size + } + } fun deleteChecklistItem(itemId: String) { _checklistItems.update { items -> diff --git a/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt b/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt index 768586f..9f1699e 100644 --- a/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt +++ b/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt @@ -1,5 +1,6 @@ package dev.dettmer.simplenotes.ui.editor +import dev.dettmer.simplenotes.models.ChecklistSortOption import org.junit.Assert.* import org.junit.Test @@ -174,4 +175,204 @@ class ChecklistSortingTest { assertEquals(1, sorted[1].order) assertEquals(2, sorted[2].order) } + + // ═══════════════════════════════════════════════════════════════════════ + // πŸ†• v1.8.1 (IMPL_15): Tests fΓΌr Add-Item Insert-Position + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Simulates calculateInsertIndexForNewItem() from NoteEditorViewModel. + * Tests the insert position logic for new unchecked items. + */ + private fun calculateInsertIndexForNewItem( + items: List, + sortOption: ChecklistSortOption + ): Int { + return when (sortOption) { + ChecklistSortOption.MANUAL, + ChecklistSortOption.UNCHECKED_FIRST -> { + val firstCheckedIndex = items.indexOfFirst { it.isChecked } + if (firstCheckedIndex >= 0) firstCheckedIndex else items.size + } + else -> items.size + } + } + + /** + * Simulates the full addChecklistItemAtEnd() logic: + * 1. Calculate insert index + * 2. Insert new item + * 3. Reassign order values + */ + private fun simulateAddItemAtEnd( + items: List, + sortOption: ChecklistSortOption + ): List { + val newItem = ChecklistItemState(id = "new", text = "", isChecked = false, order = 0) + val insertIndex = calculateInsertIndexForNewItem(items, sortOption) + val newList = items.toMutableList() + newList.add(insertIndex, newItem) + return newList.mapIndexed { i, item -> item.copy(order = i) } + } + + @Test + fun `IMPL_15 - add item at end inserts before separator in MANUAL mode`() { + // Ausgangslage: 2 unchecked, 1 checked (sortiert) + val items = listOf( + item("a", checked = false, order = 0), + item("b", checked = false, order = 1), + item("c", checked = true, order = 2) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL) + + // Neues Item muss an Index 2 stehen (vor dem checked Item) + assertEquals(4, result.size) + assertEquals("a", result[0].id) + assertEquals("b", result[1].id) + assertEquals("new", result[2].id) // ← Neues Item VOR Separator + assertFalse(result[2].isChecked) + assertEquals("c", result[3].id) // ← Checked Item bleibt UNTER Separator + assertTrue(result[3].isChecked) + } + + @Test + fun `IMPL_15 - add item at end inserts before separator in UNCHECKED_FIRST mode`() { + val items = listOf( + item("a", checked = false, order = 0), + item("b", checked = true, order = 1), + item("c", checked = true, order = 2) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.UNCHECKED_FIRST) + + assertEquals(4, result.size) + assertEquals("a", result[0].id) + assertEquals("new", result[1].id) // ← Neues Item direkt nach letztem unchecked + assertFalse(result[1].isChecked) + assertEquals("b", result[2].id) + assertEquals("c", result[3].id) + } + + @Test + fun `IMPL_15 - add item at end appends at end in CHECKED_FIRST mode`() { + val items = listOf( + item("a", checked = true, order = 0), + item("b", checked = false, order = 1) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.CHECKED_FIRST) + + assertEquals(3, result.size) + assertEquals("a", result[0].id) + assertEquals("b", result[1].id) + assertEquals("new", result[2].id) // ← Am Ende (kein Separator) + } + + @Test + fun `IMPL_15 - add item at end appends at end in ALPHABETICAL_ASC mode`() { + val items = listOf( + item("a", checked = false, order = 0), + item("b", checked = true, order = 1) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.ALPHABETICAL_ASC) + + assertEquals(3, result.size) + assertEquals("new", result[2].id) // ← Am Ende + } + + @Test + fun `IMPL_15 - add item at end appends at end in ALPHABETICAL_DESC mode`() { + val items = listOf( + item("a", checked = true, order = 0), + item("b", checked = false, order = 1) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.ALPHABETICAL_DESC) + + assertEquals(3, result.size) + assertEquals("new", result[2].id) // ← Am Ende + } + + @Test + fun `IMPL_15 - add item with no checked items appends at end`() { + val items = listOf( + item("a", checked = false, order = 0), + item("b", checked = false, order = 1) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL) + + assertEquals(3, result.size) + assertEquals("new", result[2].id) // Kein checked Item β†’ ans Ende + } + + @Test + fun `IMPL_15 - add item with all checked items inserts at position 0`() { + val items = listOf( + item("a", checked = true, order = 0), + item("b", checked = true, order = 1) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL) + + assertEquals(3, result.size) + assertEquals("new", result[0].id) // ← Ganz oben (vor allen checked Items) + assertFalse(result[0].isChecked) + assertEquals("a", result[1].id) + assertEquals("b", result[2].id) + } + + @Test + fun `IMPL_15 - add item to empty list in MANUAL mode`() { + val items = emptyList() + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL) + + assertEquals(1, result.size) + assertEquals("new", result[0].id) + assertEquals(0, result[0].order) + } + + @Test + fun `IMPL_15 - order values are sequential after add item`() { + val items = listOf( + item("a", checked = false, order = 0), + item("b", checked = false, order = 1), + item("c", checked = true, order = 2) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL) + + result.forEachIndexed { index, item -> + assertEquals("Order at index $index should be $index", index, item.order) + } + } + + @Test + fun `IMPL_15 - existing items do not change position after add item`() { + // Kernforderung: Kein Item darf sich verschieben + val items = listOf( + item("cashews", checked = false, order = 0), + item("noodles", checked = false, order = 1), + item("coffee", checked = true, order = 2) + ) + + val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL) + + // Relative Reihenfolge der bestehenden Items prΓΌfen + val existingIds = result.filter { it.id != "new" }.map { it.id } + assertEquals(listOf("cashews", "noodles", "coffee"), existingIds) + + // Cashews und Noodles mΓΌssen VOR dem neuen Item sein + val cashewsIdx = result.indexOfFirst { it.id == "cashews" } + val noodlesIdx = result.indexOfFirst { it.id == "noodles" } + val newIdx = result.indexOfFirst { it.id == "new" } + val coffeeIdx = result.indexOfFirst { it.id == "coffee" } + + assertTrue("Cashews before new", cashewsIdx < newIdx) + assertTrue("Noodles before new", noodlesIdx < newIdx) + assertTrue("New before Coffee", newIdx < coffeeIdx) + } }