feat(kanban): add board transfer models, endpoints and auto-nudge dispatcher (#1137)

This commit is contained in:
M57
2026-09-16 18:13:54 +03:00
parent 97296c111a
commit be9223eb78
5 changed files with 134 additions and 17 deletions
@@ -92,3 +92,17 @@ data class DeleteBoardResponse(
val result: JsonElement? = null, val result: JsonElement? = null,
val current: String? = null, val current: String? = null,
) )
@Serializable
data class ExportBoardBody(
val output: String = "",
val attachments: Boolean = true,
val logs: Boolean = false,
)
@Serializable
data class ImportBoardBody(
val archive: String,
val slug: String? = null,
val switch: Boolean = false,
)
@@ -252,4 +252,6 @@ data class TaskEstimate(
val complexity: String? = null, val complexity: String? = null,
val rationale: String? = null, val rationale: String? = null,
val model: String? = null, val model: String? = null,
) ) {
val tokens: Int? get() = estTokens
}
@@ -2,6 +2,8 @@ package com.m57.hermescontrol.data.remote
import com.m57.hermescontrol.data.model.AttachmentUploadResponse import com.m57.hermescontrol.data.model.AttachmentUploadResponse
import com.m57.hermescontrol.data.model.AutoDescribeResponse import com.m57.hermescontrol.data.model.AutoDescribeResponse
import com.m57.hermescontrol.data.model.BoardExportResult
import com.m57.hermescontrol.data.model.BoardImportResult
import com.m57.hermescontrol.data.model.BulkTasksBody import com.m57.hermescontrol.data.model.BulkTasksBody
import com.m57.hermescontrol.data.model.BulkTasksResponse import com.m57.hermescontrol.data.model.BulkTasksResponse
import com.m57.hermescontrol.data.model.CreateBoardBody import com.m57.hermescontrol.data.model.CreateBoardBody
@@ -10,6 +12,8 @@ import com.m57.hermescontrol.data.model.CreateTaskBody
import com.m57.hermescontrol.data.model.CreateTaskResponse import com.m57.hermescontrol.data.model.CreateTaskResponse
import com.m57.hermescontrol.data.model.DeleteBoardResponse import com.m57.hermescontrol.data.model.DeleteBoardResponse
import com.m57.hermescontrol.data.model.DispatchResult import com.m57.hermescontrol.data.model.DispatchResult
import com.m57.hermescontrol.data.model.ExportBoardBody
import com.m57.hermescontrol.data.model.ImportBoardBody
import com.m57.hermescontrol.data.model.KanbanBoardResponse import com.m57.hermescontrol.data.model.KanbanBoardResponse
import com.m57.hermescontrol.data.model.KanbanBoardsResponse import com.m57.hermescontrol.data.model.KanbanBoardsResponse
import com.m57.hermescontrol.data.model.KanbanProfilesResponse import com.m57.hermescontrol.data.model.KanbanProfilesResponse
@@ -77,6 +81,17 @@ interface KanbanApiService {
@Path("slug") slug: String, @Path("slug") slug: String,
): Response<Unit> ): Response<Unit>
@POST("api/plugins/kanban/boards/{slug}/export")
suspend fun exportBoard(
@Path("slug") slug: String,
@Body body: ExportBoardBody = ExportBoardBody(),
): Response<BoardExportResult>
@POST("api/plugins/kanban/boards/import")
suspend fun importBoard(
@Body body: ImportBoardBody,
): Response<BoardImportResult>
@GET("api/plugins/kanban/tasks/{id}") @GET("api/plugins/kanban/tasks/{id}")
suspend fun getTask( suspend fun getTask(
@Path("id") taskId: String, @Path("id") taskId: String,
@@ -0,0 +1,35 @@
package com.m57.hermescontrol.data.repository
import com.m57.hermescontrol.data.remote.KanbanApiService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
interface KanbanDispatcherNudger {
fun scheduleNudge(board: String?)
}
class DefaultKanbanDispatcherNudger(
private val apiProvider: () -> KanbanApiService,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
private val debounceMs: Long = 400L,
) : KanbanDispatcherNudger {
private val lock = Any()
private val boardJobs = mutableMapOf<String?, Job>()
override fun scheduleNudge(board: String?) {
synchronized(lock) {
boardJobs[board]?.cancel()
boardJobs[board] =
scope.launch {
delay(debounceMs)
runCatching {
apiProvider().nudgeDispatcher(board)
}
}
}
}
}
@@ -2,6 +2,8 @@ package com.m57.hermescontrol.data.repository
import com.m57.hermescontrol.data.model.AttachmentUploadResponse import com.m57.hermescontrol.data.model.AttachmentUploadResponse
import com.m57.hermescontrol.data.model.AutoDescribeResponse import com.m57.hermescontrol.data.model.AutoDescribeResponse
import com.m57.hermescontrol.data.model.BoardExportResult
import com.m57.hermescontrol.data.model.BoardImportResult
import com.m57.hermescontrol.data.model.BulkTasksBody import com.m57.hermescontrol.data.model.BulkTasksBody
import com.m57.hermescontrol.data.model.BulkTasksResponse import com.m57.hermescontrol.data.model.BulkTasksResponse
import com.m57.hermescontrol.data.model.CreateBoardBody import com.m57.hermescontrol.data.model.CreateBoardBody
@@ -10,6 +12,8 @@ import com.m57.hermescontrol.data.model.CreateTaskBody
import com.m57.hermescontrol.data.model.CreateTaskResponse import com.m57.hermescontrol.data.model.CreateTaskResponse
import com.m57.hermescontrol.data.model.DeleteBoardResponse import com.m57.hermescontrol.data.model.DeleteBoardResponse
import com.m57.hermescontrol.data.model.DispatchResult import com.m57.hermescontrol.data.model.DispatchResult
import com.m57.hermescontrol.data.model.ExportBoardBody
import com.m57.hermescontrol.data.model.ImportBoardBody
import com.m57.hermescontrol.data.model.KanbanBoardResponse import com.m57.hermescontrol.data.model.KanbanBoardResponse
import com.m57.hermescontrol.data.model.KanbanBoardsResponse import com.m57.hermescontrol.data.model.KanbanBoardsResponse
import com.m57.hermescontrol.data.model.KanbanProfilesResponse import com.m57.hermescontrol.data.model.KanbanProfilesResponse
@@ -58,6 +62,13 @@ interface KanbanRepository {
delete: Boolean = false, delete: Boolean = false,
): NetworkResult<DeleteBoardResponse> ): NetworkResult<DeleteBoardResponse>
suspend fun exportBoard(
slug: String,
body: ExportBoardBody = ExportBoardBody(),
): NetworkResult<BoardExportResult>
suspend fun importBoard(body: ImportBoardBody): NetworkResult<BoardImportResult>
suspend fun getTask( suspend fun getTask(
taskId: String, taskId: String,
board: String?, board: String?,
@@ -151,6 +162,7 @@ interface KanbanRepository {
class KanbanRepositoryImpl( class KanbanRepositoryImpl(
private val apiProvider: () -> KanbanApiService = { ApiClient.kanbanApi }, private val apiProvider: () -> KanbanApiService = { ApiClient.kanbanApi },
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val nudger: KanbanDispatcherNudger = DefaultKanbanDispatcherNudger(apiProvider),
) : KanbanRepository { ) : KanbanRepository {
override suspend fun getBoards(includeArchived: Boolean): NetworkResult<KanbanBoardsResponse> = override suspend fun getBoards(includeArchived: Boolean): NetworkResult<KanbanBoardsResponse> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
@@ -187,6 +199,19 @@ class KanbanRepositoryImpl(
safeApiCall { apiProvider().deleteBoard(slug, delete) } safeApiCall { apiProvider().deleteBoard(slug, delete) }
} }
override suspend fun exportBoard(
slug: String,
body: ExportBoardBody,
): NetworkResult<BoardExportResult> =
withContext(ioDispatcher) {
safeApiCall { apiProvider().exportBoard(slug, body) }
}
override suspend fun importBoard(body: ImportBoardBody): NetworkResult<BoardImportResult> =
withContext(ioDispatcher) {
safeApiCall { apiProvider().importBoard(body) }
}
override suspend fun getTask( override suspend fun getTask(
taskId: String, taskId: String,
board: String?, board: String?,
@@ -200,7 +225,11 @@ class KanbanRepositoryImpl(
body: CreateTaskBody, body: CreateTaskBody,
): NetworkResult<CreateTaskResponse> = ): NetworkResult<CreateTaskResponse> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
safeApiCall { apiProvider().createTask(board = board, body = body) } val result = safeApiCall { apiProvider().createTask(board = board, body = body) }
if (result is NetworkResult.Success) {
nudger.scheduleNudge(board)
}
result
} }
override suspend fun updateTask( override suspend fun updateTask(
@@ -209,7 +238,11 @@ class KanbanRepositoryImpl(
body: UpdateTaskBody, body: UpdateTaskBody,
): NetworkResult<UpdateTaskResponse> = ): NetworkResult<UpdateTaskResponse> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
safeApiCall { apiProvider().updateTask(taskId = taskId, board = board, body = body) } val result = safeApiCall { apiProvider().updateTask(taskId = taskId, board = board, body = body) }
if (result is NetworkResult.Success) {
nudger.scheduleNudge(board)
}
result
} }
override suspend fun deleteTask( override suspend fun deleteTask(
@@ -217,7 +250,11 @@ class KanbanRepositoryImpl(
board: String?, board: String?,
): NetworkResult<Unit> = ): NetworkResult<Unit> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
safeApiCall { apiProvider().deleteTask(taskId = taskId, board = board) } val result = safeApiCall { apiProvider().deleteTask(taskId = taskId, board = board) }
if (result is NetworkResult.Success) {
nudger.scheduleNudge(board)
}
result
} }
override suspend fun bulkTasks( override suspend fun bulkTasks(
@@ -225,7 +262,11 @@ class KanbanRepositoryImpl(
body: BulkTasksBody, body: BulkTasksBody,
): NetworkResult<BulkTasksResponse> = ): NetworkResult<BulkTasksResponse> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
safeApiCall { apiProvider().bulkTasks(board = board, body = body) } val result = safeApiCall { apiProvider().bulkTasks(board = board, body = body) }
if (result is NetworkResult.Success) {
nudger.scheduleNudge(board)
}
result
} }
override suspend fun addComment( override suspend fun addComment(
@@ -250,13 +291,18 @@ class KanbanRepositoryImpl(
reclaimFirst: Boolean, reclaimFirst: Boolean,
): NetworkResult<ReassignTaskResponse> = ): NetworkResult<ReassignTaskResponse> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
safeApiCall { val result =
apiProvider().reassignTask( safeApiCall {
taskId = taskId, apiProvider().reassignTask(
board = board, taskId = taskId,
body = ReassignTaskBody(profile = profile, reclaimFirst = reclaimFirst), board = board,
) body = ReassignTaskBody(profile = profile, reclaimFirst = reclaimFirst),
)
}
if (result is NetworkResult.Success) {
nudger.scheduleNudge(board)
} }
result
} }
override suspend fun reclaimTask( override suspend fun reclaimTask(
@@ -265,13 +311,18 @@ class KanbanRepositoryImpl(
reason: String?, reason: String?,
): NetworkResult<ReclaimTaskResponse> = ): NetworkResult<ReclaimTaskResponse> =
withContext(ioDispatcher) { withContext(ioDispatcher) {
safeApiCall { val result =
apiProvider().reclaimTask( safeApiCall {
taskId = taskId, apiProvider().reclaimTask(
board = board, taskId = taskId,
body = ReclaimTaskBody(reason = reason), board = board,
) body = ReclaimTaskBody(reason = reason),
)
}
if (result is NetworkResult.Success) {
nudger.scheduleNudge(board)
} }
result
} }
override suspend fun getTaskLog( override suspend fun getTaskLog(