diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/KanbanBoardModels.kt b/app/src/main/java/com/m57/hermescontrol/data/model/KanbanBoardModels.kt index 22276588..a2b4a83e 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/KanbanBoardModels.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/KanbanBoardModels.kt @@ -92,3 +92,17 @@ data class DeleteBoardResponse( val result: JsonElement? = 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, +) diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/KanbanTaskModels.kt b/app/src/main/java/com/m57/hermescontrol/data/model/KanbanTaskModels.kt index b93d3c27..194f10a7 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/KanbanTaskModels.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/KanbanTaskModels.kt @@ -252,4 +252,6 @@ data class TaskEstimate( val complexity: String? = null, val rationale: String? = null, val model: String? = null, -) +) { + val tokens: Int? get() = estTokens +} diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/KanbanApiService.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/KanbanApiService.kt index fc85ec16..bd4a7648 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/KanbanApiService.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/KanbanApiService.kt @@ -2,6 +2,8 @@ package com.m57.hermescontrol.data.remote import com.m57.hermescontrol.data.model.AttachmentUploadResponse 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.BulkTasksResponse 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.DeleteBoardResponse 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.KanbanBoardsResponse import com.m57.hermescontrol.data.model.KanbanProfilesResponse @@ -77,6 +81,17 @@ interface KanbanApiService { @Path("slug") slug: String, ): Response + @POST("api/plugins/kanban/boards/{slug}/export") + suspend fun exportBoard( + @Path("slug") slug: String, + @Body body: ExportBoardBody = ExportBoardBody(), + ): Response + + @POST("api/plugins/kanban/boards/import") + suspend fun importBoard( + @Body body: ImportBoardBody, + ): Response + @GET("api/plugins/kanban/tasks/{id}") suspend fun getTask( @Path("id") taskId: String, diff --git a/app/src/main/java/com/m57/hermescontrol/data/repository/KanbanDispatcherNudger.kt b/app/src/main/java/com/m57/hermescontrol/data/repository/KanbanDispatcherNudger.kt new file mode 100644 index 00000000..2f18d5a8 --- /dev/null +++ b/app/src/main/java/com/m57/hermescontrol/data/repository/KanbanDispatcherNudger.kt @@ -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() + + override fun scheduleNudge(board: String?) { + synchronized(lock) { + boardJobs[board]?.cancel() + boardJobs[board] = + scope.launch { + delay(debounceMs) + runCatching { + apiProvider().nudgeDispatcher(board) + } + } + } + } +} diff --git a/app/src/main/java/com/m57/hermescontrol/data/repository/KanbanRepository.kt b/app/src/main/java/com/m57/hermescontrol/data/repository/KanbanRepository.kt index efe3c419..422a7e27 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/repository/KanbanRepository.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/repository/KanbanRepository.kt @@ -2,6 +2,8 @@ package com.m57.hermescontrol.data.repository import com.m57.hermescontrol.data.model.AttachmentUploadResponse 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.BulkTasksResponse 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.DeleteBoardResponse 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.KanbanBoardsResponse import com.m57.hermescontrol.data.model.KanbanProfilesResponse @@ -58,6 +62,13 @@ interface KanbanRepository { delete: Boolean = false, ): NetworkResult + suspend fun exportBoard( + slug: String, + body: ExportBoardBody = ExportBoardBody(), + ): NetworkResult + + suspend fun importBoard(body: ImportBoardBody): NetworkResult + suspend fun getTask( taskId: String, board: String?, @@ -151,6 +162,7 @@ interface KanbanRepository { class KanbanRepositoryImpl( private val apiProvider: () -> KanbanApiService = { ApiClient.kanbanApi }, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, + private val nudger: KanbanDispatcherNudger = DefaultKanbanDispatcherNudger(apiProvider), ) : KanbanRepository { override suspend fun getBoards(includeArchived: Boolean): NetworkResult = withContext(ioDispatcher) { @@ -187,6 +199,19 @@ class KanbanRepositoryImpl( safeApiCall { apiProvider().deleteBoard(slug, delete) } } + override suspend fun exportBoard( + slug: String, + body: ExportBoardBody, + ): NetworkResult = + withContext(ioDispatcher) { + safeApiCall { apiProvider().exportBoard(slug, body) } + } + + override suspend fun importBoard(body: ImportBoardBody): NetworkResult = + withContext(ioDispatcher) { + safeApiCall { apiProvider().importBoard(body) } + } + override suspend fun getTask( taskId: String, board: String?, @@ -200,7 +225,11 @@ class KanbanRepositoryImpl( body: CreateTaskBody, ): NetworkResult = 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( @@ -209,7 +238,11 @@ class KanbanRepositoryImpl( body: UpdateTaskBody, ): NetworkResult = 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( @@ -217,7 +250,11 @@ class KanbanRepositoryImpl( board: String?, ): NetworkResult = 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( @@ -225,7 +262,11 @@ class KanbanRepositoryImpl( body: BulkTasksBody, ): NetworkResult = 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( @@ -250,13 +291,18 @@ class KanbanRepositoryImpl( reclaimFirst: Boolean, ): NetworkResult = withContext(ioDispatcher) { - safeApiCall { - apiProvider().reassignTask( - taskId = taskId, - board = board, - body = ReassignTaskBody(profile = profile, reclaimFirst = reclaimFirst), - ) + val result = + safeApiCall { + apiProvider().reassignTask( + taskId = taskId, + board = board, + body = ReassignTaskBody(profile = profile, reclaimFirst = reclaimFirst), + ) + } + if (result is NetworkResult.Success) { + nudger.scheduleNudge(board) } + result } override suspend fun reclaimTask( @@ -265,13 +311,18 @@ class KanbanRepositoryImpl( reason: String?, ): NetworkResult = withContext(ioDispatcher) { - safeApiCall { - apiProvider().reclaimTask( - taskId = taskId, - board = board, - body = ReclaimTaskBody(reason = reason), - ) + val result = + safeApiCall { + apiProvider().reclaimTask( + taskId = taskId, + board = board, + body = ReclaimTaskBody(reason = reason), + ) + } + if (result is NetworkResult.Success) { + nudger.scheduleNudge(board) } + result } override suspend fun getTaskLog(