mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
refactor [gitlab]: a better reviewers selector when creating a merge request
Now loads items in batches and displays a correct progress indicator. Also the UI is now aligned with GitHub GitOrigin-RevId: 14e22d7db4716d44acfdde00da70eca238be33de
This commit is contained in:
committed by
intellij-monorepo-bot
parent
261367fd29
commit
6b8f1270fe
+63
@@ -310,6 +310,69 @@ object ChooserPopupUtil {
|
||||
|
||||
// Multiple options:
|
||||
|
||||
/**
|
||||
* Displays a chooser popup that allows selecting multiple items from a list, with support for incremental loading.
|
||||
*
|
||||
* @param point The location where the popup will be shown as a relative point.
|
||||
* @param currentItems The list of currently available items that should be initially displayed in the chooser.
|
||||
* @param listState A [StateFlow] representing the state of incrementally loaded values for the items in the popup.
|
||||
* It provides new items, tracks loading status, and contains any exceptions encountered.
|
||||
* @param presenter A function that maps each item in the list to a [PopupItemPresentation], which determines how the
|
||||
* item will appear in the popup (e.g., text, icon).
|
||||
* @param popupConfig A [PopupConfig] to configure the popup's behavior and appearance, with a default
|
||||
* configuration provided if not specified.
|
||||
* @return A list of items selected by the user from the popup.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
@JvmOverloads
|
||||
suspend fun <T : Any> showMultipleChooserPopupWithIncrementalLoading(
|
||||
point: RelativePoint,
|
||||
currentItems: List<T>,
|
||||
listState: StateFlow<IncrementallyComputedValue<List<T>>>,
|
||||
presenter: (T) -> PopupItemPresentation,
|
||||
popupConfig: PopupConfig = PopupConfig.DEFAULT,
|
||||
): List<T> {
|
||||
val listModel = MultiChooserListModel<T>().apply {
|
||||
add(currentItems)
|
||||
setChosen(currentItems)
|
||||
}
|
||||
val list = createSelectableList(listModel, presenter)
|
||||
|
||||
list.launchOnShow("List items loader") {
|
||||
listState.collect { state ->
|
||||
list.setPaintBusy(state.isLoading)
|
||||
|
||||
state.exceptionOrNull?.let { exception ->
|
||||
list.emptyText.showError(exception, popupConfig.errorPresenter) // TODO: show error even when list is not empty
|
||||
} ?: run {
|
||||
list.emptyText.clear()
|
||||
}
|
||||
|
||||
state.onNoValue {
|
||||
listModel.removeAllExceptChosen()
|
||||
}.onValueAvailable { newList ->
|
||||
val selected = list.selectedValue
|
||||
listModel.retainChosenAndUpdate(newList)
|
||||
list.setSelectedValue(selected, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val popup = PopupChooserBuilder(list)
|
||||
.setFilteringEnabled {
|
||||
filterByNamesFromPresentation(presenter)(it as T)
|
||||
}
|
||||
.setCloseOnEnter(false)
|
||||
.configure(popupConfig)
|
||||
.createPopup()
|
||||
|
||||
CollaborationToolsPopupUtil.configureSearchField(popup, popupConfig)
|
||||
PopupUtil.setPopupToggleComponent(popup, point.component)
|
||||
|
||||
return popup.showAndAwaitSubmissions(listModel, point, popupConfig.showDirection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an asynchronous popup allowing users to select multiple items from a dynamically loaded list.
|
||||
*
|
||||
|
||||
+63
@@ -57,4 +57,67 @@ internal class MultiChooserListModel<T> : AbstractListModel<T>() {
|
||||
fireIntervalAdded(this, 0, lastIndex + count)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the list of items, ensuring that chosen items stay in the list
|
||||
*/
|
||||
fun retainChosenAndUpdate(newList: List<T>) {
|
||||
val oldSize = items.size
|
||||
|
||||
items.clear()
|
||||
itemsSet.clear()
|
||||
|
||||
chosenItems.forEach { chosen ->
|
||||
items.add(chosen)
|
||||
itemsSet.add(chosen)
|
||||
}
|
||||
|
||||
newList.forEach { item ->
|
||||
if (itemsSet.add(item)) {
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
val newSize = items.size
|
||||
when {
|
||||
oldSize == 0 && newSize > 0 -> {
|
||||
fireIntervalAdded(this, 0, newSize - 1)
|
||||
}
|
||||
newSize == 0 && oldSize > 0 -> {
|
||||
fireIntervalRemoved(this, 0, oldSize - 1)
|
||||
}
|
||||
oldSize != newSize -> {
|
||||
val minSize = minOf(oldSize, newSize)
|
||||
if (minSize > 0) {
|
||||
fireContentsChanged(this, 0, minSize - 1)
|
||||
}
|
||||
if (newSize > oldSize) {
|
||||
fireIntervalAdded(this, oldSize, newSize - 1)
|
||||
}
|
||||
else {
|
||||
fireIntervalRemoved(this, newSize, oldSize - 1)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
fireContentsChanged(this, 0, newSize - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllExceptChosen() {
|
||||
val oldSize = items.size
|
||||
items.clear()
|
||||
itemsSet.clear()
|
||||
if (oldSize != 0) {
|
||||
fireIntervalRemoved(this, 0, oldSize - 1)
|
||||
}
|
||||
|
||||
chosenItems.forEach { chosen ->
|
||||
items.add(chosen)
|
||||
itemsSet.add(chosen)
|
||||
}
|
||||
if (items.isNotEmpty()) {
|
||||
fireIntervalAdded(this, 0, items.lastIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
+213
@@ -217,4 +217,217 @@ internal class MultiChooserListModelTest {
|
||||
model.add(listOf("item1", "item2"))
|
||||
assertThat(eventFired).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test removeAllExceptChosen removes non-chosen items`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3", "item4"))
|
||||
model.setChosen(listOf("item1", "item3"))
|
||||
|
||||
model.removeAllExceptChosen()
|
||||
|
||||
assertThat(model.size).isEqualTo(2)
|
||||
assertThat(model.getElementAt(0)).isEqualTo("item1")
|
||||
assertThat(model.getElementAt(1)).isEqualTo("item3")
|
||||
assertThat(model.getChosenItems()).containsExactly("item1", "item3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test removeAllExceptChosen with no chosen items clears list`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
|
||||
model.removeAllExceptChosen()
|
||||
|
||||
assertThat(model.size).isEqualTo(0)
|
||||
assertThat(model.getChosenItems()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test removeAllExceptChosen with all chosen items keeps all`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
model.setChosen(listOf("item1", "item2", "item3"))
|
||||
|
||||
model.removeAllExceptChosen()
|
||||
|
||||
assertThat(model.size).isEqualTo(3)
|
||||
assertThat(model.getElementAt(0)).isEqualTo("item1")
|
||||
assertThat(model.getElementAt(1)).isEqualTo("item2")
|
||||
assertThat(model.getElementAt(2)).isEqualTo("item3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test removeAllExceptChosen fires interval removed event`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
model.setChosen(listOf("item1"))
|
||||
|
||||
var intervalRemovedFired = false
|
||||
|
||||
model.addListDataListener(object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) {}
|
||||
override fun intervalRemoved(e: ListDataEvent) {
|
||||
intervalRemovedFired = true
|
||||
}
|
||||
override fun contentsChanged(e: ListDataEvent) {}
|
||||
})
|
||||
|
||||
model.removeAllExceptChosen()
|
||||
assertThat(intervalRemovedFired).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test removeAllExceptChosen on empty list does nothing`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
|
||||
var eventFired = false
|
||||
|
||||
model.addListDataListener(object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) {
|
||||
eventFired = true
|
||||
}
|
||||
override fun intervalRemoved(e: ListDataEvent) {
|
||||
eventFired = true
|
||||
}
|
||||
override fun contentsChanged(e: ListDataEvent) {
|
||||
eventFired = true
|
||||
}
|
||||
})
|
||||
|
||||
model.removeAllExceptChosen()
|
||||
assertThat(eventFired).isFalse()
|
||||
assertThat(model.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate replaces items with new list`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
|
||||
model.retainChosenAndUpdate(listOf("item4", "item5"))
|
||||
|
||||
assertThat(model.size).isEqualTo(2)
|
||||
assertThat(model.getElementAt(0)).isEqualTo("item4")
|
||||
assertThat(model.getElementAt(1)).isEqualTo("item5")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate keeps chosen items`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
model.setChosen(listOf("item1", "item3"))
|
||||
|
||||
model.retainChosenAndUpdate(listOf("item4", "item5"))
|
||||
|
||||
assertThat(model.size).isEqualTo(4)
|
||||
assertThat(model.getElementAt(0)).isEqualTo("item1")
|
||||
assertThat(model.getElementAt(1)).isEqualTo("item3")
|
||||
assertThat(model.getElementAt(2)).isEqualTo("item4")
|
||||
assertThat(model.getElementAt(3)).isEqualTo("item5")
|
||||
assertThat(model.isChosen("item1")).isTrue()
|
||||
assertThat(model.isChosen("item3")).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate avoids duplicates between chosen and new items`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
model.setChosen(listOf("item1", "item2"))
|
||||
|
||||
model.retainChosenAndUpdate(listOf("item2", "item3", "item4"))
|
||||
|
||||
assertThat(model.size).isEqualTo(4)
|
||||
assertThat(model.getElementAt(0)).isEqualTo("item1")
|
||||
assertThat(model.getElementAt(1)).isEqualTo("item2")
|
||||
assertThat(model.getElementAt(2)).isEqualTo("item3")
|
||||
assertThat(model.getElementAt(3)).isEqualTo("item4")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate with empty new list keeps chosen items`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
model.setChosen(listOf("item1", "item3"))
|
||||
|
||||
model.retainChosenAndUpdate(emptyList())
|
||||
|
||||
assertThat(model.size).isEqualTo(2)
|
||||
assertThat(model.getElementAt(0)).isEqualTo("item1")
|
||||
assertThat(model.getElementAt(1)).isEqualTo("item3")
|
||||
assertThat(model.getChosenItems()).containsExactly("item1", "item3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate with no chosen items replaces all`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
|
||||
model.retainChosenAndUpdate(listOf("item4", "item5"))
|
||||
|
||||
assertThat(model.size).isEqualTo(2)
|
||||
assertThat(model.getElementAt(0)).isEqualTo("item4")
|
||||
assertThat(model.getElementAt(1)).isEqualTo("item5")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate fires appropriate events when growing`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2"))
|
||||
|
||||
var contentsChangedFired = false
|
||||
var intervalAddedFired = false
|
||||
|
||||
model.addListDataListener(object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) {
|
||||
intervalAddedFired = true
|
||||
}
|
||||
override fun intervalRemoved(e: ListDataEvent) {}
|
||||
override fun contentsChanged(e: ListDataEvent) {
|
||||
contentsChangedFired = true
|
||||
}
|
||||
})
|
||||
|
||||
model.retainChosenAndUpdate(listOf("item3", "item4", "item5"))
|
||||
|
||||
assertThat(contentsChangedFired).isTrue()
|
||||
assertThat(intervalAddedFired).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate fires appropriate events when shrinking`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3", "item4"))
|
||||
|
||||
var contentsChangedFired = false
|
||||
var intervalRemovedFired = false
|
||||
|
||||
model.addListDataListener(object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) {}
|
||||
override fun intervalRemoved(e: ListDataEvent) {
|
||||
intervalRemovedFired = true
|
||||
}
|
||||
override fun contentsChanged(e: ListDataEvent) {
|
||||
contentsChangedFired = true
|
||||
}
|
||||
})
|
||||
|
||||
model.retainChosenAndUpdate(listOf("itemA"))
|
||||
|
||||
assertThat(contentsChangedFired).isTrue()
|
||||
assertThat(intervalRemovedFired).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test retainChosenAndUpdate preserves chosen state`() {
|
||||
val model = MultiChooserListModel<String>()
|
||||
model.add(listOf("item1", "item2", "item3"))
|
||||
model.setChosen(listOf("item1", "item2"))
|
||||
|
||||
model.retainChosenAndUpdate(listOf("item1", "item4"))
|
||||
|
||||
assertThat(model.isChosen("item1")).isTrue()
|
||||
assertThat(model.isChosen("item2")).isTrue()
|
||||
assertThat(model.isChosen("item4")).isFalse()
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,8 @@ merge.request.create.error.protected.branch=Commits will be pushed to a protecte
|
||||
merge.request.create.progress.text=Creating merge request\u2026
|
||||
merge.request.create.status.already.exists.view=View
|
||||
merge.request.create.status.already.exists=Merge Request already exists
|
||||
merge.request.create.reviewers.label={0,choice,0#Add reviewers|1#Reviewer|2#Reviewers}
|
||||
merge.request.create.no.reviewers=No reviewers
|
||||
merge.request.create.reviewers=Reviewers:
|
||||
merge.request.create.tab.title=New Merge Request at {0}
|
||||
merge.request.create.title.placeholder=Title
|
||||
merge.request.create.description.placeholder=Description
|
||||
|
||||
+11
-2
@@ -3,6 +3,7 @@ package org.jetbrains.plugins.gitlab.mergerequest.ui.create
|
||||
|
||||
import com.intellij.collaboration.async.launchNow
|
||||
import com.intellij.collaboration.ui.CollaborationToolsUIUtil
|
||||
import com.intellij.collaboration.ui.LabeledListComponentsFactory
|
||||
import com.intellij.collaboration.ui.SingleValueModel
|
||||
import com.intellij.collaboration.ui.bindValueIn
|
||||
import com.intellij.collaboration.ui.codereview.commits.CommitsBrowserComponentBuilder
|
||||
@@ -50,7 +51,7 @@ internal object GitLabMergeRequestCreateComponentFactory {
|
||||
val directionSelector = createDirectionSelector(directionModel)
|
||||
val commitsLoadingPanel = createCommitsPanel(project, cs, createVm)
|
||||
val textPanel = cs.createTextPanel(project, createVm)
|
||||
val reviewersPanel = GitLabMergeRequestCreateReviewersComponentFactory.create(cs, createVm)
|
||||
val metadataPanel = createMetadataPanel(createVm)
|
||||
val statusPanel = GitLabMergeRequestCreateStatusComponentFactory.create(cs, createVm)
|
||||
val actionsPanel = GitLabMergeRequestCreateActionsComponentFactory.create(project, cs, createVm)
|
||||
|
||||
@@ -60,7 +61,7 @@ internal object GitLabMergeRequestCreateComponentFactory {
|
||||
.addSeparator()
|
||||
.addComponent(textPanel, zeroMinWidth = true, stretchYWithWeight = 0.3f, withoutBorder = true)
|
||||
.addSeparator()
|
||||
.addComponent(reviewersPanel, zeroMinWidth = true)
|
||||
.addComponent(metadataPanel, zeroMinWidth = true)
|
||||
.addSeparator()
|
||||
.addComponent(statusPanel)
|
||||
.addComponent(actionsPanel, withListBackground = false)
|
||||
@@ -140,4 +141,12 @@ internal object GitLabMergeRequestCreateComponentFactory {
|
||||
|
||||
return CollaborationToolsUIUtil.wrapWithProgressStripe(cs, createVm.commits.map { it == null }, commitsPanel)
|
||||
}
|
||||
|
||||
private fun createMetadataPanel(createVm: GitLabMergeRequestCreateViewModel): JComponent {
|
||||
val lists = buildList {
|
||||
add(GitLabMergeRequestCreateReviewersComponentFactory.createReviewersListPanelHandle(createVm))
|
||||
}
|
||||
|
||||
return LabeledListComponentsFactory.createGrid(lists)
|
||||
}
|
||||
}
|
||||
+46
-46
@@ -1,64 +1,64 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2025 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.create
|
||||
|
||||
import com.intellij.collaboration.ui.ComponentListPanelFactory
|
||||
import com.intellij.collaboration.ui.HorizontalListPanel
|
||||
import com.intellij.collaboration.ui.VerticalListPanel
|
||||
import com.intellij.collaboration.async.mapState
|
||||
import com.intellij.collaboration.ui.LabeledListComponentsFactory
|
||||
import com.intellij.collaboration.ui.codereview.avatar.Avatar
|
||||
import com.intellij.collaboration.ui.codereview.comment.CodeReviewCommentUIUtil
|
||||
import com.intellij.collaboration.ui.codereview.details.CodeReviewDetailsStatusComponentFactory
|
||||
import com.intellij.collaboration.ui.util.bindTextIn
|
||||
import com.intellij.collaboration.ui.codereview.list.search.ShowDirection
|
||||
import com.intellij.collaboration.ui.icon.IconsProvider
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.GitLabMergeRequestCreateViewModel
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.util.GitLabMergeRequestChoosersUtil
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabBundle
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
internal object GitLabMergeRequestCreateReviewersComponentFactory {
|
||||
private const val HEADER_GAP = 5
|
||||
private const val REVIEWERS_GAP = 7
|
||||
private const val REVIEWER_PRESENTATION_GAP = 8
|
||||
private const val COMPONENTS_GAP = 5
|
||||
|
||||
fun create(cs: CoroutineScope, createVm: GitLabMergeRequestCreateViewModel): JComponent {
|
||||
val header = HorizontalListPanel(gap = HEADER_GAP).apply {
|
||||
val reviewersLabel = JLabel().apply {
|
||||
foreground = UIUtil.getContextHelpForeground()
|
||||
bindTextIn(cs, createVm.adjustedReviewers.map { reviewers ->
|
||||
GitLabBundle.message("merge.request.create.reviewers.label", reviewers.size)
|
||||
})
|
||||
}
|
||||
val editButton = CodeReviewCommentUIUtil.createEditButton { event ->
|
||||
val parentComponent = event.source as? JComponent ?: return@createEditButton
|
||||
val point = RelativePoint.getSouthWestOf(parentComponent)
|
||||
createVm.adjustReviewer(point)
|
||||
}
|
||||
|
||||
add(reviewersLabel)
|
||||
add(editButton)
|
||||
}
|
||||
val reviewersComponent = ComponentListPanelFactory.createVertical(
|
||||
cs,
|
||||
createVm.adjustedReviewers,
|
||||
gap = REVIEWERS_GAP,
|
||||
componentFactory = { reviewer -> createReviewerComponent(createVm, reviewer) }
|
||||
fun createReviewersListPanelHandle(
|
||||
vm: GitLabMergeRequestCreateViewModel,
|
||||
): Pair<JComponent, JComponent> {
|
||||
val label = LabeledListComponentsFactory.createLabelPanel(
|
||||
vm.reviewers.mapState { it.isEmpty() },
|
||||
GitLabBundle.message("merge.request.create.no.reviewers"),
|
||||
GitLabBundle.message("merge.request.create.reviewers")
|
||||
)
|
||||
|
||||
return VerticalListPanel(gap = COMPONENTS_GAP).apply {
|
||||
add(header)
|
||||
add(reviewersComponent)
|
||||
}
|
||||
val list = LabeledListComponentsFactory.createListPanel(
|
||||
vm.reviewers,
|
||||
{ comp, _ -> chooseReviewers(comp, vm, vm.avatarIconProvider) },
|
||||
{ UserLabel(it, vm.avatarIconProvider) }
|
||||
)
|
||||
|
||||
return label to list
|
||||
}
|
||||
|
||||
private fun createReviewerComponent(createVm: GitLabMergeRequestCreateViewModel, reviewer: GitLabUserDTO): JComponent {
|
||||
return CodeReviewDetailsStatusComponentFactory.ReviewDetailsStatusLabel("GitLab create MR: reviewer").apply {
|
||||
iconTextGap = REVIEWER_PRESENTATION_GAP
|
||||
icon = createVm.avatarIconProvider.getIcon(reviewer, Avatar.Sizes.BASE)
|
||||
text = reviewer.name
|
||||
private suspend fun chooseReviewers(
|
||||
parentComponent: JComponent,
|
||||
vm: GitLabMergeRequestCreateViewModel,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
) {
|
||||
val point = RelativePoint.getNorthEastOf(parentComponent)
|
||||
val allowsMultiple = vm.allowsMultipleReviewers.value
|
||||
val currentReviewers = vm.reviewers.value
|
||||
|
||||
val newList = if (allowsMultiple) {
|
||||
GitLabMergeRequestChoosersUtil.chooseUsers(point, currentReviewers, vm.projectMembers, avatarIconsProvider, ShowDirection.ABOVE)
|
||||
}
|
||||
else {
|
||||
val reviewer =
|
||||
GitLabMergeRequestChoosersUtil.chooseUser(point, vm.projectMembers, avatarIconsProvider, ShowDirection.ABOVE)
|
||||
listOfNotNull(reviewer)
|
||||
}
|
||||
vm.setReviewers(newList)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("FunctionName")
|
||||
private fun UserLabel(user: GitLabUserDTO, avatarIconsProvider: IconsProvider<GitLabUserDTO>) =
|
||||
JLabel(user.name, avatarIconsProvider.getIcon(user, Avatar.Sizes.BASE), SwingConstants.LEFT).apply {
|
||||
border = JBUI.Borders.empty(UIUtil.DEFAULT_VGAP, UIUtil.DEFAULT_HGAP / 2)
|
||||
}
|
||||
}
|
||||
+20
-31
@@ -5,7 +5,6 @@ import com.intellij.collaboration.api.HttpStatusErrorException
|
||||
import com.intellij.collaboration.async.childScope
|
||||
import com.intellij.collaboration.async.combineAndCollect
|
||||
import com.intellij.collaboration.async.extensionListFlow
|
||||
import com.intellij.collaboration.async.launchNow
|
||||
import com.intellij.collaboration.async.mapNullableScoped
|
||||
import com.intellij.collaboration.async.modelFlow
|
||||
import com.intellij.collaboration.async.stateInNow
|
||||
@@ -13,12 +12,13 @@ import com.intellij.collaboration.async.withInitial
|
||||
import com.intellij.collaboration.ui.ListenableProgressIndicator
|
||||
import com.intellij.collaboration.ui.codereview.create.CodeReviewTitleDescriptionViewModel
|
||||
import com.intellij.collaboration.ui.icon.IconsProvider
|
||||
import com.intellij.collaboration.util.IncrementallyComputedValue
|
||||
import com.intellij.collaboration.util.ResultUtil.runCatchingUser
|
||||
import com.intellij.collaboration.util.SingleCoroutineLauncher
|
||||
import com.intellij.collaboration.util.collectIncrementallyTo
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import com.intellij.vcs.log.Hash
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import git4idea.GitBranch
|
||||
@@ -44,7 +44,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
@@ -55,9 +55,7 @@ import org.jetbrains.plugins.gitlab.GitLabProjectsManager
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestState
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabProject
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.util.GitLabMergeRequestReviewersUtil
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabBundle
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabCoroutineUtil
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabProjectMapping
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabStatistics
|
||||
|
||||
@@ -70,7 +68,7 @@ internal interface GitLabMergeRequestCreateViewModel : CodeReviewTitleDescriptio
|
||||
|
||||
val isBusy: Flow<Boolean>
|
||||
|
||||
val allowsMultipleReviewers: Flow<Boolean>
|
||||
val allowsMultipleReviewers: StateFlow<Boolean>
|
||||
val branchState: Flow<BranchState?>
|
||||
|
||||
val existingMergeRequest: Flow<String?>
|
||||
@@ -80,14 +78,14 @@ internal interface GitLabMergeRequestCreateViewModel : CodeReviewTitleDescriptio
|
||||
val reviewRequirementsErrorState: Flow<MergeRequestRequirementsErrorType?>
|
||||
val reviewCreatingError: Flow<Throwable?>
|
||||
|
||||
val potentialReviewers: Flow<Result<List<GitLabUserDTO>>>
|
||||
val adjustedReviewers: StateFlow<List<GitLabUserDTO>>
|
||||
val projectMembers: StateFlow<IncrementallyComputedValue<List<GitLabUserDTO>>>
|
||||
val reviewers: StateFlow<List<GitLabUserDTO>>
|
||||
|
||||
val openReviewTabAction: suspend (mrIid: String) -> Unit
|
||||
|
||||
fun updateBranchState(state: BranchState?)
|
||||
|
||||
fun adjustReviewer(point: RelativePoint)
|
||||
fun setReviewers(reviewers: List<GitLabUserDTO>)
|
||||
|
||||
fun createMergeRequest()
|
||||
}
|
||||
@@ -107,14 +105,14 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
|
||||
override val isBusy: Flow<Boolean> = taskLauncher.busy
|
||||
|
||||
override val allowsMultipleReviewers: Flow<Boolean> = suspend {
|
||||
override val allowsMultipleReviewers: StateFlow<Boolean> = suspend {
|
||||
try {
|
||||
projectData.isMultipleReviewersAllowed()
|
||||
}
|
||||
catch (e: Exception) {
|
||||
catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}.asFlow()
|
||||
}.asFlow().stateIn(cs, SharingStarted.Eagerly, false)
|
||||
|
||||
private val listenableProgressIndicator = ListenableProgressIndicator()
|
||||
override val creatingProgressText: Flow<String?> = callbackFlow {
|
||||
@@ -183,11 +181,13 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
private val _reviewCreatingError: MutableStateFlow<Throwable?> = MutableStateFlow(null)
|
||||
override val reviewCreatingError: StateFlow<Throwable?> = _reviewCreatingError.asStateFlow()
|
||||
|
||||
override val potentialReviewers: Flow<Result<List<GitLabUserDTO>>> =
|
||||
GitLabCoroutineUtil.batchesResultsFlow(projectData.dataReloadSignal, projectData::getMembersBatches)
|
||||
override val projectMembers: StateFlow<IncrementallyComputedValue<List<GitLabUserDTO>>> =
|
||||
projectData.dataReloadSignal.withInitial(Unit).transformLatest {
|
||||
projectData.getMembersBatches().collectIncrementallyTo(this)
|
||||
}.stateIn(cs, SharingStarted.Lazily, IncrementallyComputedValue.loading())
|
||||
|
||||
private val _adjustedReviewers: MutableStateFlow<List<GitLabUserDTO>> = MutableStateFlow(listOf())
|
||||
override val adjustedReviewers: StateFlow<List<GitLabUserDTO>> = _adjustedReviewers.asStateFlow()
|
||||
private val _reviewers: MutableStateFlow<List<GitLabUserDTO>> = MutableStateFlow(listOf())
|
||||
override val reviewers: StateFlow<List<GitLabUserDTO>> = _reviewers.asStateFlow()
|
||||
|
||||
private val _title: MutableStateFlow<String> = MutableStateFlow("")
|
||||
override val titleText: StateFlow<String> = _title.asStateFlow()
|
||||
@@ -249,20 +249,9 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
_branchState.value = state
|
||||
}
|
||||
|
||||
override fun adjustReviewer(point: RelativePoint) {
|
||||
cs.launchNow(Dispatchers.Main) {
|
||||
val allowsMultipleReviewers = allowsMultipleReviewers.first()
|
||||
val updatedReviewers = if (allowsMultipleReviewers) {
|
||||
GitLabMergeRequestReviewersUtil.selectReviewers(point, adjustedReviewers.value, potentialReviewers, avatarIconProvider)
|
||||
}
|
||||
else {
|
||||
GitLabMergeRequestReviewersUtil.selectReviewer(point, potentialReviewers, avatarIconProvider)?.let { listOfNotNull(it) }
|
||||
}
|
||||
|
||||
updatedReviewers ?: return@launchNow
|
||||
_adjustedReviewers.value = updatedReviewers
|
||||
GitLabStatistics.logMrCreationReviewersAdjusted(project)
|
||||
}
|
||||
override fun setReviewers(reviewers: List<GitLabUserDTO>) {
|
||||
_reviewers.value = reviewers
|
||||
GitLabStatistics.logMrCreationReviewersAdjusted(project)
|
||||
}
|
||||
|
||||
override fun createMergeRequest() {
|
||||
@@ -277,7 +266,7 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
targetBranch = baseBranch.nameForRemoteOperations,
|
||||
title = titleText.value.ifBlank { gitRemoteBranch.nameForRemoteOperations },
|
||||
description = descriptionText.value.ifBlank { null },
|
||||
reviewers = adjustedReviewers.value
|
||||
reviewers = reviewers.value
|
||||
)
|
||||
openReviewTabAction(mergeRequest.iid)
|
||||
onReviewCreated()
|
||||
|
||||
+3
-3
@@ -45,7 +45,7 @@ import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestSub
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestSubmitReviewViewModel.SubmittableReview
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestSubmitReviewViewModelImpl
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.getSubmittableReview
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.util.GitLabMergeRequestReviewersUtil
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.util.GitLabMergeRequestChoosersUtil
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabBundle
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabCoroutineUtil
|
||||
|
||||
@@ -275,10 +275,10 @@ internal class GitLabMergeRequestReviewFlowViewModelImpl(
|
||||
scope.launchNow(Dispatchers.Main) {
|
||||
val allowsMultipleReviewers = allowsMultipleReviewers.first()
|
||||
val updatedReviewers = if (allowsMultipleReviewers) {
|
||||
GitLabMergeRequestReviewersUtil.selectReviewers(point, reviewers.value, potentialReviewers, avatarIconsProvider)
|
||||
GitLabMergeRequestChoosersUtil.chooseUsers(point, reviewers.value, potentialReviewers, avatarIconsProvider)
|
||||
}
|
||||
else {
|
||||
GitLabMergeRequestReviewersUtil.selectReviewer(point, potentialReviewers, avatarIconsProvider)?.let { listOfNotNull(it) }
|
||||
GitLabMergeRequestChoosersUtil.chooseUser(point, potentialReviewers, avatarIconsProvider)?.let { listOfNotNull(it) }
|
||||
}
|
||||
|
||||
updatedReviewers ?: return@launchNow
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// 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.util
|
||||
|
||||
import com.intellij.collaboration.ui.codereview.avatar.Avatar
|
||||
import com.intellij.collaboration.ui.codereview.list.search.ChooserPopupUtil
|
||||
import com.intellij.collaboration.ui.codereview.list.search.PopupConfig
|
||||
import com.intellij.collaboration.ui.codereview.list.search.ShowDirection
|
||||
import com.intellij.collaboration.ui.icon.IconsProvider
|
||||
import com.intellij.collaboration.ui.util.popup.PopupItemPresentation
|
||||
import com.intellij.collaboration.util.IncrementallyComputedValue
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
|
||||
|
||||
internal object GitLabMergeRequestChoosersUtil {
|
||||
suspend fun chooseUser(
|
||||
point: RelativePoint,
|
||||
users: Flow<Result<List<GitLabUserDTO>>>,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
): GitLabUserDTO? {
|
||||
return ChooserPopupUtil.showAsyncChooserPopup(
|
||||
point,
|
||||
users,
|
||||
presenter = { reviewer ->
|
||||
PopupItemPresentation.Simple(
|
||||
reviewer.username,
|
||||
avatarIconsProvider.getIcon(reviewer, Avatar.Sizes.BASE),
|
||||
reviewer.name,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun chooseUsers(
|
||||
point: RelativePoint,
|
||||
chosenUsers: List<GitLabUserDTO>,
|
||||
users: Flow<Result<List<GitLabUserDTO>>>,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
): List<GitLabUserDTO> {
|
||||
val usersBatch = flow {
|
||||
val batch = users.first()
|
||||
emit(batch)
|
||||
}
|
||||
return ChooserPopupUtil.showAsyncMultipleChooserPopup(
|
||||
point,
|
||||
chosenUsers,
|
||||
usersBatch,
|
||||
presenter = { reviewer ->
|
||||
PopupItemPresentation.Simple(
|
||||
reviewer.username,
|
||||
avatarIconsProvider.getIcon(reviewer, Avatar.Sizes.BASE),
|
||||
reviewer.name,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun chooseUser(
|
||||
point: RelativePoint,
|
||||
users: StateFlow<IncrementallyComputedValue<List<GitLabUserDTO>>>,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
showDirection: ShowDirection = ShowDirection.BELOW,
|
||||
): GitLabUserDTO? =
|
||||
ChooserPopupUtil.showChooserPopupWithIncrementalLoading(
|
||||
point,
|
||||
users,
|
||||
presenter = { reviewer ->
|
||||
PopupItemPresentation.Simple(
|
||||
reviewer.username,
|
||||
avatarIconsProvider.getIcon(reviewer, Avatar.Sizes.BASE),
|
||||
reviewer.name,
|
||||
)
|
||||
},
|
||||
PopupConfig.DEFAULT.copy(showDirection = showDirection)
|
||||
)
|
||||
|
||||
suspend fun chooseUsers(
|
||||
point: RelativePoint,
|
||||
choseUsers: List<GitLabUserDTO>,
|
||||
users: StateFlow<IncrementallyComputedValue<List<GitLabUserDTO>>>,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
showDirection: ShowDirection = ShowDirection.BELOW,
|
||||
): List<GitLabUserDTO> =
|
||||
ChooserPopupUtil.showMultipleChooserPopupWithIncrementalLoading(
|
||||
point,
|
||||
choseUsers,
|
||||
users,
|
||||
presenter = { reviewer ->
|
||||
PopupItemPresentation.Simple(
|
||||
reviewer.username,
|
||||
avatarIconsProvider.getIcon(reviewer, Avatar.Sizes.BASE),
|
||||
reviewer.name,
|
||||
)
|
||||
},
|
||||
PopupConfig.DEFAULT.copy(showDirection = showDirection)
|
||||
)
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
// 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.util
|
||||
|
||||
import com.intellij.collaboration.ui.codereview.avatar.Avatar
|
||||
import com.intellij.collaboration.ui.codereview.list.search.ChooserPopupUtil
|
||||
import com.intellij.collaboration.ui.icon.IconsProvider
|
||||
import com.intellij.collaboration.ui.util.popup.PopupItemPresentation
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
|
||||
|
||||
internal object GitLabMergeRequestReviewersUtil {
|
||||
suspend fun selectReviewer(
|
||||
point: RelativePoint,
|
||||
potentialReviewers: Flow<Result<List<GitLabUserDTO>>>,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
): GitLabUserDTO? {
|
||||
return ChooserPopupUtil.showAsyncChooserPopup(
|
||||
point,
|
||||
potentialReviewers,
|
||||
presenter = { reviewer ->
|
||||
PopupItemPresentation.Simple(
|
||||
reviewer.username,
|
||||
avatarIconsProvider.getIcon(reviewer, Avatar.Sizes.BASE),
|
||||
reviewer.name,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun selectReviewers(
|
||||
point: RelativePoint,
|
||||
originalReviewers: List<GitLabUserDTO>,
|
||||
potentialReviewers: Flow<Result<List<GitLabUserDTO>>>,
|
||||
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
|
||||
): List<GitLabUserDTO> {
|
||||
val potentialReviewersBatch = flow {
|
||||
val batch = potentialReviewers.first()
|
||||
emit(batch)
|
||||
}
|
||||
return ChooserPopupUtil.showAsyncMultipleChooserPopup(
|
||||
point,
|
||||
originalReviewers,
|
||||
potentialReviewersBatch,
|
||||
presenter = { reviewer ->
|
||||
PopupItemPresentation.Simple(
|
||||
reviewer.username,
|
||||
avatarIconsProvider.getIcon(reviewer, Avatar.Sizes.BASE),
|
||||
reviewer.name,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user