feat: add board detail repository create list and card operations
This commit is contained in:
@@ -113,7 +113,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
|
||||
- Tapping on the filter by tag or search buttonswhen either of them is applied disables the active filter.
|
||||
- When a card(s) is selected by a long press, the filter by tag and search buttons get hidden by the select all, move card and delete card buttons until all cards are deselected.
|
||||
- When a card(s) is selected by a long press, the back arrow in the title bar and the back system button remove all selections.
|
||||
- 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`. Card move requests try these variants in order for compatibility across Kan.bn API versions: `PUT /api/v1/cards/{cardPublicId}` with `listPublicId`, then a GET+full-body `PUT /api/v1/cards/{cardPublicId}` payload (`title`, `description`, `index`, `listPublicId`, `dueDate`), then `PUT /api/v1/cards/{cardPublicId}` with `listId`, then `PATCH /api/v1/cards/{cardPublicId}` with `listId`. Board detail parsing now prefers public ids (`publicId`/`public_id`) over internal `id` values so follow-up card/list mutations target the correct API identifiers. 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.
|
||||
- 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`. Card move requests try these variants in order for compatibility across Kan.bn API versions: `PUT /api/v1/cards/{cardPublicId}` with `listPublicId`, then a GET+full-body `PUT /api/v1/cards/{cardPublicId}` payload (`title`, `description`, `index`, `listPublicId`, `dueDate`), then `PUT /api/v1/cards/{cardPublicId}` with `listId`, then `PATCH /api/v1/cards/{cardPublicId}` with `listId`. Board detail parsing now prefers public ids (`publicId`/`public_id`) over internal `id` values so follow-up card/list mutations target the correct API identifiers. 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. The repository now also exposes create-list and create-card methods that validate/normalize user input and delegate to API create endpoints using public IDs. 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.
|
||||
|
||||
@@ -70,15 +70,40 @@ class BoardDetailRepository(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun createList(boardPublicId: String, title: String): BoardsApiResult<CreatedEntityRef> {
|
||||
val normalizedBoardPublicId = boardPublicId.trim()
|
||||
if (normalizedBoardPublicId.isBlank()) {
|
||||
return BoardsApiResult.Failure("Board id is required")
|
||||
}
|
||||
|
||||
val normalizedTitle = title.trim()
|
||||
if (normalizedTitle.isBlank()) {
|
||||
return BoardsApiResult.Failure("List title is required")
|
||||
}
|
||||
|
||||
val session = when (val sessionResult = session()) {
|
||||
is BoardsApiResult.Success -> sessionResult.value
|
||||
is BoardsApiResult.Failure -> return sessionResult
|
||||
}
|
||||
|
||||
return apiClient.createList(
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
boardPublicId = normalizedBoardPublicId,
|
||||
title = normalizedTitle,
|
||||
appendIndex = 0,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun createCard(
|
||||
listId: String,
|
||||
listPublicId: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
dueDate: LocalDate?,
|
||||
tagIds: Collection<String>,
|
||||
tagPublicIds: Collection<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
val normalizedListId = listId.trim()
|
||||
if (normalizedListId.isBlank()) {
|
||||
val normalizedListPublicId = listPublicId.trim()
|
||||
if (normalizedListPublicId.isBlank()) {
|
||||
return BoardsApiResult.Failure("List id is required")
|
||||
}
|
||||
|
||||
@@ -88,7 +113,7 @@ class BoardDetailRepository(
|
||||
}
|
||||
|
||||
val normalizedDescription = description?.trim()?.takeIf { it.isNotBlank() }
|
||||
val normalizedTagIds = tagIds
|
||||
val normalizedTagPublicIds = tagPublicIds
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
@@ -101,11 +126,11 @@ class BoardDetailRepository(
|
||||
return apiClient.createCard(
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
listPublicId = normalizedListId,
|
||||
listPublicId = normalizedListPublicId,
|
||||
title = normalizedTitle,
|
||||
description = normalizedDescription,
|
||||
dueDate = dueDate,
|
||||
tagPublicIds = normalizedTagIds,
|
||||
tagPublicIds = normalizedTagPublicIds,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -214,11 +214,11 @@ class BoardDetailRepositoryTest {
|
||||
val repository = createRepository(apiClient = apiClient)
|
||||
|
||||
val result = repository.createCard(
|
||||
listId = " list-1 ",
|
||||
listPublicId = " list-1 ",
|
||||
title = " Card title ",
|
||||
description = " Description ",
|
||||
dueDate = LocalDate.of(2026, 3, 16),
|
||||
tagIds = listOf(" tag-1 ", "", "tag-2", "tag-1", " ", " tag-2"),
|
||||
tagPublicIds = listOf(" tag-1 ", "", "tag-2", "tag-1", " ", " tag-2"),
|
||||
)
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
@@ -235,17 +235,59 @@ class BoardDetailRepositoryTest {
|
||||
val repository = createRepository(apiClient = apiClient)
|
||||
|
||||
val result = repository.createCard(
|
||||
listId = "list-1",
|
||||
listPublicId = "list-1",
|
||||
title = "Card",
|
||||
description = " ",
|
||||
dueDate = null,
|
||||
tagIds = emptyList(),
|
||||
tagPublicIds = emptyList(),
|
||||
)
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
assertEquals(null, apiClient.lastCreateCardDescription)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createCard_delegatesWhenDueDateIsNull() = runTest {
|
||||
val apiClient = FakeBoardDetailApiClient()
|
||||
val repository = createRepository(apiClient = apiClient)
|
||||
|
||||
val result = repository.createCard(
|
||||
listPublicId = "list-1",
|
||||
title = "Card",
|
||||
description = "Description",
|
||||
dueDate = null,
|
||||
tagPublicIds = listOf("tag-1"),
|
||||
)
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
assertEquals("list-1", apiClient.lastCreateCardListPublicId)
|
||||
assertEquals("Card", apiClient.lastCreateCardTitle)
|
||||
assertEquals(null, apiClient.lastCreateCardDueDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createListRejectsBlankTitle() = runTest {
|
||||
val repository = createRepository()
|
||||
|
||||
val result = repository.createList(boardPublicId = "board-1", title = " ")
|
||||
|
||||
assertTrue(result is BoardsApiResult.Failure)
|
||||
assertEquals("List title is required", (result as BoardsApiResult.Failure).message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createListDelegatesToApiWithTrimmedIds() = runTest {
|
||||
val apiClient = FakeBoardDetailApiClient()
|
||||
val repository = createRepository(apiClient = apiClient)
|
||||
|
||||
val result = repository.createList(boardPublicId = " board-1 ", title = " New List ")
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
assertEquals("board-1", apiClient.lastCreateListBoardPublicId)
|
||||
assertEquals("New List", apiClient.lastCreateListTitle)
|
||||
assertEquals(0, apiClient.lastCreateListAppendIndex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBoardDetailValidatesBoardId() = runTest {
|
||||
val repository = createRepository()
|
||||
@@ -507,6 +549,9 @@ class BoardDetailRepositoryTest {
|
||||
var deletedCardIds: MutableList<String> = mutableListOf()
|
||||
var lastMoveTargetListId: String? = null
|
||||
var getLabelByPublicIdCalls: MutableList<String> = mutableListOf()
|
||||
var lastCreateListBoardPublicId: String? = null
|
||||
var lastCreateListTitle: String? = null
|
||||
var lastCreateListAppendIndex: Int? = null
|
||||
var lastCreateCardListPublicId: String? = null
|
||||
var lastCreateCardTitle: String? = null
|
||||
var lastCreateCardDescription: String? = null
|
||||
@@ -550,6 +595,9 @@ class BoardDetailRepositoryTest {
|
||||
title: String,
|
||||
appendIndex: Int,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
lastCreateListBoardPublicId = boardPublicId
|
||||
lastCreateListTitle = title
|
||||
lastCreateListAppendIndex = appendIndex
|
||||
return createListResult
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user