diff --git a/plugins/gitlab/resources/graphql/fragment/note.graphql b/plugins/gitlab/resources/graphql/fragment/note.graphql index 6a4deb8e44f0..34a3e6f1e41c 100644 --- a/plugins/gitlab/resources/graphql/fragment/note.graphql +++ b/plugins/gitlab/resources/graphql/fragment/note.graphql @@ -25,5 +25,6 @@ fragment note on Note { resolved userPermissions { resolveNote + adminNote } } \ No newline at end of file diff --git a/plugins/gitlab/resources/graphql/query/destroyNote.graphql b/plugins/gitlab/resources/graphql/query/destroyNote.graphql new file mode 100644 index 000000000000..a6b0481c274c --- /dev/null +++ b/plugins/gitlab/resources/graphql/query/destroyNote.graphql @@ -0,0 +1,5 @@ +mutation($noteId: NoteID!) { + destroyNote(input: {id: $noteId}) { + errors + } +} \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabApi.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabApi.kt index 3dfb801c72d4..b9744b391737 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabApi.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabApi.kt @@ -11,6 +11,7 @@ import com.intellij.collaboration.api.json.JsonHttpApiHelper import com.intellij.collaboration.api.json.loadJsonList import com.intellij.openapi.diagnostic.logger import com.intellij.util.io.HttpSecurityUtil +import org.jetbrains.plugins.gitlab.api.dto.GitLabGraphQLMutationResultDTO import java.net.http.HttpResponse class GitLabApi private constructor(httpHelper: HttpApiHelper) @@ -45,4 +46,13 @@ class GitLabApi private constructor(httpHelper: HttpApiHelper) requestConfigurer = requestConfigurer) } } +} + +@Throws(GitLabGraphQLMutationException::class) +fun HttpResponse.getResultOrThrow(): R { + val result = body() + if (result == null) throw GitLabGraphQLMutationEmptyResultException() + val errors = result.errors + if (errors != null) throw GitLabGraphQLMutationErrorException(errors) + return result } \ 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 ffd391a30db6..5d5b891419b1 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGQLQueries.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGQLQueries.kt @@ -7,4 +7,5 @@ object GitLabGQLQueries { const val getProjectLabels = "graphql/query/getProjectLabels.graphql" const val getMergeRequestDiscussions = "graphql/query/getMergeRequestDiscussions.graphql" const val toggleMergeRequestDiscussionResolve = "graphql/query/toggleMergeRequestDiscussionResolve.graphql" + const val destroyNote = "graphql/query/destroyNote.graphql" } \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationEmptyResultException.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationEmptyResultException.kt new file mode 100644 index 000000000000..712330a0ecb9 --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationEmptyResultException.kt @@ -0,0 +1,5 @@ +// 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.api + +class GitLabGraphQLMutationEmptyResultException + : GitLabGraphQLMutationException("Mutation returned empty result") \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationErrorException.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationErrorException.kt new file mode 100644 index 000000000000..ca305c2839dd --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationErrorException.kt @@ -0,0 +1,5 @@ +// 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.api + +class GitLabGraphQLMutationErrorException(val errors: List) + : GitLabGraphQLMutationException("Mutation execution returned errors: $errors") \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationException.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationException.kt new file mode 100644 index 000000000000..f8bc4cf00bee --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/GitLabGraphQLMutationException.kt @@ -0,0 +1,4 @@ +// 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.api + +abstract class GitLabGraphQLMutationException(message: String?) : Exception(message) \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/dto/GitLabGraphQLMutationResultDTO.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/dto/GitLabGraphQLMutationResultDTO.kt new file mode 100644 index 000000000000..debf710fc7fc --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/dto/GitLabGraphQLMutationResultDTO.kt @@ -0,0 +1,6 @@ +// 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.api.dto + +open class GitLabGraphQLMutationResultDTO( + val errors: List? +) \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/dto/GitLabNoteDTO.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/dto/GitLabNoteDTO.kt index 8949b73d6e06..00a91906f30e 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/dto/GitLabNoteDTO.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/api/dto/GitLabNoteDTO.kt @@ -34,6 +34,7 @@ data class GitLabNoteDTO( ) data class UserPermissions( - val resolveNote: Boolean + val resolveNote: Boolean, + val adminNote: Boolean ) } 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 index 0d0853b90f81..338e7f8ec178 100644 --- 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 @@ -10,6 +10,7 @@ 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.GitLabGraphQLMutationResultDTO import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestId import java.net.http.HttpResponse @@ -39,4 +40,15 @@ suspend fun GitLabApi.changeMergeRequestDiscussionResolve( ) val request = gqlQuery(project.serverPath.gqlApiUri, GitLabGQLQueries.toggleMergeRequestDiscussionResolve, parameters) return loadGQLResponse(request, GitLabDiscussionDTO::class.java, "discussionToggleResolve", "discussion") +} + +suspend fun GitLabApi.deleteNote( + project: GitLabProjectCoordinates, + noteId: String +): HttpResponse { + val parameters = mapOf( + "noteId" to noteId + ) + val request = gqlQuery(project.serverPath.gqlApiUri, GitLabGQLQueries.destroyNote, parameters) + return loadGQLResponse(request, GitLabGraphQLMutationResultDTO::class.java, "destroyNote") } \ No newline at end of file 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 75f28140fd73..b7c7185ff43d 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 @@ -3,19 +3,19 @@ package org.jetbrains.plugins.gitlab.mergerequest.data 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.* 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 id: String + val createdAt: Date val notes: Flow> @@ -25,15 +25,18 @@ interface GitLabDiscussion { suspend fun changeResolvedState() } +@OptIn(ExperimentalCoroutinesApi::class) class LoadedGitLabDiscussion( parentCs: CoroutineScope, private val connection: GitLabProjectConnection, + private val eventSink: suspend (GitLabDiscussionEvent) -> Unit, private val discussion: GitLabDiscussionDTO ) : GitLabDiscussion { init { require(discussion.notes.isNotEmpty()) { "Discussion with empty notes" } } + override val id: String = discussion.id override val createdAt: Date = discussion.createdAt private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e -> @@ -41,10 +44,38 @@ class LoadedGitLabDiscussion( }) private val operationsGuard = Mutex() + private val events = MutableSharedFlow() - private val loadedNotes = MutableStateFlow(discussion.notes) - override val notes: Flow> = loadedNotes.map { notes -> - notes.map { LoadedGitLabNote(it) } + private val loadedNotes = MutableSharedFlow>(1).apply { + tryEmit(discussion.notes) + } + + override val notes: Flow> = loadedNotes.transformLatest, List> { loadedNotes -> + coroutineScope { + val notesCs = this + val notes = Collections.synchronizedMap(LinkedHashMap()) + loadedNotes.associateByTo(notes, GitLabNoteDTO::id) { noteData -> + LoadedGitLabNote(notesCs, connection, { events.emit(it) }, noteData) + } + emit(notes.values.toList()) + + events.collectLatest { + when (it) { + is GitLabNoteEvent.NoteDeleted -> { + notes.remove(it.noteId)?.destroy() + } + } + + emit(notes.values.toList()) + } + awaitCancellation() + } + }.transform { + if (it.isEmpty()) { + eventSink(GitLabDiscussionEvent.DiscussionDeleted(id)) + currentCoroutineContext().cancel() + } + emit(it) }.shareIn(cs, SharingStarted.Lazily, 1) private val firstNote = loadedNotes.map { it.first() } @@ -61,8 +92,16 @@ class LoadedGitLabDiscussion( connection.apiClient .changeMergeRequestDiscussionResolve(connection.repo.repository, discussion.id, !resolved).body()!! } - loadedNotes.value = result.notes + updateNotes(result.notes) } } } + + private suspend fun updateNotes(notes: List) { + loadedNotes.emit(notes) + } + + suspend fun destroy() { + cs.coroutineContext[Job]!!.cancelAndJoin() + } } \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabDiscussionEvent.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabDiscussionEvent.kt new file mode 100644 index 000000000000..fa0f732c1e90 --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabDiscussionEvent.kt @@ -0,0 +1,6 @@ +// 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 + +sealed interface GitLabDiscussionEvent { + class DiscussionDeleted(val discussionId: String) : GitLabDiscussionEvent +} \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsModel.kt index 63bc349f98aa..832a450c39bf 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsModel.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabMergeRequestDiscussionsModel.kt @@ -3,20 +3,21 @@ package org.jetbrains.plugins.gitlab.mergerequest.data import com.intellij.collaboration.api.page.ApiPageUtil import com.intellij.collaboration.api.page.foldToList -import com.intellij.collaboration.async.mapScoped import com.intellij.util.childScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.jetbrains.plugins.gitlab.api.GitLabProjectConnection import org.jetbrains.plugins.gitlab.api.dto.GitLabDiscussionDTO import org.jetbrains.plugins.gitlab.mergerequest.api.request.loadMergeRequestDiscussions +import java.util.concurrent.ConcurrentHashMap +//TODO: granualar collection changes notifications interface GitLabMergeRequestDiscussionsModel { - val userDiscussions: Flow> - val systemDiscussions: Flow> + val userDiscussions: Flow> + val systemDiscussions: Flow> } +@OptIn(ExperimentalCoroutinesApi::class) class GitLabMergeRequestDiscussionsModelImpl( parentCs: CoroutineScope, private val connection: GitLabProjectConnection, @@ -24,25 +25,43 @@ class GitLabMergeRequestDiscussionsModelImpl( ) : GitLabMergeRequestDiscussionsModel { private val cs = parentCs.childScope(Dispatchers.Default) + private val events = MutableSharedFlow(extraBufferCapacity = 64) private val nonEmptyDiscussionsData = flow { + emit(loadNonEmptyDiscussions()) + }.shareIn(cs, SharingStarted.Lazily, 1) + + override val userDiscussions: Flow> = nonEmptyDiscussionsData.transformLatest { loadedDiscussions -> + coroutineScope { + val discussionsCs = this + val discussions = ConcurrentHashMap() + loadedDiscussions.associateByTo(discussions, GitLabDiscussionDTO::id) { noteData -> + LoadedGitLabDiscussion(discussionsCs, connection, { events.emit(it) }, noteData) + } + emit(discussions.values.toList()) + + events.collectLatest { + when (it) { + is GitLabDiscussionEvent.DiscussionDeleted -> { + discussions.remove(it.discussionId)?.destroy() + } + } + emit(discussions.values.toList()) + } + + awaitCancellation() + } + }.shareIn(cs, SharingStarted.Lazily, 1) + + override val systemDiscussions: Flow> = + nonEmptyDiscussionsData.map { discussions -> + discussions.filter { it.notes.first().system } + }.shareIn(cs, SharingStarted.Lazily, 1) + + private suspend fun loadNonEmptyDiscussions(): List = ApiPageUtil.createGQLPagesFlow { connection.apiClient.loadMergeRequestDiscussions(connection.repo.repository, mr, it) }.map { discussions -> discussions.filter { it.notes.isNotEmpty() } }.foldToList() - .let { emit(it) } - }.shareIn(cs, SharingStarted.Lazily, 1) - - override val userDiscussions: Flow> = - nonEmptyDiscussionsData.mapScoped { discussions -> - val cs = this - discussions.filter { !it.notes.first().system } - .map { LoadedGitLabDiscussion(cs, connection, it) } - } - - override val systemDiscussions: Flow> = - nonEmptyDiscussionsData.map { discussions -> - discussions.filter { it.notes.first().system } - } } \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt index 4f4ed52c1a0d..cee13a8d0ba0 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNote.kt @@ -1,25 +1,63 @@ // 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 com.intellij.openapi.diagnostic.logger +import com.intellij.util.childScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.jetbrains.plugins.gitlab.api.GitLabProjectConnection import org.jetbrains.plugins.gitlab.api.dto.GitLabNoteDTO import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO +import org.jetbrains.plugins.gitlab.api.getResultOrThrow +import org.jetbrains.plugins.gitlab.mergerequest.api.request.deleteNote import java.util.* interface GitLabNote { val author: GitLabUserDTO val createdAt: Date + val canAdmin: Boolean val body: Flow + + suspend fun delete() } class LoadedGitLabNote( - note: GitLabNoteDTO + parentCs: CoroutineScope, + private val connection: GitLabProjectConnection, + private val eventSink: suspend (GitLabNoteEvent) -> Unit, + private val note: GitLabNoteDTO ) : GitLabNote { + private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e -> + logger().info(e.localizedMessage) + }) + + private val operationsGuard = Mutex() + override val author: GitLabUserDTO = note.author override val createdAt: Date = note.createdAt + override val canAdmin: Boolean = note.userPermissions.adminNote override val body: Flow = flowOf(note.body) + + override suspend fun delete() { + withContext(cs.coroutineContext) { + operationsGuard.withLock { + withContext(Dispatchers.IO) { + connection.apiClient.deleteNote(connection.repo.repository, note.id).getResultOrThrow() + } + } + withContext(NonCancellable) { + eventSink(GitLabNoteEvent.NoteDeleted(note.id)) + } + } + } + + suspend fun destroy() { + cs.coroutineContext[Job]!!.cancelAndJoin() + } } diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNoteEvent.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNoteEvent.kt new file mode 100644 index 000000000000..ce76e3228f39 --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabNoteEvent.kt @@ -0,0 +1,6 @@ +// 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 + +sealed interface GitLabNoteEvent { + class NoteDeleted(val noteId: String) : GitLabNoteEvent +} \ No newline at end of file diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestNoteActionsViewModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestNoteActionsViewModel.kt new file mode 100644 index 000000000000..1cb30774a736 --- /dev/null +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestNoteActionsViewModel.kt @@ -0,0 +1,34 @@ +// 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.flow.Flow +import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabNote +import org.jetbrains.plugins.gitlab.util.SingleCoroutineLauncher + +interface GitLabMergeRequestNoteActionsViewModel { + val busy: Flow + + fun delete() +} + +class GitLabMergeRequestNoteActionsViewModelImpl(parentCs: CoroutineScope, private val note: GitLabNote) + : GitLabMergeRequestNoteActionsViewModel { + + private val taskLauncher = SingleCoroutineLauncher(parentCs.childScope()) + override val busy: Flow = taskLauncher.busy + + override fun delete() { + taskLauncher.launch { + try { + note.delete() + } + catch (e: Exception) { + if (e is CancellationException) throw e + //TODO: handle??? + } + } + } +} diff --git a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestNoteViewModel.kt b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestNoteViewModel.kt index 07a189cef3b9..4fb64ae5983c 100644 --- a/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestNoteViewModel.kt +++ b/plugins/gitlab/src/org/jetbrains/plugins/gitlab/mergerequest/ui/comment/GitLabMergeRequestNoteViewModel.kt @@ -18,6 +18,8 @@ interface GitLabMergeRequestNoteViewModel { val author: GitLabUserDTO val createdAt: Date + val actionsVm: GitLabMergeRequestNoteActionsViewModel? + val htmlBody: Flow<@Nls String> } @@ -31,6 +33,9 @@ class GitLabMergeRequestNoteViewModelImpl( override val author: GitLabUserDTO = note.author override val createdAt: Date = note.createdAt + override val actionsVm: GitLabMergeRequestNoteActionsViewModel? = + if (note.canAdmin) GitLabMergeRequestNoteActionsViewModelImpl(cs, note) else null + private val body: Flow = note.body override val htmlBody: Flow = body.map { GitLabUIUtil.convertToHtml(it) } .shareIn(cs, SharingStarted.Lazily, 1) 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 1d3710636211..f6675b225ee4 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 @@ -8,9 +8,13 @@ import com.intellij.collaboration.ui.SimpleHtmlPane import com.intellij.collaboration.ui.VerticalListPanel import com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil import com.intellij.collaboration.ui.codereview.CodeReviewTimelineUIUtil.Thread.Replies +import com.intellij.collaboration.ui.codereview.comment.CodeReviewCommentUIUtil import com.intellij.collaboration.ui.icon.IconsProvider import com.intellij.collaboration.ui.icon.OverlaidOffsetIconsIcon -import com.intellij.collaboration.ui.util.* +import com.intellij.collaboration.ui.util.bindDisabled +import com.intellij.collaboration.ui.util.bindIcon +import com.intellij.collaboration.ui.util.bindText +import com.intellij.collaboration.ui.util.bindVisibility import com.intellij.ui.components.labels.LinkLabel import com.intellij.ui.components.labels.LinkListener import com.intellij.util.containers.nullize @@ -44,6 +48,8 @@ object GitLabMergeRequestTimelineDiscussionComponentFactory { } } + val actionsPanel = createNoteActions(cs, item.mainNote) + val repliesPanel = VerticalListPanel().apply { cs.launch { item.replies.combine(item.repliesFolded) { replies, folded -> @@ -69,7 +75,7 @@ object GitLabMergeRequestTimelineDiscussionComponentFactory { return CodeReviewChatItemUIUtil.build(CodeReviewChatItemUIUtil.ComponentType.FULL, { avatarIconsProvider.getIcon(constAuthor, it) }, contentPanel) { - withHeader(createTitleTextPane(cs, item.author, item.date)) + withHeader(createTitleTextPane(cs, item.author, item.date), actionsPanel) }.let { VerticalListPanel().apply { add(it) @@ -159,13 +165,30 @@ object GitLabMergeRequestTimelineDiscussionComponentFactory { avatarIconsProvider: IconsProvider, vm: GitLabMergeRequestNoteViewModel): JComponent { val contentPanel = createNoteTextPanel(cs, vm.htmlBody) + val actionsPanel = createNoteActions(cs, flowOf(vm)) return CodeReviewChatItemUIUtil.build(CodeReviewChatItemUIUtil.ComponentType.FULL_SECONDARY, { avatarIconsProvider.getIcon(vm.author, it) }, contentPanel) { - withHeader(createTitleTextPane(vm.author, vm.createdAt)) + withHeader(createTitleTextPane(vm.author, vm.createdAt), actionsPanel) } } + private fun createNoteActions(cs: CoroutineScope, note: Flow): JComponent { + val panel = HorizontalListPanel(CodeReviewCommentUIUtil.Actions.HORIZONTAL_GAP).apply { + cs.launch { + note.mapNotNull { it.actionsVm }.collect { + removeAll() + CodeReviewCommentUIUtil.createDeleteCommentIconButton { _ -> it.delete() }.apply { + bindDisabled(cs, it.busy) + }.also(::add) + repaint() + revalidate() + } + } + } + return panel + } + private fun createNoteTextPanel(cs: CoroutineScope, textFlow: Flow<@Nls String>): JComponent = SimpleHtmlPane().apply { bindText(cs, textFlow)