[gitlab] ability to resolve MR discussions from timeline

GitOrigin-RevId: 73d790c6024042500503a67164d9609a136a526c
This commit is contained in:
Ivan Semenov
2023-01-15 16:07:14 +01:00
committed by intellij-monorepo-bot
parent 9db486be3d
commit 02af04d951
9 changed files with 180 additions and 28 deletions

View File

@@ -0,0 +1,7 @@
mutation($discussionId: DiscussionID!, $resolved: Boolean!) {
discussionToggleResolve(input: {id: $discussionId, resolve: $resolved}) {
discussion {
...discussion
}
}
}

View File

@@ -6,4 +6,5 @@ object GitLabGQLQueries {
const val getProjectMembers = "graphql/query/getProjectMembers.graphql"
const val getProjectLabels = "graphql/query/getProjectLabels.graphql"
const val getMergeRequestDiscussions = "graphql/query/getMergeRequestDiscussions.graphql"
const val toggleMergeRequestDiscussionResolve = "graphql/query/toggleMergeRequestDiscussionResolve.graphql"
}

View File

@@ -0,0 +1,41 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gitlab.mergerequest.api.request
import com.intellij.collaboration.api.data.asParameters
import com.intellij.collaboration.api.dto.GraphQLConnectionDTO
import com.intellij.collaboration.api.dto.GraphQLCursorPageInfoDTO
import com.intellij.collaboration.api.page.ApiPageUtil
import kotlinx.coroutines.flow.Flow
import org.jetbrains.plugins.gitlab.api.GitLabApi
import org.jetbrains.plugins.gitlab.api.GitLabGQLQueries
import org.jetbrains.plugins.gitlab.api.GitLabProjectCoordinates
import org.jetbrains.plugins.gitlab.api.dto.GitLabDiscussionDTO
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestId
import java.net.http.HttpResponse
suspend fun GitLabApi.loadAllMergeRequestDiscussions(project: GitLabProjectCoordinates,
mr: GitLabMergeRequestId): Flow<List<GitLabDiscussionDTO>> =
ApiPageUtil.createGQLPagesFlow {
val parameters = it.asParameters() + mapOf(
"projectId" to project.projectPath.fullPath(),
"mriid" to mr.iid
)
val request = gqlQuery(project.serverPath.gqlApiUri, GitLabGQLQueries.getMergeRequestDiscussions, parameters)
loadGQLResponse(request, DiscussionConnection::class.java, "project", "mergeRequest", "discussions").body()
}
private class DiscussionConnection(pageInfo: GraphQLCursorPageInfoDTO, nodes: List<GitLabDiscussionDTO>)
: GraphQLConnectionDTO<GitLabDiscussionDTO>(pageInfo, nodes)
suspend fun GitLabApi.changeMergeRequestDiscussionResolve(
project: GitLabProjectCoordinates,
discussionId: String,
resolved: Boolean
): HttpResponse<out GitLabDiscussionDTO?> {
val parameters = mapOf(
"discussionId" to discussionId,
"resolved" to resolved
)
val request = gqlQuery(project.serverPath.gqlApiUri, GitLabGQLQueries.toggleMergeRequestDiscussionResolve, parameters)
return loadGQLResponse(request, GitLabDiscussionDTO::class.java, "discussionToggleResolve", "discussion")
}

View File

@@ -1,18 +1,11 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gitlab.mergerequest.api.request
import com.intellij.collaboration.api.data.asParameters
import com.intellij.collaboration.api.dto.GraphQLConnectionDTO
import com.intellij.collaboration.api.dto.GraphQLCursorPageInfoDTO
import com.intellij.collaboration.api.json.loadJsonList
import com.intellij.collaboration.api.page.ApiPageUtil
import com.intellij.collaboration.util.resolveRelative
import com.intellij.collaboration.util.withQuery
import kotlinx.coroutines.flow.Flow
import org.jetbrains.plugins.gitlab.api.GitLabApi
import org.jetbrains.plugins.gitlab.api.GitLabGQLQueries
import org.jetbrains.plugins.gitlab.api.GitLabProjectCoordinates
import org.jetbrains.plugins.gitlab.api.dto.GitLabDiscussionDTO
import org.jetbrains.plugins.gitlab.api.dto.GitLabResourceLabelEventDTO
import org.jetbrains.plugins.gitlab.api.dto.GitLabResourceMilestoneEventDTO
import org.jetbrains.plugins.gitlab.api.dto.GitLabResourceStateEventDTO
@@ -28,20 +21,6 @@ suspend fun GitLabApi.loadMergeRequests(project: GitLabProjectCoordinates,
return loadJsonList(request)
}
suspend fun GitLabApi.loadAllMergeRequestDiscussions(project: GitLabProjectCoordinates,
mr: GitLabMergeRequestId): Flow<List<GitLabDiscussionDTO>> =
ApiPageUtil.createGQLPagesFlow {
val parameters = it.asParameters() + mapOf(
"projectId" to project.projectPath.fullPath(),
"mriid" to mr.iid
)
val request = gqlQuery(project.serverPath.gqlApiUri, GitLabGQLQueries.getMergeRequestDiscussions, parameters)
loadGQLResponse(request, DiscussionConnection::class.java, "project", "mergeRequest", "discussions").body()
}
private class DiscussionConnection(pageInfo: GraphQLCursorPageInfoDTO, nodes: List<GitLabDiscussionDTO>)
: GraphQLConnectionDTO<GitLabDiscussionDTO>(pageInfo, nodes)
suspend fun GitLabApi.loadMergeRequestStateEvents(project: GitLabProjectCoordinates,
mr: GitLabMergeRequestId)
: HttpResponse<out List<GitLabResourceStateEventDTO>> {

View File

@@ -1,19 +1,35 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gitlab.mergerequest.data
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import com.intellij.openapi.diagnostic.logger
import com.intellij.util.childScope
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.jetbrains.plugins.gitlab.api.GitLabProjectConnection
import org.jetbrains.plugins.gitlab.api.dto.GitLabDiscussionDTO
import org.jetbrains.plugins.gitlab.api.dto.GitLabNoteDTO
import org.jetbrains.plugins.gitlab.mergerequest.api.request.changeMergeRequestDiscussionResolve
import java.util.*
interface GitLabDiscussion {
val createdAt: Date
val notes: Flow<List<GitLabNoteDTO>>
val canResolve: Boolean
val resolved: Flow<Boolean>
suspend fun changeResolvedState()
}
class LoadedGitLabDiscussion(
discussion: GitLabDiscussionDTO
parentCs: CoroutineScope,
private val connection: GitLabProjectConnection,
private val discussion: GitLabDiscussionDTO
) : GitLabDiscussion {
init {
require(discussion.notes.isNotEmpty()) { "Discussion with empty notes" }
@@ -21,5 +37,31 @@ class LoadedGitLabDiscussion(
override val createdAt: Date = discussion.createdAt
override val notes: Flow<List<GitLabNoteDTO>> = MutableStateFlow(discussion.notes)
private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e ->
logger<GitLabDiscussion>().info(e.localizedMessage)
})
private val operationsGuard = Mutex()
private val _notes = MutableStateFlow(discussion.notes)
override val notes: Flow<List<GitLabNoteDTO>> = _notes.asStateFlow()
private val firstNote = notes.map { it.first() }
// a little cheat that greatly simplifies the implementation
override val canResolve: Boolean = discussion.notes.first().let { it.resolvable && it.userPermissions.resolveNote }
override val resolved: Flow<Boolean> = firstNote.map { it.resolved }
override suspend fun changeResolvedState() {
withContext(cs.coroutineContext) {
operationsGuard.withLock {
val resolved = resolved.first()
val result = withContext(Dispatchers.IO) {
connection.apiClient
.changeMergeRequestDiscussionResolve(connection.repo.repository, discussion.id, !resolved).body()!!
}
_notes.value = result.notes
}
}
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gitlab.mergerequest.ui.comment
import com.intellij.util.childScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabDiscussion
import java.util.*
interface GitLabMergeRequestDiscussionResolveViewModel {
val resolved: Flow<Boolean>
val busy: Flow<Boolean>
fun changeResolvedState()
}
class GitLabMergeRequestDiscussionResolveViewModelImpl(parentCs: CoroutineScope, private val discussion: GitLabDiscussion)
: GitLabMergeRequestDiscussionResolveViewModel {
private val cs = parentCs.childScope()
override val resolved: Flow<Boolean> = discussion.resolved
private val currentTaskKey = MutableStateFlow<UUID?>(null)
override val busy: Flow<Boolean> = currentTaskKey.map { it !== null }
override fun changeResolvedState() {
cs.launch {
val key = UUID.randomUUID()
// other task in progress
if (!currentTaskKey.compareAndSet(null, key)) return@launch
try {
discussion.changeResolvedState()
}
catch (e: Exception) {
if (e is CancellationException) throw e
//TODO: handle???
}
finally {
currentTaskKey.compareAndSet(key, null)
}
}
}
}

View File

@@ -21,6 +21,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.jetbrains.annotations.Nls
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.GitLabMergeRequestDiscussionResolveViewModel
import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.GitLabMergeRequestNoteViewModel
import org.jetbrains.plugins.gitlab.mergerequest.ui.timeline.GitLabMergeRequestTimelineUIUtil.createTitleTextPane
import javax.swing.JComponent
@@ -121,13 +122,39 @@ object GitLabMergeRequestTimelineDiscussionComponentFactory {
})
}
return HorizontalListPanel(Replies.ActionsFolded.HORIZONTAL_GAP).apply {
val repliesActions = HorizontalListPanel(Replies.ActionsFolded.HORIZONTAL_GAP).apply {
add(authorsLabel)
add(repliesLink)
add(lastReplyDateLabel)
}.apply {
bindVisibility(cs, item.replies.map { it.isNotEmpty() })
}
return HorizontalListPanel(Replies.ActionsFolded.HORIZONTAL_GROUP_GAP).apply {
add(repliesActions)
item.resolvedVm?.let {
createUnResolveLink(cs, it).also(::add)
}
}
}
private fun createUnResolveLink(cs: CoroutineScope,
vm: GitLabMergeRequestDiscussionResolveViewModel): LinkLabel<Any> =
LinkLabel<Any>("", null) { _, _ ->
vm.changeResolvedState()
}.apply {
isFocusable = true
bindDisabled(cs, vm.busy)
bindText(cs, vm.resolved.map { resolved ->
if (resolved) {
CollaborationToolsBundle.message("review.comments.unresolve.action")
}
else {
CollaborationToolsBundle.message("review.comments.resolve.action")
}
})
}
private fun createNoteItem(cs: CoroutineScope,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
vm: GitLabMergeRequestNoteViewModel): JComponent {

View File

@@ -8,6 +8,8 @@ import kotlinx.coroutines.flow.*
import org.jetbrains.plugins.gitlab.api.dto.GitLabNoteDTO
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabDiscussion
import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.GitLabMergeRequestDiscussionResolveViewModel
import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.GitLabMergeRequestDiscussionResolveViewModelImpl
import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.GitLabMergeRequestNoteViewModel
import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.LoadedGitLabMergeRequestNoteViewModel
@@ -19,12 +21,14 @@ interface GitLabMergeRequestTimelineDiscussionViewModel {
val repliesFolded: Flow<Boolean>
val resolvedVm: GitLabMergeRequestDiscussionResolveViewModel?
fun setRepliesFolded(folded: Boolean)
}
class GitLabMergeRequestTimelineDiscussionViewModelImpl(
parentCs: CoroutineScope,
discussion: GitLabDiscussion
private val discussion: GitLabDiscussion
) : GitLabMergeRequestTimelineDiscussionViewModel {
private val cs = parentCs.childScope()
@@ -43,6 +47,9 @@ class GitLabMergeRequestTimelineDiscussionViewModelImpl(
override val author: Flow<GitLabUserDTO> = mainNote.map { it.author }
override val resolvedVm: GitLabMergeRequestDiscussionResolveViewModel? =
if (discussion.canResolve) GitLabMergeRequestDiscussionResolveViewModelImpl(cs, discussion) else null
override fun setRepliesFolded(folded: Boolean) {
_repliesFolded.value = folded
}

View File

@@ -85,7 +85,7 @@ class LoadAllGitLabMergeRequestTimelineViewModel(
GitLabMergeRequestTimelineItemViewModel.SystemDiscussion(it)
}
else {
GitLabMergeRequestTimelineItemViewModel.Discussion(cs, LoadedGitLabDiscussion(it))
GitLabMergeRequestTimelineItemViewModel.Discussion(cs, LoadedGitLabDiscussion(cs, connection, it))
}
})
}