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.BoardDetail
|
||||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardListDetail
|
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardListDetail
|
||||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardTagSummary
|
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.BoardSummary
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
|
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
||||||
@@ -62,6 +63,28 @@ interface KanbnApiClient {
|
|||||||
return BoardsApiResult.Failure("List rename is not implemented.")
|
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> {
|
suspend fun moveCard(baseUrl: String, apiKey: String, cardId: String, targetListId: String): BoardsApiResult<Unit> {
|
||||||
return BoardsApiResult.Failure("Card move is not implemented.")
|
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(
|
override suspend fun moveCard(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
@@ -667,6 +755,27 @@ class HttpKanbnApiClient : KanbnApiClient {
|
|||||||
return LabelDetail(id = id, colorHex = colorHex)
|
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> {
|
private fun parseLists(board: Map<*, *>): List<BoardListDetail> {
|
||||||
return extractObjectArray(board, "lists", "items", "data").mapNotNull { rawList ->
|
return extractObjectArray(board, "lists", "items", "data").mapNotNull { rawList ->
|
||||||
val id = extractId(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 {
|
suspend fun moveCards(cardIds: Collection<String>, targetListId: String): CardBatchMutationResult {
|
||||||
val normalizedTargetListId = targetListId.trim()
|
val normalizedTargetListId = targetListId.trim()
|
||||||
if (normalizedTargetListId.isBlank()) {
|
if (normalizedTargetListId.isBlank()) {
|
||||||
|
|||||||
@@ -207,6 +207,23 @@ class BoardDetailRepositoryTest {
|
|||||||
assertEquals("New title", apiClient.lastListTitle)
|
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
|
@Test
|
||||||
fun getBoardDetailValidatesBoardId() = runTest {
|
fun getBoardDetailValidatesBoardId() = runTest {
|
||||||
val repository = createRepository()
|
val repository = createRepository()
|
||||||
@@ -454,6 +471,8 @@ class BoardDetailRepositoryTest {
|
|||||||
BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
||||||
var boardDetailResult: BoardsApiResult<BoardDetail> = BoardsApiResult.Success(sampleBoardDetail())
|
var boardDetailResult: BoardsApiResult<BoardDetail> = BoardsApiResult.Success(sampleBoardDetail())
|
||||||
var renameListResult: BoardsApiResult<Unit> = BoardsApiResult.Success(Unit)
|
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 moveOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
|
||||||
var deleteOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
|
var deleteOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
|
||||||
var labelByIdResults: Map<String, BoardsApiResult<LabelDetail>> = emptyMap()
|
var labelByIdResults: Map<String, BoardsApiResult<LabelDetail>> = emptyMap()
|
||||||
@@ -466,6 +485,7 @@ class BoardDetailRepositoryTest {
|
|||||||
var deletedCardIds: MutableList<String> = mutableListOf()
|
var deletedCardIds: MutableList<String> = mutableListOf()
|
||||||
var lastMoveTargetListId: String? = null
|
var lastMoveTargetListId: String? = null
|
||||||
var getLabelByPublicIdCalls: MutableList<String> = mutableListOf()
|
var getLabelByPublicIdCalls: MutableList<String> = mutableListOf()
|
||||||
|
var lastCreateCardListPublicId: String? = null
|
||||||
|
|
||||||
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
||||||
|
|
||||||
@@ -497,6 +517,29 @@ class BoardDetailRepositoryTest {
|
|||||||
return renameListResult
|
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(
|
override suspend fun moveCard(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user