mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 06:50:54 +07:00
[gitlab] avoid timeline relayout on model changes
GitOrigin-RevId: d18d5df2997b8d0298fa44dcd9a90d75e57e5ca8
This commit is contained in:
committed by
intellij-monorepo-bot
parent
de3ff8eea8
commit
df744a30eb
@@ -2,6 +2,7 @@
|
||||
package com.intellij.collaboration.async
|
||||
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.childScope
|
||||
import kotlinx.coroutines.*
|
||||
@@ -145,4 +146,52 @@ suspend fun <T> Flow<T>.collectWithPrevious(initial: T, collector: suspend (prev
|
||||
collector(prev, it)
|
||||
prev = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy shared flow that logs all exceptions as errors and never throws (beside cancellation)
|
||||
*/
|
||||
fun <T> Flow<T>.modelFlow(cs: CoroutineScope, log: Logger): SharedFlow<T> =
|
||||
catch { log.error(it) }.shareIn(cs, SharingStarted.Lazily, 1)
|
||||
|
||||
fun <ID : Any, T, R> Flow<List<T>>.mapCaching(sourceIdentifier: (T) -> ID,
|
||||
mapper: suspend (T) -> R,
|
||||
destroy: suspend R.() -> Unit,
|
||||
update: (suspend R.(T) -> Unit)? = null): Flow<List<R>> {
|
||||
var initial = true
|
||||
val result = LinkedHashMap<ID, R>()
|
||||
return transform { items ->
|
||||
var hasStructureChanges = false
|
||||
val itemsById = items.associateBy(sourceIdentifier)
|
||||
|
||||
// remove missing
|
||||
val iter = result.iterator()
|
||||
while (iter.hasNext()) {
|
||||
val (key, exisingResult) = iter.next()
|
||||
if (!itemsById.containsKey(key)) {
|
||||
iter.remove()
|
||||
hasStructureChanges = true
|
||||
exisingResult.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// add new or update existing
|
||||
for (item in items) {
|
||||
val id = sourceIdentifier(item)
|
||||
|
||||
val existing = result[id]
|
||||
if (existing != null && update != null) {
|
||||
existing.update(item)
|
||||
}
|
||||
else {
|
||||
result[id] = mapper(item)
|
||||
hasStructureChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStructureChanges || initial) {
|
||||
initial = false
|
||||
emit(result.values.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.collaboration.ui
|
||||
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.ui.ClientProperty
|
||||
import com.intellij.util.childScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.*
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ListModel
|
||||
@@ -46,4 +52,76 @@ object ComponentListPanelFactory {
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
// NOTE: new items are ALWAYS added to the end
|
||||
fun <T : Any> createVertical(parentCs: CoroutineScope,
|
||||
items: Flow<List<T>>,
|
||||
itemKeyExtractor: ((T) -> Any),
|
||||
gap: Int,
|
||||
componentFactory: suspend (CoroutineScope, T) -> JComponent): JPanel {
|
||||
val cs = parentCs.childScope(Dispatchers.Main)
|
||||
val panel = VerticalListPanel(gap)
|
||||
val keyList = LinkedList<Any>()
|
||||
|
||||
suspend fun addComponent(idx: Int, item: T) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val scope = cs.childScope()
|
||||
val component = componentFactory(scope, item).also {
|
||||
ClientProperty.put(it, COMPONENT_SCOPE_KEY, scope)
|
||||
}
|
||||
panel.add(component, idx)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeComponent(idx: Int) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
val component = panel.getComponent(idx)
|
||||
val componentCs = ClientProperty.get(component, COMPONENT_SCOPE_KEY)
|
||||
cs.launch {
|
||||
componentCs?.coroutineContext?.get(Job)?.cancelAndJoin()
|
||||
}
|
||||
panel.remove(idx)
|
||||
}
|
||||
}
|
||||
|
||||
cs.launch(Dispatchers.Default, start = CoroutineStart.UNDISPATCHED) {
|
||||
items.collect { items ->
|
||||
val itemsByKey = items.associateBy(itemKeyExtractor)
|
||||
|
||||
// remove missing
|
||||
val iter = keyList.iterator()
|
||||
var keyIdx = 0
|
||||
while (iter.hasNext()) {
|
||||
val key = iter.next()
|
||||
if (!itemsByKey.containsKey(key)) {
|
||||
iter.remove()
|
||||
removeComponent(keyIdx)
|
||||
}
|
||||
else {
|
||||
keyIdx++
|
||||
}
|
||||
}
|
||||
|
||||
//add new
|
||||
val keySet = keyList.toMutableSet()
|
||||
for (item in items) {
|
||||
val key = itemKeyExtractor(item)
|
||||
if (keySet.contains(key)) continue
|
||||
|
||||
val idx = keyList.size
|
||||
keyList.add(key)
|
||||
keySet.add(key)
|
||||
addComponent(idx, item)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
panel.revalidate()
|
||||
panel.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
return panel
|
||||
}
|
||||
|
||||
private val COMPONENT_SCOPE_KEY = Key.create<CoroutineScope>("ComponentListPanelFactory.Component.Scope")
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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.collaboration.async.mapCaching
|
||||
import com.intellij.collaboration.async.modelFlow
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.util.childScope
|
||||
import kotlinx.coroutines.*
|
||||
@@ -26,64 +28,67 @@ interface GitLabDiscussion {
|
||||
suspend fun changeResolvedState()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val LOG = logger<GitLabDiscussion>()
|
||||
|
||||
class LoadedGitLabDiscussion(
|
||||
parentCs: CoroutineScope,
|
||||
private val connection: GitLabProjectConnection,
|
||||
private val eventSink: suspend (GitLabDiscussionEvent) -> Unit,
|
||||
private val discussion: GitLabDiscussionDTO
|
||||
private val discussionData: GitLabDiscussionDTO
|
||||
) : GitLabDiscussion {
|
||||
init {
|
||||
require(discussion.notes.isNotEmpty()) { "Discussion with empty notes" }
|
||||
require(discussionData.notes.isNotEmpty()) { "Discussion with empty notes" }
|
||||
}
|
||||
|
||||
override val id: String = discussion.id
|
||||
override val createdAt: Date = discussion.createdAt
|
||||
override val id: String = discussionData.id
|
||||
override val createdAt: Date = discussionData.createdAt
|
||||
|
||||
private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e ->
|
||||
logger<GitLabDiscussion>().info(e.localizedMessage)
|
||||
})
|
||||
private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e -> LOG.warn(e) })
|
||||
|
||||
private val operationsGuard = Mutex()
|
||||
private val events = MutableSharedFlow<GitLabNoteEvent>()
|
||||
|
||||
private val loadedNotes = MutableSharedFlow<List<GitLabNoteDTO>>(1).apply {
|
||||
tryEmit(discussion.notes)
|
||||
}
|
||||
private val noteEvents = MutableSharedFlow<GitLabNoteEvent>()
|
||||
private val loadedNotes = channelFlow {
|
||||
val notesData = discussionData.notes.toMutableList()
|
||||
|
||||
override val notes: Flow<List<GitLabNote>> = loadedNotes.transformLatest<List<GitLabNoteDTO>, List<GitLabNote>> { loadedNotes ->
|
||||
coroutineScope {
|
||||
val notesCs = this
|
||||
val notes = Collections.synchronizedMap(LinkedHashMap<String, LoadedGitLabNote>())
|
||||
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()
|
||||
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
noteEvents.collectLatest { event ->
|
||||
when (event) {
|
||||
is GitLabNoteEvent.Deleted -> notesData.removeIf { it.id == event.noteId }
|
||||
is GitLabNoteEvent.NotesChanged -> {
|
||||
notesData.clear()
|
||||
notesData.addAll(event.notes)
|
||||
}
|
||||
}
|
||||
|
||||
emit(notes.values.toList())
|
||||
}
|
||||
awaitCancellation()
|
||||
}
|
||||
}.transform {
|
||||
if (it.isEmpty()) {
|
||||
eventSink(GitLabDiscussionEvent.DiscussionDeleted(id))
|
||||
currentCoroutineContext().cancel()
|
||||
}
|
||||
emit(it)
|
||||
}.shareIn(cs, SharingStarted.Lazily, 1)
|
||||
if (notesData.isEmpty()) {
|
||||
eventSink(GitLabDiscussionEvent.Deleted(discussionData.id))
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
private val firstNote = loadedNotes.map { it.first() }
|
||||
send(Collections.unmodifiableList(notesData))
|
||||
}
|
||||
}
|
||||
send(Collections.unmodifiableList(notesData))
|
||||
}.modelFlow(cs, LOG)
|
||||
|
||||
override val notes: Flow<List<GitLabNote>> =
|
||||
loadedNotes
|
||||
.mapCaching(
|
||||
GitLabNoteDTO::id,
|
||||
{ LoadedGitLabNote(cs, connection, { noteEvents.emit(it) }, it) },
|
||||
LoadedGitLabNote::destroy,
|
||||
LoadedGitLabNote::update
|
||||
)
|
||||
.modelFlow(cs, LOG)
|
||||
|
||||
// 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 val canResolve: Boolean = discussionData.notes.first().let { it.resolvable && it.userPermissions.resolveNote }
|
||||
override val resolved: Flow<Boolean> =
|
||||
loadedNotes
|
||||
.map { it.first().resolved }
|
||||
.distinctUntilChanged()
|
||||
.modelFlow(cs, LOG)
|
||||
|
||||
override suspend fun changeResolvedState() {
|
||||
withContext(cs.coroutineContext) {
|
||||
@@ -91,18 +96,21 @@ class LoadedGitLabDiscussion(
|
||||
val resolved = resolved.first()
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
connection.apiClient
|
||||
.changeMergeRequestDiscussionResolve(connection.repo.repository, discussion.id, !resolved).getResultOrThrow()
|
||||
.changeMergeRequestDiscussionResolve(connection.repo.repository, discussionData.id, !resolved).getResultOrThrow()
|
||||
}
|
||||
updateNotes(result.notes)
|
||||
noteEvents.emit(GitLabNoteEvent.NotesChanged(result.notes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateNotes(notes: List<GitLabNoteDTO>) {
|
||||
loadedNotes.emit(notes)
|
||||
suspend fun destroy() {
|
||||
try {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
}
|
||||
catch (e: CancellationException) {
|
||||
// ignore, cuz we don't want to cancel the invoker
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun destroy() {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
}
|
||||
override fun toString(): String = "LoadedGitLabDiscussion(id='$id', createdAt=$createdAt, canResolve=$canResolve)"
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
package org.jetbrains.plugins.gitlab.mergerequest.data
|
||||
|
||||
sealed interface GitLabDiscussionEvent {
|
||||
class DiscussionDeleted(val discussionId: String) : GitLabDiscussionEvent
|
||||
class Deleted(val discussionId: String) : GitLabDiscussionEvent
|
||||
}
|
||||
@@ -3,60 +3,60 @@ 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.mapCaching
|
||||
import com.intellij.collaboration.async.modelFlow
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.util.childScope
|
||||
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<Collection<GitLabDiscussion>>
|
||||
val systemDiscussions: Flow<Collection<GitLabDiscussionDTO>>
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val LOG = logger<GitLabMergeRequestDiscussionsModel>()
|
||||
|
||||
class GitLabMergeRequestDiscussionsModelImpl(
|
||||
parentCs: CoroutineScope,
|
||||
private val connection: GitLabProjectConnection,
|
||||
private val mr: GitLabMergeRequestId
|
||||
) : GitLabMergeRequestDiscussionsModel {
|
||||
|
||||
private val cs = parentCs.childScope(Dispatchers.Default)
|
||||
private val events = MutableSharedFlow<GitLabDiscussionEvent>(extraBufferCapacity = 64)
|
||||
private val cs = parentCs.childScope(Dispatchers.Default + CoroutineExceptionHandler { _, e -> LOG.warn(e) })
|
||||
|
||||
private val nonEmptyDiscussionsData = flow {
|
||||
emit(loadNonEmptyDiscussions())
|
||||
}.shareIn(cs, SharingStarted.Lazily, 1)
|
||||
|
||||
override val userDiscussions: Flow<Collection<GitLabDiscussion>> = nonEmptyDiscussionsData.transformLatest { loadedDiscussions ->
|
||||
coroutineScope {
|
||||
val discussionsCs = this
|
||||
val discussions = ConcurrentHashMap<String, LoadedGitLabDiscussion>()
|
||||
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()
|
||||
private val discussionEvents = MutableSharedFlow<GitLabDiscussionEvent>()
|
||||
private val nonEmptyDiscussionsData: Flow<List<GitLabDiscussionDTO>> =
|
||||
channelFlow {
|
||||
val discussions = loadNonEmptyDiscussions().toMutableList()
|
||||
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
discussionEvents.collectLatest { e ->
|
||||
when (e) {
|
||||
is GitLabDiscussionEvent.Deleted -> discussions.removeIf { it.id == e.discussionId }
|
||||
}
|
||||
send(discussions)
|
||||
}
|
||||
emit(discussions.values.toList())
|
||||
}
|
||||
send(discussions)
|
||||
}.modelFlow(cs, LOG)
|
||||
|
||||
awaitCancellation()
|
||||
}
|
||||
}.shareIn(cs, SharingStarted.Lazily, 1)
|
||||
override val userDiscussions: Flow<List<GitLabDiscussion>> =
|
||||
nonEmptyDiscussionsData
|
||||
.mapFiltered { !it.notes.first().system }
|
||||
.mapCaching(
|
||||
GitLabDiscussionDTO::id,
|
||||
{ LoadedGitLabDiscussion(cs, connection, { discussionEvents.emit(it) }, it) },
|
||||
LoadedGitLabDiscussion::destroy
|
||||
)
|
||||
.modelFlow(cs, LOG)
|
||||
|
||||
override val systemDiscussions: Flow<List<GitLabDiscussionDTO>> =
|
||||
nonEmptyDiscussionsData.map { discussions ->
|
||||
discussions.filter { it.notes.first().system }
|
||||
}.shareIn(cs, SharingStarted.Lazily, 1)
|
||||
nonEmptyDiscussionsData
|
||||
.mapFiltered { it.notes.first().system }
|
||||
.modelFlow(cs, LOG)
|
||||
|
||||
private suspend fun loadNonEmptyDiscussions(): List<GitLabDiscussionDTO> =
|
||||
ApiPageUtil.createGQLPagesFlow {
|
||||
@@ -64,4 +64,6 @@ class GitLabMergeRequestDiscussionsModelImpl(
|
||||
}.map { discussions ->
|
||||
discussions.filter { it.notes.isNotEmpty() }
|
||||
}.foldToList()
|
||||
|
||||
private fun <T> Flow<List<T>>.mapFiltered(predicate: (T) -> Boolean): Flow<List<T>> = map { it.filter(predicate) }
|
||||
}
|
||||
@@ -18,46 +18,48 @@ import org.jetbrains.plugins.gitlab.mergerequest.api.request.updateNote
|
||||
import java.util.*
|
||||
|
||||
interface GitLabNote {
|
||||
val id: String
|
||||
val author: GitLabUserDTO
|
||||
val createdAt: Date
|
||||
val canAdmin: Boolean
|
||||
|
||||
val body: StateFlow<String>
|
||||
|
||||
suspend fun updateBody(newText: String)
|
||||
suspend fun setBody(newText: String)
|
||||
suspend fun delete()
|
||||
|
||||
suspend fun update(item: GitLabNoteDTO)
|
||||
}
|
||||
|
||||
private val LOG = logger<GitLabDiscussion>()
|
||||
|
||||
class LoadedGitLabNote(
|
||||
parentCs: CoroutineScope,
|
||||
private val connection: GitLabProjectConnection,
|
||||
private val eventSink: suspend (GitLabNoteEvent) -> Unit,
|
||||
private val note: GitLabNoteDTO
|
||||
private val noteData: GitLabNoteDTO
|
||||
) : GitLabNote {
|
||||
|
||||
private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e ->
|
||||
logger<GitLabDiscussion>().info(e.localizedMessage)
|
||||
})
|
||||
private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e -> LOG.warn(e) })
|
||||
|
||||
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 id: String = noteData.id
|
||||
override val author: GitLabUserDTO = noteData.author
|
||||
override val createdAt: Date = noteData.createdAt
|
||||
override val canAdmin: Boolean = noteData.userPermissions.adminNote
|
||||
|
||||
private val _body = MutableStateFlow(note.body)
|
||||
private val _body = MutableStateFlow(noteData.body)
|
||||
override val body: StateFlow<String> = _body.asStateFlow()
|
||||
|
||||
override suspend fun updateBody(newText: String) {
|
||||
override suspend fun setBody(newText: String) {
|
||||
withContext(cs.coroutineContext) {
|
||||
operationsGuard.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
connection.apiClient.updateNote(connection.repo.repository, note.id, newText).getResultOrThrow()
|
||||
connection.apiClient.updateNote(connection.repo.repository, noteData.id, newText).getResultOrThrow()
|
||||
}
|
||||
}
|
||||
withContext(NonCancellable) {
|
||||
_body.value = newText
|
||||
}
|
||||
_body.value = newText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,16 +67,26 @@ class LoadedGitLabNote(
|
||||
withContext(cs.coroutineContext) {
|
||||
operationsGuard.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
connection.apiClient.deleteNote(connection.repo.repository, note.id).getResultOrThrow()
|
||||
connection.apiClient.deleteNote(connection.repo.repository, noteData.id).getResultOrThrow()
|
||||
}
|
||||
}
|
||||
withContext(NonCancellable) {
|
||||
eventSink(GitLabNoteEvent.NoteDeleted(note.id))
|
||||
}
|
||||
eventSink(GitLabNoteEvent.Deleted(noteData.id))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun destroy() {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
override suspend fun update(item: GitLabNoteDTO) {
|
||||
_body.value = item.body
|
||||
}
|
||||
|
||||
suspend fun destroy() {
|
||||
try {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
}
|
||||
catch (ce: CancellationException) {
|
||||
// ignore, cuz we don't want to cancel the invoker
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
"LoadedGitLabNote(id='$id', author=$author, createdAt=$createdAt, body=${body.value})"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// 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 org.jetbrains.plugins.gitlab.api.dto.GitLabNoteDTO
|
||||
|
||||
sealed interface GitLabNoteEvent {
|
||||
class NoteDeleted(val noteId: String) : GitLabNoteEvent
|
||||
class NotesChanged(val notes: List<GitLabNoteDTO>) : GitLabNoteEvent
|
||||
class Deleted(val noteId: String) : GitLabNoteEvent
|
||||
}
|
||||
@@ -2,10 +2,7 @@
|
||||
package org.jetbrains.plugins.gitlab.mergerequest.ui.timeline
|
||||
|
||||
import com.intellij.collaboration.async.mapScoped
|
||||
import com.intellij.collaboration.ui.CollaborationToolsUIUtil
|
||||
import com.intellij.collaboration.ui.SimpleHtmlPane
|
||||
import com.intellij.collaboration.ui.TransparentScrollPane
|
||||
import com.intellij.collaboration.ui.VerticalListPanel
|
||||
import com.intellij.collaboration.ui.*
|
||||
import com.intellij.collaboration.ui.codereview.CodeReviewChatItemUIUtil
|
||||
import com.intellij.collaboration.ui.codereview.timeline.StatusMessageComponentFactory
|
||||
import com.intellij.collaboration.ui.codereview.timeline.StatusMessageType
|
||||
@@ -51,11 +48,8 @@ object GitLabMergeRequestTimelineComponentFactory {
|
||||
SimpleHtmlPane(state.exception.localizedMessage)
|
||||
}
|
||||
is LoadingState.Result -> {
|
||||
val itemScope = this
|
||||
VerticalListPanel(0).apply {
|
||||
for (item in state.items) {
|
||||
add(createItemComponent(project, itemScope, avatarIconsProvider, item))
|
||||
}
|
||||
ComponentListPanelFactory.createVertical(this, state.items, GitLabMergeRequestTimelineItemViewModel::id, 0) { cs, item ->
|
||||
createItemComponent(project, cs, avatarIconsProvider, item)
|
||||
}.let {
|
||||
TransparentScrollPane(it)
|
||||
}
|
||||
@@ -70,7 +64,7 @@ object GitLabMergeRequestTimelineComponentFactory {
|
||||
|
||||
return panel.also {
|
||||
UiNotifyConnector.doWhenFirstShown(it) {
|
||||
vm.startLoading()
|
||||
vm.requestLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,12 +75,13 @@ object GitLabMergeRequestTimelineComponentFactory {
|
||||
item: GitLabMergeRequestTimelineItemViewModel): JComponent =
|
||||
when (item) {
|
||||
is GitLabMergeRequestTimelineItemViewModel.Immutable -> {
|
||||
val content = createContent(item)
|
||||
val immutatebleItem = item.item
|
||||
val content = createContent(immutatebleItem)
|
||||
|
||||
CodeReviewChatItemUIUtil.build(CodeReviewChatItemUIUtil.ComponentType.FULL,
|
||||
{ avatarIconsProvider.getIcon(item.actor, it) },
|
||||
{ avatarIconsProvider.getIcon(immutatebleItem.actor, it) },
|
||||
content) {
|
||||
withHeader(createTitleTextPane(item.actor, item.date))
|
||||
withHeader(createTitleTextPane(immutatebleItem.actor, immutatebleItem.date))
|
||||
}
|
||||
}
|
||||
is GitLabMergeRequestTimelineItemViewModel.Discussion -> {
|
||||
@@ -94,15 +89,15 @@ object GitLabMergeRequestTimelineComponentFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createContent(item: GitLabMergeRequestTimelineItemViewModel.Immutable): JComponent =
|
||||
private fun createContent(item: GitLabMergeRequestTimelineItem.Immutable): JComponent =
|
||||
when (item) {
|
||||
is GitLabMergeRequestTimelineItemViewModel.SystemDiscussion -> createSystemDiscussionContent(item)
|
||||
is GitLabMergeRequestTimelineItemViewModel.LabelEvent -> createLabeledEventContent(item)
|
||||
is GitLabMergeRequestTimelineItemViewModel.MilestoneEvent -> createMilestonedEventContent(item)
|
||||
is GitLabMergeRequestTimelineItemViewModel.StateEvent -> createStateChangeContent(item)
|
||||
is GitLabMergeRequestTimelineItem.SystemDiscussion -> createSystemDiscussionContent(item)
|
||||
is GitLabMergeRequestTimelineItem.LabelEvent -> createLabeledEventContent(item)
|
||||
is GitLabMergeRequestTimelineItem.MilestoneEvent -> createMilestonedEventContent(item)
|
||||
is GitLabMergeRequestTimelineItem.StateEvent -> createStateChangeContent(item)
|
||||
}
|
||||
|
||||
private fun createSystemDiscussionContent(item: GitLabMergeRequestTimelineItemViewModel.SystemDiscussion): JComponent {
|
||||
private fun createSystemDiscussionContent(item: GitLabMergeRequestTimelineItem.SystemDiscussion): JComponent {
|
||||
val content = item.content
|
||||
if (content.contains("Compare with previous version")) {
|
||||
try {
|
||||
@@ -121,7 +116,7 @@ object GitLabMergeRequestTimelineComponentFactory {
|
||||
return StatusMessageComponentFactory.create(SimpleHtmlPane(GitLabUIUtil.convertToHtml(content)))
|
||||
}
|
||||
|
||||
private fun createLabeledEventContent(item: GitLabMergeRequestTimelineItemViewModel.LabelEvent): JComponent {
|
||||
private fun createLabeledEventContent(item: GitLabMergeRequestTimelineItem.LabelEvent): JComponent {
|
||||
val text = when (item.event.actionEnum) {
|
||||
GitLabResourceLabelEventDTO.Action.ADD -> GitLabBundle.message("merge.request.event.label.added", item.event.label.toHtml())
|
||||
GitLabResourceLabelEventDTO.Action.REMOVE -> GitLabBundle.message("merge.request.event.label.removed", item.event.label.toHtml())
|
||||
@@ -130,7 +125,7 @@ object GitLabMergeRequestTimelineComponentFactory {
|
||||
return StatusMessageComponentFactory.create(textPane)
|
||||
}
|
||||
|
||||
private fun createMilestonedEventContent(item: GitLabMergeRequestTimelineItemViewModel.MilestoneEvent): JComponent {
|
||||
private fun createMilestonedEventContent(item: GitLabMergeRequestTimelineItem.MilestoneEvent): JComponent {
|
||||
val text = when (item.event.actionEnum) {
|
||||
GitLabResourceMilestoneEventDTO.Action.ADD ->
|
||||
GitLabBundle.message("merge.request.event.milestone.changed", item.event.milestone.toHtml())
|
||||
@@ -141,7 +136,7 @@ object GitLabMergeRequestTimelineComponentFactory {
|
||||
return StatusMessageComponentFactory.create(textPane)
|
||||
}
|
||||
|
||||
private fun createStateChangeContent(item: GitLabMergeRequestTimelineItemViewModel.StateEvent): JComponent {
|
||||
private fun createStateChangeContent(item: GitLabMergeRequestTimelineItem.StateEvent): JComponent {
|
||||
val text = when (item.event.stateEnum) {
|
||||
GitLabResourceStateEventDTO.State.CLOSED -> GitLabBundle.message("merge.request.event.closed")
|
||||
GitLabResourceStateEventDTO.State.REOPENED -> GitLabBundle.message("merge.request.event.reopened")
|
||||
|
||||
@@ -40,12 +40,12 @@ object GitLabMergeRequestTimelineDiscussionComponentFactory {
|
||||
fun create(project: Project,
|
||||
cs: CoroutineScope,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
item: GitLabMergeRequestTimelineItemViewModel.Discussion): JComponent {
|
||||
val repliesActionsPanel = createRepliesActionsPanel(cs, avatarIconsProvider, item).apply {
|
||||
discussion: GitLabMergeRequestTimelineDiscussionViewModel): JComponent {
|
||||
val repliesActionsPanel = createRepliesActionsPanel(cs, avatarIconsProvider, discussion).apply {
|
||||
border = JBUI.Borders.empty(Replies.ActionsFolded.VERTICAL_PADDING, 0)
|
||||
bindVisibility(cs, item.repliesFolded)
|
||||
bindVisibility(cs, discussion.repliesFolded)
|
||||
}
|
||||
val mainNoteVm = item.mainNote
|
||||
val mainNoteVm = discussion.mainNote
|
||||
val textPanel = createNoteTextPanel(cs, mainNoteVm.flatMapLatest { it.htmlBody })
|
||||
|
||||
// oh well... probably better to make a suitable API in EditableComponentFactory, but that would look ugly
|
||||
@@ -66,30 +66,16 @@ object GitLabMergeRequestTimelineDiscussionComponentFactory {
|
||||
|
||||
val actionsPanel = createNoteActions(cs, mainNoteVm)
|
||||
|
||||
val repliesPanel = VerticalListPanel().apply {
|
||||
cs.launch {
|
||||
item.replies.combine(item.repliesFolded) { replies, folded ->
|
||||
if (!folded) replies else emptyList()
|
||||
}.collectLatest { notes ->
|
||||
coroutineScope {
|
||||
val notesScope = this
|
||||
removeAll()
|
||||
notes.forEach {
|
||||
add(createNoteItem(project, notesScope, avatarIconsProvider, it))
|
||||
}
|
||||
revalidate()
|
||||
repaint()
|
||||
awaitCancellation()
|
||||
}
|
||||
}
|
||||
}
|
||||
bindVisibility(cs, item.repliesFolded.inverted())
|
||||
val repliesPanel = ComponentListPanelFactory.createVertical(cs, discussion.replies, GitLabNoteViewModel::id, 0) { noteCs, noteVm ->
|
||||
createNoteItem(project, noteCs, avatarIconsProvider, noteVm)
|
||||
}.apply {
|
||||
bindVisibility(cs, discussion.repliesFolded.inverted())
|
||||
}
|
||||
|
||||
return CodeReviewChatItemUIUtil.buildDynamic(CodeReviewChatItemUIUtil.ComponentType.FULL,
|
||||
{ item.author.createIconValue(cs, avatarIconsProvider, it) },
|
||||
{ discussion.author.createIconValue(cs, avatarIconsProvider, it) },
|
||||
contentPanel) {
|
||||
withHeader(createTitleTextPane(cs, item.author, item.date), actionsPanel)
|
||||
withHeader(createTitleTextPane(cs, discussion.author, discussion.date), actionsPanel)
|
||||
}.let {
|
||||
VerticalListPanel().apply {
|
||||
add(it)
|
||||
@@ -109,7 +95,7 @@ object GitLabMergeRequestTimelineDiscussionComponentFactory {
|
||||
|
||||
private fun createRepliesActionsPanel(cs: CoroutineScope,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
item: GitLabMergeRequestTimelineItemViewModel.Discussion): JComponent {
|
||||
item: GitLabMergeRequestTimelineDiscussionViewModel): JComponent {
|
||||
val authorsLabel = JLabel().apply {
|
||||
bindVisibility(cs, item.replies.map { it.isNotEmpty() })
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// 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.timeline
|
||||
|
||||
import com.intellij.collaboration.async.mapCaching
|
||||
import com.intellij.collaboration.async.mapScoped
|
||||
import com.intellij.collaboration.async.modelFlow
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.util.childScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabDiscussion
|
||||
@@ -12,8 +15,10 @@ import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.GitLabMergeRequestDi
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.comment.GitLabMergeRequestDiscussionResolveViewModelImpl
|
||||
import org.jetbrains.plugins.gitlab.ui.comment.GitLabNoteViewModel
|
||||
import org.jetbrains.plugins.gitlab.ui.comment.GitLabNoteViewModelImpl
|
||||
import java.util.*
|
||||
|
||||
interface GitLabMergeRequestTimelineDiscussionViewModel {
|
||||
val date: Date
|
||||
val author: Flow<GitLabUserDTO>
|
||||
|
||||
val mainNote: Flow<GitLabNoteViewModel>
|
||||
@@ -24,28 +29,39 @@ interface GitLabMergeRequestTimelineDiscussionViewModel {
|
||||
val resolvedVm: GitLabMergeRequestDiscussionResolveViewModel?
|
||||
|
||||
fun setRepliesFolded(folded: Boolean)
|
||||
|
||||
suspend fun destroy()
|
||||
}
|
||||
|
||||
private val LOG = logger<GitLabMergeRequestTimelineDiscussionViewModel>()
|
||||
|
||||
class GitLabMergeRequestTimelineDiscussionViewModelImpl(
|
||||
parentCs: CoroutineScope,
|
||||
discussion: GitLabDiscussion
|
||||
) : GitLabMergeRequestTimelineDiscussionViewModel {
|
||||
|
||||
private val cs = parentCs.childScope()
|
||||
private val cs = parentCs.childScope(CoroutineExceptionHandler { _, e -> LOG.warn(e) })
|
||||
|
||||
override val mainNote: Flow<GitLabNoteViewModel> = discussion.notes.mapScoped {
|
||||
createNoteVm(this, it.first())
|
||||
}.share()
|
||||
override val mainNote: Flow<GitLabNoteViewModel> = discussion.notes
|
||||
.map { it.first() }
|
||||
.distinctUntilChangedBy { it.id }
|
||||
.mapScoped { GitLabNoteViewModelImpl(this, it) }
|
||||
.modelFlow(cs, LOG)
|
||||
|
||||
override val date: Date = discussion.createdAt
|
||||
override val author: Flow<GitLabUserDTO> = mainNote.map { it.author }
|
||||
|
||||
private val _repliesFolded = MutableStateFlow(true)
|
||||
override val repliesFolded: Flow<Boolean> = _repliesFolded.asStateFlow()
|
||||
|
||||
override val replies: Flow<List<GitLabNoteViewModel>> =
|
||||
discussion.notes.mapScoped { notesList ->
|
||||
notesList.asSequence().drop(1).map { createNoteVm(this, it) }.toList()
|
||||
}.share()
|
||||
|
||||
override val author: Flow<GitLabUserDTO> = mainNote.map { it.author }
|
||||
override val replies: Flow<List<GitLabNoteViewModel>> = discussion.notes
|
||||
.map { it.drop(1) }
|
||||
.mapCaching(
|
||||
GitLabNote::id,
|
||||
{ GitLabNoteViewModelImpl(cs, it) },
|
||||
GitLabNoteViewModelImpl::destroy
|
||||
)
|
||||
.modelFlow(cs, LOG)
|
||||
|
||||
override val resolvedVm: GitLabMergeRequestDiscussionResolveViewModel? =
|
||||
if (discussion.canResolve) GitLabMergeRequestDiscussionResolveViewModelImpl(cs, discussion) else null
|
||||
@@ -54,8 +70,12 @@ class GitLabMergeRequestTimelineDiscussionViewModelImpl(
|
||||
_repliesFolded.value = folded
|
||||
}
|
||||
|
||||
private fun createNoteVm(parentCs: CoroutineScope, note: GitLabNote): GitLabNoteViewModel =
|
||||
GitLabNoteViewModelImpl(parentCs, note)
|
||||
|
||||
private fun <T> Flow<T>.share() = shareIn(cs, SharingStarted.Lazily, 1)
|
||||
override suspend fun destroy() {
|
||||
try {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
}
|
||||
catch (e: CancellationException) {
|
||||
// ignore, cuz we don't want to cancel the invoker
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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.timeline
|
||||
|
||||
import org.jetbrains.plugins.gitlab.api.dto.*
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabDiscussion
|
||||
import java.util.*
|
||||
|
||||
sealed interface GitLabMergeRequestTimelineItem {
|
||||
|
||||
val id: String
|
||||
val date: Date
|
||||
|
||||
sealed interface Immutable : GitLabMergeRequestTimelineItem {
|
||||
val actor: GitLabUserDTO
|
||||
}
|
||||
|
||||
class StateEvent(
|
||||
val event: GitLabResourceStateEventDTO
|
||||
) : Immutable {
|
||||
override val id: String = "StateEvent:" + event.id.toString()
|
||||
override val actor: GitLabUserDTO = event.user
|
||||
override val date: Date = event.createdAt
|
||||
}
|
||||
|
||||
class LabelEvent(
|
||||
val event: GitLabResourceLabelEventDTO
|
||||
) : Immutable {
|
||||
override val id: String = "LabelEvent:" + event.id.toString()
|
||||
override val actor: GitLabUserDTO = event.user
|
||||
override val date: Date = event.createdAt
|
||||
}
|
||||
|
||||
class MilestoneEvent(
|
||||
val event: GitLabResourceMilestoneEventDTO
|
||||
) : Immutable {
|
||||
override val id: String = "MilestoneEvent:" + event.id.toString()
|
||||
override val actor: GitLabUserDTO = event.user
|
||||
override val date: Date = event.createdAt
|
||||
}
|
||||
|
||||
class SystemDiscussion(
|
||||
discussion: GitLabDiscussionDTO
|
||||
) : Immutable {
|
||||
private val firstNote = discussion.notes.first()
|
||||
|
||||
override val id: String = discussion.id
|
||||
override val actor: GitLabUserDTO = firstNote.author
|
||||
override val date: Date = discussion.createdAt
|
||||
|
||||
val content: String = firstNote.body
|
||||
}
|
||||
|
||||
class UserDiscussion(
|
||||
val discussion: GitLabDiscussion
|
||||
) : GitLabMergeRequestTimelineItem {
|
||||
|
||||
override val id: String = discussion.id
|
||||
override val date: Date = discussion.createdAt
|
||||
}
|
||||
}
|
||||
@@ -2,53 +2,15 @@
|
||||
package org.jetbrains.plugins.gitlab.mergerequest.ui.timeline
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.plugins.gitlab.api.dto.*
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabDiscussion
|
||||
import java.util.*
|
||||
|
||||
sealed interface GitLabMergeRequestTimelineItemViewModel {
|
||||
|
||||
val id: String
|
||||
val date: Date
|
||||
|
||||
sealed interface Immutable : GitLabMergeRequestTimelineItemViewModel {
|
||||
val actor: GitLabUserDTO
|
||||
}
|
||||
|
||||
class StateEvent(
|
||||
val event: GitLabResourceStateEventDTO
|
||||
) : Immutable {
|
||||
override val id: String = "StateEvent:" + event.id.toString()
|
||||
override val actor: GitLabUserDTO = event.user
|
||||
override val date: Date = event.createdAt
|
||||
}
|
||||
|
||||
class LabelEvent(
|
||||
val event: GitLabResourceLabelEventDTO
|
||||
) : Immutable {
|
||||
override val id: String = "LabelEvent:" + event.id.toString()
|
||||
override val actor: GitLabUserDTO = event.user
|
||||
override val date: Date = event.createdAt
|
||||
}
|
||||
|
||||
class MilestoneEvent(
|
||||
val event: GitLabResourceMilestoneEventDTO
|
||||
) : Immutable {
|
||||
override val id: String = "MilestoneEvent:" + event.id.toString()
|
||||
override val actor: GitLabUserDTO = event.user
|
||||
override val date: Date = event.createdAt
|
||||
}
|
||||
|
||||
class SystemDiscussion(
|
||||
discussion: GitLabDiscussionDTO
|
||||
) : Immutable {
|
||||
private val firstNote = discussion.notes.first()
|
||||
|
||||
override val id: String = "SystemDiscussion:" + discussion.id
|
||||
override val actor: GitLabUserDTO = firstNote.author
|
||||
override val date: Date = discussion.createdAt
|
||||
|
||||
val content: String = firstNote.body
|
||||
class Immutable(
|
||||
val item: GitLabMergeRequestTimelineItem.Immutable
|
||||
) : GitLabMergeRequestTimelineItemViewModel {
|
||||
override val id: String = item.id
|
||||
}
|
||||
|
||||
class Discussion(
|
||||
@@ -57,7 +19,6 @@ sealed interface GitLabMergeRequestTimelineItemViewModel {
|
||||
) : GitLabMergeRequestTimelineItemViewModel,
|
||||
GitLabMergeRequestTimelineDiscussionViewModel
|
||||
by GitLabMergeRequestTimelineDiscussionViewModelImpl(parentCs, discussion) {
|
||||
override val id: String = "Discussion:" + discussion.id
|
||||
override val date: Date = discussion.createdAt
|
||||
override val id: String = discussion.id
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// 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.timeline
|
||||
|
||||
import com.intellij.collaboration.async.mapScoped
|
||||
import com.intellij.collaboration.async.mapCaching
|
||||
import com.intellij.collaboration.async.modelFlow
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.util.childScope
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import kotlinx.coroutines.*
|
||||
@@ -13,21 +15,22 @@ import org.jetbrains.plugins.gitlab.mergerequest.api.request.loadMergeRequestSta
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestDiscussionsModelImpl
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestId
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.timeline.GitLabMergeRequestTimelineViewModel.LoadingState
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
interface GitLabMergeRequestTimelineViewModel {
|
||||
val timelineLoadingFlow: Flow<LoadingState?>
|
||||
|
||||
fun startLoading()
|
||||
fun reset()
|
||||
fun requestLoad()
|
||||
|
||||
sealed interface LoadingState {
|
||||
object Loading : LoadingState
|
||||
class Error(val exception: Throwable) : LoadingState
|
||||
class Result(val items: List<GitLabMergeRequestTimelineItemViewModel>) : LoadingState
|
||||
class Result(val items: Flow<List<GitLabMergeRequestTimelineItemViewModel>>) : LoadingState
|
||||
}
|
||||
}
|
||||
|
||||
private val LOG = logger<GitLabMergeRequestTimelineViewModel>()
|
||||
|
||||
class LoadAllGitLabMergeRequestTimelineViewModel(
|
||||
parentCs: CoroutineScope,
|
||||
private val connection: GitLabProjectConnection,
|
||||
@@ -35,100 +38,87 @@ class LoadAllGitLabMergeRequestTimelineViewModel(
|
||||
) : GitLabMergeRequestTimelineViewModel {
|
||||
|
||||
private val cs = parentCs.childScope(Dispatchers.Default)
|
||||
private var loadingRequestState = MutableStateFlow<Flow<List<GitLabMergeRequestTimelineItemViewModel>>?>(null)
|
||||
private val timelineLoadingRequests = MutableSharedFlow<Unit>(1)
|
||||
|
||||
private val discussionsDataProvider = GitLabMergeRequestDiscussionsModelImpl(cs, connection, mr)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val timelineLoadingFlow: Flow<LoadingState?> =
|
||||
loadingRequestState.transformLatest { itemsFlow ->
|
||||
if (itemsFlow == null) {
|
||||
emit(null)
|
||||
return@transformLatest
|
||||
}
|
||||
|
||||
override val timelineLoadingFlow: Flow<LoadingState> =
|
||||
timelineLoadingRequests.transformLatest {
|
||||
emit(LoadingState.Loading)
|
||||
|
||||
// so it's not a supervisor
|
||||
coroutineScope {
|
||||
itemsFlow.catch {
|
||||
emit(LoadingState.Error(it))
|
||||
}.collectLatest {
|
||||
emit(LoadingState.Result(it))
|
||||
val result = try {
|
||||
LoadingState.Result(createItemsFlow(this).mapToVms(this).stateIn(this))
|
||||
}
|
||||
catch (ce: CancellationException) {
|
||||
throw ce
|
||||
}
|
||||
catch (e: Exception) {
|
||||
LoadingState.Error(e)
|
||||
}
|
||||
emit(result)
|
||||
awaitCancellation()
|
||||
}
|
||||
}
|
||||
}.modelFlow(cs, LOG)
|
||||
|
||||
@RequiresEdt
|
||||
override fun startLoading() {
|
||||
loadingRequestState.update {
|
||||
it ?: createItemsFlow()
|
||||
override fun requestLoad() {
|
||||
cs.launch {
|
||||
timelineLoadingRequests.emit(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createItemsFlow(): Flow<List<GitLabMergeRequestTimelineItemViewModel>> {
|
||||
/**
|
||||
* Load all simple events and discussions and subscribe to user discussions changes
|
||||
*/
|
||||
private suspend fun createItemsFlow(cs: CoroutineScope): Flow<List<GitLabMergeRequestTimelineItem>> {
|
||||
val api = connection.apiClient
|
||||
val project = connection.repo.repository
|
||||
|
||||
val discussionsFlow = discussionsDataProvider.userDiscussions.mapScoped { discussions ->
|
||||
val discussionsScope = this
|
||||
discussions.map {
|
||||
GitLabMergeRequestTimelineItemViewModel.Discussion(discussionsScope, it)
|
||||
val simpleEventsRequest = cs.async(Dispatchers.IO) {
|
||||
val vms = ConcurrentLinkedQueue<GitLabMergeRequestTimelineItem>()
|
||||
launch {
|
||||
discussionsDataProvider.systemDiscussions.first()
|
||||
.map { GitLabMergeRequestTimelineItem.SystemDiscussion(it) }
|
||||
.also { vms.addAll(it) }
|
||||
}
|
||||
|
||||
launch {
|
||||
api.loadMergeRequestStateEvents(project, mr).body()
|
||||
.map { GitLabMergeRequestTimelineItem.StateEvent(it) }
|
||||
.also { vms.addAll(it) }
|
||||
}
|
||||
|
||||
launch {
|
||||
api.loadMergeRequestLabelEvents(project, mr).body()
|
||||
.map { GitLabMergeRequestTimelineItem.LabelEvent(it) }
|
||||
.also { vms.addAll(it) }
|
||||
}
|
||||
|
||||
launch {
|
||||
api.loadMergeRequestMilestoneEvents(project, mr).body()
|
||||
.map { GitLabMergeRequestTimelineItem.MilestoneEvent(it) }
|
||||
.also { vms.addAll(it) }
|
||||
}
|
||||
vms
|
||||
}
|
||||
|
||||
val simpleEventsFlow: Flow<List<GitLabMergeRequestTimelineItemViewModel.Immutable>> =
|
||||
channelFlow {
|
||||
launch {
|
||||
discussionsDataProvider.systemDiscussions.collect { discussions ->
|
||||
send(discussions.map { GitLabMergeRequestTimelineItemViewModel.SystemDiscussion(it) })
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
api.loadMergeRequestStateEvents(project, mr).body().let { events ->
|
||||
events.map { GitLabMergeRequestTimelineItemViewModel.StateEvent(it) }
|
||||
}.also {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
api.loadMergeRequestStateEvents(project, mr).body().let { events ->
|
||||
events.map { GitLabMergeRequestTimelineItemViewModel.StateEvent(it) }
|
||||
}.also {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
api.loadMergeRequestLabelEvents(project, mr).body().let { events ->
|
||||
events.map { GitLabMergeRequestTimelineItemViewModel.LabelEvent(it) }
|
||||
}.also {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
|
||||
launch {
|
||||
api.loadMergeRequestMilestoneEvents(project, mr).body().let { events ->
|
||||
events.map { GitLabMergeRequestTimelineItemViewModel.MilestoneEvent(it) }
|
||||
}.also {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
|
||||
return combine(discussionsFlow, simpleEventsFlow) { discussions, simpleEvents ->
|
||||
TreeSet(Comparator.comparing(GitLabMergeRequestTimelineItemViewModel::date)).apply {
|
||||
addAll(simpleEvents)
|
||||
addAll(discussions)
|
||||
}.toList()
|
||||
return discussionsDataProvider.userDiscussions.map { discussions ->
|
||||
(simpleEventsRequest.await() + discussions.map(GitLabMergeRequestTimelineItem::UserDiscussion)).sortedBy { it.date }
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
override fun reset() {
|
||||
loadingRequestState.value = null
|
||||
}
|
||||
private suspend fun Flow<List<GitLabMergeRequestTimelineItem>>.mapToVms(cs: CoroutineScope) =
|
||||
mapCaching(
|
||||
GitLabMergeRequestTimelineItem::id,
|
||||
{ createVm(cs, it) },
|
||||
{ if (this is GitLabMergeRequestTimelineItemViewModel.Discussion) destroy() }
|
||||
)
|
||||
|
||||
private fun createVm(cs: CoroutineScope, item: GitLabMergeRequestTimelineItem): GitLabMergeRequestTimelineItemViewModel =
|
||||
when (item) {
|
||||
is GitLabMergeRequestTimelineItem.Immutable -> GitLabMergeRequestTimelineItemViewModel.Immutable(item)
|
||||
is GitLabMergeRequestTimelineItem.UserDiscussion -> GitLabMergeRequestTimelineItemViewModel.Discussion(cs, item.discussion)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class GitLabNoteEditingViewModelImpl(parentCs: CoroutineScope,
|
||||
val newText = text.first()
|
||||
_state.value = GitLabNoteEditingViewModel.SubmissionState.Loading
|
||||
try {
|
||||
note.updateBody(newText)
|
||||
note.setBody(newText)
|
||||
_state.value = null
|
||||
onDone()
|
||||
}
|
||||
@@ -54,6 +54,11 @@ class GitLabNoteEditingViewModelImpl(parentCs: CoroutineScope,
|
||||
}
|
||||
|
||||
override suspend fun destroy() {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
try {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
}
|
||||
catch (e: CancellationException) {
|
||||
// ignore, cuz we don't want to cancel the invoker
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
// 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.ui.comment
|
||||
|
||||
import com.intellij.collaboration.async.modelFlow
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.util.childScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabNote
|
||||
@@ -15,6 +14,7 @@ import org.jetbrains.plugins.gitlab.ui.GitLabUIUtil
|
||||
import java.util.*
|
||||
|
||||
interface GitLabNoteViewModel {
|
||||
val id: String
|
||||
val author: GitLabUserDTO
|
||||
val createdAt: Date
|
||||
|
||||
@@ -23,6 +23,8 @@ interface GitLabNoteViewModel {
|
||||
val htmlBody: Flow<@Nls String>
|
||||
}
|
||||
|
||||
private val LOG = logger<GitLabNoteViewModel>()
|
||||
|
||||
class GitLabNoteViewModelImpl(
|
||||
parentCs: CoroutineScope,
|
||||
note: GitLabNote
|
||||
@@ -30,6 +32,7 @@ class GitLabNoteViewModelImpl(
|
||||
|
||||
private val cs = parentCs.childScope(Dispatchers.Default)
|
||||
|
||||
override val id: String = note.id
|
||||
override val author: GitLabUserDTO = note.author
|
||||
override val createdAt: Date = note.createdAt
|
||||
|
||||
@@ -37,6 +40,14 @@ class GitLabNoteViewModelImpl(
|
||||
if (note.canAdmin) GitLabNoteAdminActionsViewModelImpl(cs, note) else null
|
||||
|
||||
private val body: Flow<String> = note.body
|
||||
override val htmlBody: Flow<String> = body.map { GitLabUIUtil.convertToHtml(it) }
|
||||
.shareIn(cs, SharingStarted.Lazily, 1)
|
||||
override val htmlBody: Flow<String> = body.map { GitLabUIUtil.convertToHtml(it) }.modelFlow(cs, LOG)
|
||||
|
||||
suspend fun destroy() {
|
||||
try {
|
||||
cs.coroutineContext[Job]!!.cancelAndJoin()
|
||||
}
|
||||
catch (e: CancellationException) {
|
||||
// ignore, cuz we don't want to cancel the invoker
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user