feat: add board detail create list and card api calls
This commit is contained in:
@@ -12,6 +12,7 @@ import space.hackenslacker.kanbn4droid.app.boarddetail.BoardCardSummary
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetail
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardListDetail
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardTagSummary
|
||||
import space.hackenslacker.kanbn4droid.app.boarddetail.CreatedEntityRef
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
||||
@@ -62,6 +63,28 @@ interface KanbnApiClient {
|
||||
return BoardsApiResult.Failure("List rename is not implemented.")
|
||||
}
|
||||
|
||||
suspend fun createList(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
boardPublicId: String,
|
||||
title: String,
|
||||
appendIndex: Int,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return BoardsApiResult.Failure("List creation is not implemented.")
|
||||
}
|
||||
|
||||
suspend fun createCard(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
listPublicId: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
dueDate: String?,
|
||||
tagPublicIds: List<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return BoardsApiResult.Failure("Card creation is not implemented.")
|
||||
}
|
||||
|
||||
suspend fun moveCard(baseUrl: String, apiKey: String, cardId: String, targetListId: String): BoardsApiResult<Unit> {
|
||||
return BoardsApiResult.Failure("Card move is not implemented.")
|
||||
}
|
||||
@@ -269,6 +292,71 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createList(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
boardPublicId: String,
|
||||
title: String,
|
||||
appendIndex: Int,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val payload = JSONObject()
|
||||
.put("boardPublicId", boardPublicId)
|
||||
.put("name", title)
|
||||
.put("index", appendIndex)
|
||||
request(
|
||||
baseUrl = baseUrl,
|
||||
path = "/api/v1/lists",
|
||||
method = "POST",
|
||||
apiKey = apiKey,
|
||||
body = payload.toString(),
|
||||
) { code, body ->
|
||||
if (code in 200..299) {
|
||||
parseCreatedEntityRef(body)
|
||||
?.let { BoardsApiResult.Success(it) }
|
||||
?: BoardsApiResult.Failure("Malformed create list response.")
|
||||
} else {
|
||||
BoardsApiResult.Failure(serverMessage(body, code))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createCard(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
listPublicId: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
dueDate: String?,
|
||||
tagPublicIds: List<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val payload = JSONObject()
|
||||
.put("listPublicId", listPublicId)
|
||||
.put("title", title)
|
||||
.put("description", description ?: "")
|
||||
.put("dueDate", dueDate)
|
||||
.put("index", 0)
|
||||
.put("labelPublicIds", JSONArray(tagPublicIds))
|
||||
request(
|
||||
baseUrl = baseUrl,
|
||||
path = "/api/v1/cards",
|
||||
method = "POST",
|
||||
apiKey = apiKey,
|
||||
body = payload.toString(),
|
||||
) { code, body ->
|
||||
if (code in 200..299) {
|
||||
parseCreatedEntityRef(body)
|
||||
?.let { BoardsApiResult.Success(it) }
|
||||
?: BoardsApiResult.Failure("Malformed create card response.")
|
||||
} else {
|
||||
BoardsApiResult.Failure(serverMessage(body, code))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun moveCard(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
@@ -667,6 +755,27 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
return LabelDetail(id = id, colorHex = colorHex)
|
||||
}
|
||||
|
||||
private fun parseCreatedEntityRef(body: String): CreatedEntityRef? {
|
||||
if (body.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val root = parseJsonObject(body) ?: return null
|
||||
val data = root["data"]
|
||||
val entity = when (data) {
|
||||
is Map<*, *> -> {
|
||||
(data["card"] as? Map<*, *>)
|
||||
?: (data["list"] as? Map<*, *>)
|
||||
?: data
|
||||
}
|
||||
|
||||
else -> root
|
||||
}
|
||||
|
||||
val publicId = extractString(entity, "publicId", "public_id", "id")
|
||||
return CreatedEntityRef(publicId = publicId.ifBlank { null })
|
||||
}
|
||||
|
||||
private fun parseLists(board: Map<*, *>): List<BoardListDetail> {
|
||||
return extractObjectArray(board, "lists", "items", "data").mapNotNull { rawList ->
|
||||
val id = extractId(rawList)
|
||||
|
||||
@@ -69,6 +69,46 @@ class BoardDetailRepository(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun createCard(
|
||||
listId: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
dueDate: String?,
|
||||
tagIds: Collection<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
val normalizedListId = listId.trim()
|
||||
if (normalizedListId.isBlank()) {
|
||||
return BoardsApiResult.Failure("List id is required")
|
||||
}
|
||||
|
||||
val normalizedTitle = title.trim()
|
||||
if (normalizedTitle.isBlank()) {
|
||||
return BoardsApiResult.Failure("Card title is required")
|
||||
}
|
||||
|
||||
val normalizedDescription = description?.trim()?.takeIf { it.isNotBlank() }
|
||||
val normalizedDueDate = dueDate?.trim()?.takeIf { it.isNotBlank() }
|
||||
val normalizedTagIds = tagIds
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
|
||||
val session = when (val sessionResult = session()) {
|
||||
is BoardsApiResult.Success -> sessionResult.value
|
||||
is BoardsApiResult.Failure -> return sessionResult
|
||||
}
|
||||
|
||||
return apiClient.createCard(
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
listPublicId = normalizedListId,
|
||||
title = normalizedTitle,
|
||||
description = normalizedDescription,
|
||||
dueDate = normalizedDueDate,
|
||||
tagPublicIds = normalizedTagIds,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun moveCards(cardIds: Collection<String>, targetListId: String): CardBatchMutationResult {
|
||||
val normalizedTargetListId = targetListId.trim()
|
||||
if (normalizedTargetListId.isBlank()) {
|
||||
|
||||
@@ -207,6 +207,23 @@ class BoardDetailRepositoryTest {
|
||||
assertEquals("New title", apiClient.lastListTitle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createCard_callsApiWithPublicIdsAndTopIndex() = runTest {
|
||||
val apiClient = FakeBoardDetailApiClient()
|
||||
val repository = createRepository(apiClient = apiClient)
|
||||
|
||||
val result = repository.createCard(
|
||||
listId = " list-1 ",
|
||||
title = " Card title ",
|
||||
description = "Description",
|
||||
dueDate = null,
|
||||
tagIds = listOf(" tag-1 ", "", "tag-2"),
|
||||
)
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
assertEquals("list-1", apiClient.lastCreateCardListPublicId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBoardDetailValidatesBoardId() = runTest {
|
||||
val repository = createRepository()
|
||||
@@ -454,6 +471,8 @@ class BoardDetailRepositoryTest {
|
||||
BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
||||
var boardDetailResult: BoardsApiResult<BoardDetail> = BoardsApiResult.Success(sampleBoardDetail())
|
||||
var renameListResult: BoardsApiResult<Unit> = BoardsApiResult.Success(Unit)
|
||||
var createListResult: BoardsApiResult<CreatedEntityRef> = BoardsApiResult.Success(CreatedEntityRef("list-new"))
|
||||
var createCardResult: BoardsApiResult<CreatedEntityRef> = BoardsApiResult.Success(CreatedEntityRef("card-new"))
|
||||
var moveOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
|
||||
var deleteOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
|
||||
var labelByIdResults: Map<String, BoardsApiResult<LabelDetail>> = emptyMap()
|
||||
@@ -466,6 +485,7 @@ class BoardDetailRepositoryTest {
|
||||
var deletedCardIds: MutableList<String> = mutableListOf()
|
||||
var lastMoveTargetListId: String? = null
|
||||
var getLabelByPublicIdCalls: MutableList<String> = mutableListOf()
|
||||
var lastCreateCardListPublicId: String? = null
|
||||
|
||||
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
||||
|
||||
@@ -497,6 +517,29 @@ class BoardDetailRepositoryTest {
|
||||
return renameListResult
|
||||
}
|
||||
|
||||
override suspend fun createList(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
boardPublicId: String,
|
||||
title: String,
|
||||
appendIndex: Int,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return createListResult
|
||||
}
|
||||
|
||||
override suspend fun createCard(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
listPublicId: String,
|
||||
title: String,
|
||||
description: String?,
|
||||
dueDate: String?,
|
||||
tagPublicIds: List<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
lastCreateCardListPublicId = listPublicId
|
||||
return createCardResult
|
||||
}
|
||||
|
||||
override suspend fun moveCard(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
|
||||
Reference in New Issue
Block a user