From 02af04d951f88af10a4008f3f241fe60ba68f0b6 Mon Sep 17 00:00:00 2001 From: Ivan Semenov Date: Sun, 15 Jan 2023 16:07:14 +0100 Subject: [PATCH] [gitlab] ability to resolve MR discussions from timeline GitOrigin-RevId: 73d790c6024042500503a67164d9609a136a526c --- ...oggleMergeRequestDiscussionResolve.graphql | 7 +++ .../plugins/gitlab/api/GitLabGQLQueries.kt | 1 + .../request/GitLabMergeRequestCommentsApi.kt | 41 +++++++++++++++ .../api/request/GitLabMergeRequestsApi.kt | 21 -------- .../mergerequest/data/GitLabDiscussion.kt | 50 +++++++++++++++++-- ...bMergeRequestDiscussionResolveViewModel.kt | 48 ++++++++++++++++++ ...questTimelineDiscussionComponentFactory.kt | 29 ++++++++++- ...eRequestTimelineDiscussionViewModelImpl.kt | 9 +++- .../GitLabMergeRequestTimelineViewModel.kt | 2 +- 9 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 plugins/gitlab/resources/graphql/query/toggleMergeRequestDiscussionResolve.graphql create mode 100644 plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestCommentsApi.kt create mode 100644 plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestDiscussionResolveViewModel.kt diff --git a/plugins/gitlab/resources/graphql/query/toggleMergeRequestDiscussionResolve.graphql b/plugins/gitlab/resources/graphql/query/toggleMergeRequestDiscussionResolve.graphql new file mode 100644 index 000000000000..c8a0973e003f --- /dev/null +++ b/plugins/gitlab/resources/graphql/query/toggleMergeRequestDiscussionResolve.graphql @@ -0,0 +1,7 @@ +mutation($discussionId: DiscussionID!, $resolved: Boolean!) { + discussionToggleResolve(input: {id: $discussionId, resolve: $resolved}) { + discussion { + ...discussion + } + } +} \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGQLQueries.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGQLQueries.kt index 71f9a5cdc42b..ffd391a30db6 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGQLQueries.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGQLQueries.kt @@ -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" } \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestCommentsApi.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestCommentsApi.kt new file mode 100644 index 000000000000..7ec3b2e31087 --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestCommentsApi.kt @@ -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> = + 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) + : GraphQLConnectionDTO(pageInfo, nodes) + +suspend fun GitLabApi.changeMergeRequestDiscussionResolve( + project: GitLabProjectCoordinates, + discussionId: String, + resolved: Boolean +): HttpResponse { + 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") +} \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt index c08ea185a3b7..7468c1ef1774 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt @@ -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> = - 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) - : GraphQLConnectionDTO(pageInfo, nodes) - suspend fun GitLabApi.loadMergeRequestStateEvents(project: GitLabProjectCoordinates, mr: GitLabMergeRequestId) : HttpResponse> { diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabDiscussion.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabDiscussion.kt index 715834e66ee4..3b5cc5142cf8 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabDiscussion.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabDiscussion.kt @@ -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> + + val canResolve: Boolean + val resolved: Flow + + 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> = MutableStateFlow(discussion.notes) + private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e -> + logger().info(e.localizedMessage) + }) + + private val operationsGuard = Mutex() + + private val _notes = MutableStateFlow(discussion.notes) + override val notes: Flow> = _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 = 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 + } + } + } } \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestDiscussionResolveViewModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestDiscussionResolveViewModel.kt new file mode 100644 index 000000000000..ddfcc2e31f38 --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestDiscussionResolveViewModel.kt @@ -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 + val busy: Flow + + fun changeResolvedState() +} + +class GitLabMergeRequestDiscussionResolveViewModelImpl(parentCs: CoroutineScope, private val discussion: GitLabDiscussion) + : GitLabMergeRequestDiscussionResolveViewModel { + private val cs = parentCs.childScope() + + override val resolved: Flow = discussion.resolved + + private val currentTaskKey = MutableStateFlow(null) + override val busy: Flow = 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) + } + } + } +} diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt index 750319cdb87c..1d3710636211 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionComponentFactory.kt @@ -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 = + LinkLabel("", 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, vm: GitLabMergeRequestNoteViewModel): JComponent { diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionViewModelImpl.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionViewModelImpl.kt index dcc46f819fdc..d4d981ee615f 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionViewModelImpl.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineDiscussionViewModelImpl.kt @@ -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 + 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 = mainNote.map { it.author } + override val resolvedVm: GitLabMergeRequestDiscussionResolveViewModel? = + if (discussion.canResolve) GitLabMergeRequestDiscussionResolveViewModelImpl(cs, discussion) else null + override fun setRepliesFolded(folded: Boolean) { _repliesFolded.value = folded } diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt index c0a5105dcd16..a6e05a139f84 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/timeline/GitLabMergeRequestTimelineViewModel.kt @@ -85,7 +85,7 @@ class LoadAllGitLabMergeRequestTimelineViewModel( GitLabMergeRequestTimelineItemViewModel.SystemDiscussion(it) } else { - GitLabMergeRequestTimelineItemViewModel.Discussion(cs, LoadedGitLabDiscussion(it)) + GitLabMergeRequestTimelineItemViewModel.Discussion(cs, LoadedGitLabDiscussion(cs, connection, it)) } }) }