diff --git a/AGENTS.md b/AGENTS.md index bd3eecb..fbe5909 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,7 +97,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel - Pressing "Delete" in the modal dialog MUST show a second confirmation modal asking if the user is sure, with buttons for "Cancel" and "I'm sure". - Only on pressing "I'm sure" in the second confirmation modal dialog should a board delete request be sent to the API. - Long-pressing any of the buttons must show a tooltip with the button name. -- Current status: implemented in `BoardDetailActivity` with `ViewPager2` (one list per page), inline list-title edit, card rendering (title/tags/due date locale formatting and expiry color), cross-page card selection, page-scoped select-all, move dialog with list selector, two-step delete confirmation, mutation guards while in progress, and API-backed reload/reconciliation behavior through `BoardDetailViewModel` and `BoardDetailRepository`. Selection action icons use local vector drawables (`ic_select_all_grid_24`, `ic_move_cards_horizontal_24`, `ic_delete_24`) with day/night resource variants so dark mode uses light icon fills automatically. Startup blocking dialogs are shown for missing board id and missing session. +- Current status: implemented in `BoardDetailActivity` with `ViewPager2` (one list per page), inline list-title edit, card rendering (title/tags/due date locale formatting and expiry color), cross-page card selection, page-scoped select-all, move dialog with list selector, two-step delete confirmation, mutation guards while in progress, and API-backed reload/reconciliation behavior through `BoardDetailViewModel` and `BoardDetailRepository`. Label chip border colors are hydrated from the Kan.bn `Get a label by public ID` endpoint (`colourCode`) and cached in-memory by `BoardDetailRepository` so each label color is fetched only once per app process. Selection action icons use local vector drawables (`ic_select_all_grid_24`, `ic_move_cards_horizontal_24`, `ic_delete_24`) with day/night resource variants so dark mode uses light icon fills automatically. Startup blocking dialogs are shown for missing board id and missing session. **Card detail view** - The view shows the card's title in bold letters. Tapping on the card's title allows editing it. @@ -132,5 +132,5 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel - It is preferable to use standard Android libraries and tools whenever possible. - If a task would be better served by or can only be done with external libraries then it is allowed to use them but you MUST ask the user for permission to add the library to the project first. - The documentation for the Kan.bn API is available here https://docs.kan.bn/api-reference/introduction -- Never commit or push code unless explicitely prompted to do so. +- Never push code unless explicitely prompted to do so. - After every run, update this file as needed to reflect the changes made. diff --git a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardDetailFlowTest.kt b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardDetailFlowTest.kt index 8b06a6b..4fda0df 100644 --- a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardDetailFlowTest.kt +++ b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardDetailFlowTest.kt @@ -25,6 +25,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.color.MaterialColors +import com.google.android.material.chip.Chip import java.text.DateFormat import java.util.ArrayDeque import java.util.Date @@ -148,6 +149,22 @@ class BoardDetailFlowTest { onView(withId(R.id.cardDueDateText)).check(matches(withCurrentTextColor(expectedColor ?: Color.BLACK))) } + @Test + fun invalidTagColorFallsBackToOnSurfaceColor() { + defaultDataSource.currentDetail = detailWithInvalidTagColor() + val scenario = launchBoardDetail() + + var expectedColor: Int? = null + scenario.onActivity { activity -> + expectedColor = MaterialColors.getColor( + activity.findViewById(android.R.id.content), + com.google.android.material.R.attr.colorOnSurface, + ) + } + + onView(withText("Backend")).check(matches(withChipStrokeColor(expectedColor ?: Color.DKGRAY))) + } + @Test fun dueDateUsesSystemLocaleFormatting() { Locale.setDefault(Locale.FRANCE) @@ -571,6 +588,21 @@ class BoardDetailFlowTest { } } + private fun withChipStrokeColor(expectedColor: Int): Matcher { + return object : TypeSafeMatcher() { + override fun describeTo(description: Description) { + description.appendText("with chip stroke color: $expectedColor") + } + + override fun matchesSafely(item: View): Boolean { + if (item !is Chip) { + return false + } + return item.chipStrokeColor?.defaultColor == expectedColor + } + } + } + private fun awaitCondition(timeoutMs: Long = 4_000L, condition: () -> Boolean) { val instrumentation = androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() val start = System.currentTimeMillis() @@ -757,6 +789,25 @@ class BoardDetailFlowTest { ) } + fun detailWithInvalidTagColor(): BoardDetail { + return detailOneList().copy( + lists = listOf( + BoardListDetail( + id = "list-1", + title = "To Do", + cards = listOf( + BoardCardSummary( + id = "card-1", + title = "Card 1", + tags = listOf(BoardTagSummary("tag-1", "Backend", "bad-color")), + dueAtEpochMillis = null, + ), + ), + ), + ), + ) + } + fun detailWithCardTitle(title: String): BoardDetail { return detailOneList().copy( lists = listOf( diff --git a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt index 603ee70..42e5eb1 100644 --- a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt +++ b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt @@ -23,6 +23,7 @@ import org.junit.runner.RunWith import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient +import space.hackenslacker.kanbn4droid.app.auth.LabelDetail import space.hackenslacker.kanbn4droid.app.auth.SessionStore import space.hackenslacker.kanbn4droid.app.boards.BoardSummary import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate @@ -199,5 +200,13 @@ class BoardsFlowTest { boards.removeAll { it.id == boardId } return BoardsApiResult.Success(Unit) } + + override suspend fun getLabelByPublicId( + baseUrl: String, + apiKey: String, + labelId: String, + ): BoardsApiResult { + return BoardsApiResult.Failure("Not needed in boards flow tests") + } } } diff --git a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/LoginFlowTest.kt b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/LoginFlowTest.kt index 67350a9..f3a8cd9 100644 --- a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/LoginFlowTest.kt +++ b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/LoginFlowTest.kt @@ -21,6 +21,7 @@ import space.hackenslacker.kanbn4droid.app.auth.AuthFailureReason import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient +import space.hackenslacker.kanbn4droid.app.auth.LabelDetail import space.hackenslacker.kanbn4droid.app.auth.SessionStore @RunWith(AndroidJUnit4::class) @@ -200,5 +201,13 @@ class LoginFlowTest { private val result: AuthResult, ) : KanbnApiClient { override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = result + + override suspend fun getLabelByPublicId( + baseUrl: String, + apiKey: String, + labelId: String, + ): space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult { + return space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult.Failure("Not needed in login tests") + } } } diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt index ebd8bba..89764c0 100644 --- a/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/auth/KanbnApiClient.kt @@ -69,8 +69,17 @@ interface KanbnApiClient { suspend fun deleteCard(baseUrl: String, apiKey: String, cardId: String): BoardsApiResult { return BoardsApiResult.Failure("Card deletion is not implemented.") } + + suspend fun getLabelByPublicId(baseUrl: String, apiKey: String, labelId: String): BoardsApiResult { + return BoardsApiResult.Failure("Label detail is not implemented.") + } } +data class LabelDetail( + val id: String, + val colorHex: String, +) + class HttpKanbnApiClient : KanbnApiClient { override suspend fun listWorkspaces(baseUrl: String, apiKey: String): BoardsApiResult> { @@ -304,6 +313,29 @@ class HttpKanbnApiClient : KanbnApiClient { } } + override suspend fun getLabelByPublicId( + baseUrl: String, + apiKey: String, + labelId: String, + ): BoardsApiResult { + return withContext(Dispatchers.IO) { + request( + baseUrl = baseUrl, + path = "/api/v1/labels/$labelId", + method = "GET", + apiKey = apiKey, + ) { code, body -> + if (code in 200..299) { + parseLabelDetail(body, labelId) + ?.let { BoardsApiResult.Success(it) } + ?: BoardsApiResult.Failure("Malformed label detail response.") + } else { + BoardsApiResult.Failure(serverMessage(body, code)) + } + } + } + } + private fun request( baseUrl: String, path: String, @@ -504,6 +536,23 @@ class HttpKanbnApiClient : KanbnApiClient { return BoardDetail(id = boardId, title = boardTitle, lists = lists) } + private fun parseLabelDetail(body: String, fallbackId: String): LabelDetail? { + if (body.isBlank()) { + return null + } + + val root = parseJsonObject(body) ?: return null + val data = root["data"] as? Map<*, *> + val label = (data?.get("label") as? Map<*, *>) + ?: data + ?: (root["label"] as? Map<*, *>) + ?: root + + val id = extractId(label).ifBlank { fallbackId } + val colorHex = extractString(label, "colourCode", "colorCode", "colorHex", "color", "hex") + return LabelDetail(id = id, colorHex = colorHex) + } + private fun parseLists(board: Map<*, *>): List { return extractObjectArray(board, "lists", "items", "data").mapNotNull { rawList -> val id = extractId(rawList) diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepository.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepository.kt index 21ee759..a00ad43 100644 --- a/app/src/main/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepository.kt +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepository.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient +import space.hackenslacker.kanbn4droid.app.auth.LabelDetail import space.hackenslacker.kanbn4droid.app.auth.SessionStore import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult @@ -14,6 +15,8 @@ class BoardDetailRepository( private val apiClient: KanbnApiClient, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { + private val labelColorCache = mutableMapOf() + companion object { const val MISSING_SESSION_MESSAGE = "Missing session. Please sign in again." } @@ -33,7 +36,13 @@ class BoardDetailRepository( baseUrl = session.baseUrl, apiKey = session.apiKey, boardId = normalizedBoardId, - ) + ).mapSuccess { detail -> + hydrateLabelColors( + detail = detail, + baseUrl = session.baseUrl, + apiKey = session.apiKey, + ) + } } suspend fun renameList(listId: String, newTitle: String): BoardsApiResult { @@ -192,6 +201,63 @@ class BoardDetailRepository( } } + private suspend fun hydrateLabelColors( + detail: BoardDetail, + baseUrl: String, + apiKey: String, + ): BoardDetail { + val missingColorIds = detail.lists + .asSequence() + .flatMap { list -> list.cards.asSequence() } + .flatMap { card -> card.tags.asSequence() } + .map { it.id.trim() } + .filter { it.isNotBlank() } + .distinct() + .filter { !labelColorCache.containsKey(it) } + .toList() + + missingColorIds.forEach { labelId -> + val colorHex = when (val labelResult = apiClient.getLabelByPublicId(baseUrl, apiKey, labelId)) { + is BoardsApiResult.Success -> normalizeColorHex(labelResult.value) + is BoardsApiResult.Failure -> "" + } + if (colorHex.isNotBlank()) { + labelColorCache[labelId] = colorHex + } + } + + val hydratedLists = detail.lists.map { list -> + list.copy( + cards = list.cards.map { card -> + card.copy( + tags = card.tags.map { tag -> + val cached = labelColorCache[tag.id.trim()].orEmpty() + val merged = if (cached.isNotBlank()) cached else tag.colorHex + if (merged == tag.colorHex) { + tag + } else { + tag.copy(colorHex = merged) + } + }, + ) + }, + ) + } + + return if (hydratedLists == detail.lists) detail else detail.copy(lists = hydratedLists) + } + + private fun normalizeColorHex(label: LabelDetail): String { + return label.colorHex.trim() + } + + private suspend fun BoardsApiResult.mapSuccess(transform: suspend (T) -> R): BoardsApiResult { + return when (this) { + is BoardsApiResult.Success -> BoardsApiResult.Success(transform(this.value)) + is BoardsApiResult.Failure -> BoardsApiResult.Failure(this.message) + } + } + private data class SessionSnapshot( val baseUrl: String, val apiKey: String, diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt index fb80d73..0cb1d89 100644 --- a/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/auth/HttpKanbnApiClientBoardDetailParsingTest.kt @@ -341,6 +341,84 @@ class HttpKanbnApiClientBoardDetailParsingTest { } } + @Test + fun getLabelByPublicIdParsesColourCodeFromWrappedPayload() = runTest { + TestServer().use { server -> + server.register( + path = "/api/v1/labels/label-1", + status = 200, + responseBody = + """ + { + "data": { + "label": { + "public_id": "label-1", + "colourCode": "#A1B2C3" + } + } + } + """.trimIndent(), + ) + + val result = HttpKanbnApiClient().getLabelByPublicId(server.baseUrl, "api-key", "label-1") + + assertTrue(result is BoardsApiResult.Success<*>) + val detail = (result as BoardsApiResult.Success<*>).value as LabelDetail + assertEquals("label-1", detail.id) + assertEquals("#A1B2C3", detail.colorHex) + } + } + + @Test + fun getLabelByPublicIdUsesFallbackIdWhenPayloadHasNoId() = runTest { + TestServer().use { server -> + server.register( + path = "/api/v1/labels/label-fallback", + status = 200, + responseBody = + """ + { + "label": { + "colorCode": "#ABCDEF" + } + } + """.trimIndent(), + ) + + val result = HttpKanbnApiClient().getLabelByPublicId(server.baseUrl, "api-key", "label-fallback") + + assertTrue(result is BoardsApiResult.Success<*>) + val detail = (result as BoardsApiResult.Success<*>).value as LabelDetail + assertEquals("label-fallback", detail.id) + assertEquals("#ABCDEF", detail.colorHex) + } + } + + @Test + fun getLabelByPublicIdFailureUsesServerMessageAndFallback() = runTest { + TestServer().use { server -> + server.register( + path = "/api/v1/labels/label-msg", + status = 404, + responseBody = """{"message":"Label not found"}""", + ) + server.register( + path = "/api/v1/labels/label-fallback", + status = 500, + responseBody = "{}", + ) + + val client = HttpKanbnApiClient() + val withMessage = client.getLabelByPublicId(server.baseUrl, "api", "label-msg") + val fallback = client.getLabelByPublicId(server.baseUrl, "api", "label-fallback") + + assertTrue(withMessage is BoardsApiResult.Failure) + assertEquals("Label not found", (withMessage as BoardsApiResult.Failure).message) + assertTrue(fallback is BoardsApiResult.Failure) + assertEquals("Server error: 500", (fallback as BoardsApiResult.Failure).message) + } + } + private data class CapturedRequest( val method: String, val path: String, diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepositoryTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepositoryTest.kt index b7d1352..9916424 100644 --- a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepositoryTest.kt +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boarddetail/BoardDetailRepositoryTest.kt @@ -7,6 +7,7 @@ import org.junit.Test import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient +import space.hackenslacker.kanbn4droid.app.auth.LabelDetail import space.hackenslacker.kanbn4droid.app.auth.SessionStore import space.hackenslacker.kanbn4droid.app.boards.BoardSummary import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate @@ -99,6 +100,95 @@ class BoardDetailRepositoryTest { assertEquals("Server says no", (result as BoardsApiResult.Failure).message) } + @Test + fun getBoardDetailFetchesLabelColorCodesAndCachesThem() = runTest { + val detailWithTags = BoardDetail( + id = "board-1", + title = "Board", + lists = listOf( + BoardListDetail( + id = "list-1", + title = "To Do", + cards = listOf( + BoardCardSummary( + id = "card-1", + title = "Card 1", + tags = listOf( + BoardTagSummary(id = "label-1", name = "Urgent", colorHex = ""), + BoardTagSummary(id = "label-2", name = "Backend", colorHex = "#000000"), + ), + dueAtEpochMillis = null, + ), + BoardCardSummary( + id = "card-2", + title = "Card 2", + tags = listOf( + BoardTagSummary(id = "label-1", name = "Urgent", colorHex = ""), + ), + dueAtEpochMillis = null, + ), + ), + ), + ), + ) + val apiClient = FakeBoardDetailApiClient().apply { + boardDetailResult = BoardsApiResult.Success(detailWithTags) + labelByIdResults = mapOf( + "label-1" to BoardsApiResult.Success(LabelDetail("label-1", "#112233")), + "label-2" to BoardsApiResult.Success(LabelDetail("label-2", "#445566")), + ) + } + val repository = createRepository(apiClient = apiClient) + + val first = repository.getBoardDetail("board-1") + val second = repository.getBoardDetail("board-1") + + assertTrue(first is BoardsApiResult.Success<*>) + assertTrue(second is BoardsApiResult.Success<*>) + val firstDetail = (first as BoardsApiResult.Success).value + val secondDetail = (second as BoardsApiResult.Success).value + assertEquals("#112233", firstDetail.lists[0].cards[0].tags[0].colorHex) + assertEquals("#445566", firstDetail.lists[0].cards[0].tags[1].colorHex) + assertEquals("#112233", firstDetail.lists[0].cards[1].tags[0].colorHex) + assertEquals("#112233", secondDetail.lists[0].cards[0].tags[0].colorHex) + assertEquals("#445566", secondDetail.lists[0].cards[0].tags[1].colorHex) + assertEquals(listOf("label-1", "label-2"), apiClient.getLabelByPublicIdCalls) + } + + @Test + fun getBoardDetailKeepsOriginalColorWhenLabelLookupFails() = runTest { + val detailWithTag = BoardDetail( + id = "board-1", + title = "Board", + lists = listOf( + BoardListDetail( + id = "list-1", + title = "To Do", + cards = listOf( + BoardCardSummary( + id = "card-1", + title = "Card 1", + tags = listOf(BoardTagSummary(id = "label-1", name = "Urgent", colorHex = "#ABCDEF")), + dueAtEpochMillis = null, + ), + ), + ), + ), + ) + val apiClient = FakeBoardDetailApiClient().apply { + boardDetailResult = BoardsApiResult.Success(detailWithTag) + labelByIdResults = mapOf("label-1" to BoardsApiResult.Failure("Server unavailable")) + } + val repository = createRepository(apiClient = apiClient) + + val result = repository.getBoardDetail("board-1") + + assertTrue(result is BoardsApiResult.Success<*>) + val detail = (result as BoardsApiResult.Success).value + assertEquals("#ABCDEF", detail.lists[0].cards[0].tags[0].colorHex) + assertEquals(listOf("label-1"), apiClient.getLabelByPublicIdCalls) + } + @Test fun renameListPropagatesApiFailureMessageUnchanged() = runTest { val apiClient = FakeBoardDetailApiClient().apply { @@ -366,6 +456,7 @@ class BoardDetailRepositoryTest { var renameListResult: BoardsApiResult = BoardsApiResult.Success(Unit) var moveOutcomes: Map> = emptyMap() var deleteOutcomes: Map> = emptyMap() + var labelByIdResults: Map> = emptyMap() var listWorkspacesCalls: Int = 0 var lastBoardId: String? = null @@ -374,6 +465,7 @@ class BoardDetailRepositoryTest { var movedCardIds: MutableList = mutableListOf() var deletedCardIds: MutableList = mutableListOf() var lastMoveTargetListId: String? = null + var getLabelByPublicIdCalls: MutableList = mutableListOf() override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success @@ -425,6 +517,15 @@ class BoardDetailRepositoryTest { return deleteOutcomes[cardId] ?: BoardsApiResult.Success(Unit) } + override suspend fun getLabelByPublicId( + baseUrl: String, + apiKey: String, + labelId: String, + ): BoardsApiResult { + getLabelByPublicIdCalls += labelId + return labelByIdResults[labelId] ?: BoardsApiResult.Failure("Missing fake label") + } + override suspend fun listBoards( baseUrl: String, apiKey: String, diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepositoryTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepositoryTest.kt index 2aabe86..7798f3f 100644 --- a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepositoryTest.kt +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsRepositoryTest.kt @@ -7,6 +7,7 @@ import org.junit.Test import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient +import space.hackenslacker.kanbn4droid.app.auth.LabelDetail import space.hackenslacker.kanbn4droid.app.auth.SessionStore class BoardsRepositoryTest { @@ -250,5 +251,13 @@ class BoardsRepositoryTest { lastDeletedId = boardId return deleteBoardResult } + + override suspend fun getLabelByPublicId( + baseUrl: String, + apiKey: String, + labelId: String, + ): BoardsApiResult { + return BoardsApiResult.Failure("Not needed in boards tests") + } } } diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt index a5d901e..f06b0af 100644 --- a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsViewModelTest.kt @@ -18,6 +18,7 @@ import org.junit.Test import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore import space.hackenslacker.kanbn4droid.app.auth.AuthResult import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient +import space.hackenslacker.kanbn4droid.app.auth.LabelDetail import space.hackenslacker.kanbn4droid.app.auth.SessionStore @OptIn(ExperimentalCoroutinesApi::class) @@ -194,5 +195,13 @@ class BoardsViewModelTest { lastDeletedId = boardId return deleteBoardResult } + + override suspend fun getLabelByPublicId( + baseUrl: String, + apiKey: String, + labelId: String, + ): BoardsApiResult { + return BoardsApiResult.Failure("Not needed in boards tests") + } } }