[gitlab]: ability to set labels for new merge requests

IJPL-84272

GitOrigin-RevId: a1f80dd23994c28be6963b33ba01bc1b8d699977
This commit is contained in:
Ivan Semenov
2026-02-13 14:11:18 +00:00
committed by intellij-monorepo-bot
parent a17bd75480
commit 8e0f5ef8fb
12 changed files with 132 additions and 12 deletions
@@ -265,7 +265,15 @@ f:com.intellij.collaboration.api.json.JsonHttpApiHelperKt
- f:attachSearch(javax.swing.JList,com.intellij.ui.SearchTextField,kotlin.jvm.functions.Function1):V
- f:cleanupUrl(java.lang.String):java.lang.String
- f:createTagLabel(com.intellij.collaboration.ui.SingleValueModel):javax.swing.JComponent
- f:createTagLabel(com.intellij.collaboration.ui.SingleValueModel,java.awt.Color):javax.swing.JComponent
- f:createTagLabel(com.intellij.collaboration.ui.SingleValueModel,java.awt.Color,java.awt.Color):javax.swing.JComponent
- f:createTagLabel(com.intellij.collaboration.ui.SingleValueModel,java.awt.Color,java.awt.Color,Z):javax.swing.JComponent
- f:createTagLabel(java.lang.String):javax.swing.JComponent
- f:createTagLabel(java.lang.String,java.awt.Color):javax.swing.JComponent
- f:createTagLabel(java.lang.String,java.awt.Color,java.awt.Color):javax.swing.JComponent
- f:createTagLabel(java.lang.String,java.awt.Color,java.awt.Color,Z):javax.swing.JComponent
- bs:createTagLabel$default(com.intellij.collaboration.ui.CollaborationToolsUIUtil,com.intellij.collaboration.ui.SingleValueModel,java.awt.Color,java.awt.Color,Z,I,java.lang.Object):javax.swing.JComponent
- bs:createTagLabel$default(com.intellij.collaboration.ui.CollaborationToolsUIUtil,java.lang.String,java.awt.Color,java.awt.Color,Z,I,java.lang.Object):javax.swing.JComponent
- f:defaultButton(javax.swing.JButton):javax.swing.JButton
- f:focusPanel(javax.swing.JComponent):V
- f:getAnimatedLoadingIcon():javax.swing.Icon
@@ -308,20 +308,35 @@ object CollaborationToolsUIUtil {
* A text label with a rounded rectangle as a background
* To be used for various tags and badges
*/
fun createTagLabel(text: @Nls String): JComponent = createTagLabel(SingleValueModel(text))
@JvmOverloads
fun createTagLabel(
text: @Nls String,
textColor: Color = CodeReviewColorUtil.Review.stateForeground,
backgroundColor: Color = CodeReviewColorUtil.Review.stateBackground,
compact: Boolean = true,
): JComponent = createTagLabel(SingleValueModel(text), textColor, backgroundColor, compact)
fun createTagLabel(model: SingleValueModel<@Nls String?>): JComponent =
@JvmOverloads
fun createTagLabel(
model: SingleValueModel<@Nls String?>,
textColor: Color = CodeReviewColorUtil.Review.stateForeground,
backgroundColor: Color = CodeReviewColorUtil.Review.stateBackground,
compact: Boolean = true,
): JComponent =
JLabel(model.value).apply {
font = JBFont.small()
foreground = CodeReviewColorUtil.Review.stateForeground
if (compact) {
font = JBFont.small()
}
foreground = textColor
border = JBUI.Borders.empty(0, 4)
model.addListener {
text = it
}
}.let {
BackgroundRoundedPanel(4, SingleComponentCenteringLayout()).apply {
fillBorder = false
border = JBUI.Borders.empty()
background = CodeReviewColorUtil.Review.stateBackground
background = backgroundColor
add(it)
}
}
@@ -7,7 +7,6 @@ import com.intellij.icons.AllIcons
import com.intellij.openapi.util.NlsContexts
import com.intellij.util.ui.InlineIconButton
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.JBUI.Panels.simplePanel
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.WrapLayout
import com.intellij.util.ui.launchOnShow
@@ -108,7 +107,10 @@ object LabeledListComponentsFactory {
}
val lastItem = newList.last()
// attach controls to the last items so they are moved to the next line together
val itemWithControls = simplePanel(itemComponentFactory(lastItem)).andTransparent().addToRight(editButton)
val itemWithControls = HorizontalListPanel().apply {
add(itemComponentFactory(lastItem))
add(editButton)
}
add(itemWithControls)
}
revalidate()
@@ -2,17 +2,25 @@
package com.intellij.ui.components.panels
import com.intellij.util.ui.GraphicsUtil
import com.intellij.util.ui.JBInsets
import org.jetbrains.annotations.ApiStatus
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.LayoutManager
import javax.swing.JPanel
import kotlin.properties.Delegates.observable
//TODO: provide an external API
@ApiStatus.Internal
open class BackgroundRoundedPanel(private val arcSize: Int, layoutManager: LayoutManager? = null) : JPanel(layoutManager) {
var fillBorder: Boolean by observable(true) { _, _, _ -> repaint() }
init {
isOpaque = false
super.isOpaque = false
}
final override fun setOpaque(isOpaque: Boolean) {
// this panel is always non-opaque because of rounding
}
override fun paintComponent(g: Graphics) {
@@ -20,9 +28,13 @@ open class BackgroundRoundedPanel(private val arcSize: Int, layoutManager: Layou
//java.awt.Graphics2D#clip provides worse painting result than explicit rounded rectangle painting
val g2 = g.create() as Graphics2D
try {
val rect = bounds
if (!fillBorder) {
JBInsets.removeFrom(rect, insets)
}
GraphicsUtil.setupAAPainting(g2)
g2.color = background
g2.fillRoundRect(0, 0, width, height, arcSize, arcSize)
g2.fillRoundRect(rect.x, rect.y, rect.width, rect.height, arcSize, arcSize)
}
finally {
g2.dispose()
@@ -134,6 +134,8 @@ merge.request.create.no.reviewers=No reviewers
merge.request.create.reviewers=Reviewers:
merge.request.create.no.assignees=No assignees
merge.request.create.assignees=Assignees:
merge.request.create.no.labels=No labels
merge.request.create.labels=Labels:
merge.request.create.tab.title=New Merge Request at {0}
merge.request.create.title.placeholder=Title
merge.request.create.description.placeholder=Description
@@ -49,6 +49,7 @@ suspend fun GitLabApi.Rest.createMergeRequest(
description: String? = null,
reviewerIds: List<String>? = null,
assigneeIds: List<String>? = null,
labels: List<String>? = null
): HttpResponse<out GitLabMergeRequestShortRestDTO> {
val uri = project.restApiUri
.resolveRelative("merge_requests")
@@ -59,6 +60,7 @@ suspend fun GitLabApi.Rest.createMergeRequest(
"description" eq description
"reviewer_ids" eq reviewerIds
"assignee_ids" eq assigneeIds
"labels" eq labels
}
val request = request(uri).POST(HttpRequest.BodyPublishers.noBody()).build()
return withErrorStats(GitLabApiRequestName.REST_CREATE_MERGE_REQUEST) {
@@ -1,8 +1,10 @@
// Copyright 2000-2026 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.util.NlsSafe
class GitLabLabel(
val title: String,
val title: @NlsSafe String,
val colorHex: String,
) {
override fun equals(other: Any?): Boolean {
@@ -90,6 +90,7 @@ interface GitLabProject {
description: String?,
reviewers: List<GitLabUserDTO> = emptyList(),
assignees: List<GitLabUserDTO> = emptyList(),
labels: List<GitLabLabel> = emptyList(),
): GitLabMergeRequestDTO
fun reloadData()
@@ -181,10 +182,12 @@ class GitLabLazyProject(
description: String?,
reviewers: List<GitLabUserDTO>,
assignees: List<GitLabUserDTO>,
labels: List<GitLabLabel>,
): GitLabMergeRequestDTO {
return cs.async(Dispatchers.IO) {
val reviewerIds = reviewers.nullize()?.map { GitLabGidData(it.id).guessRestId() }
val assigneeIds = assignees.nullize()?.map { GitLabGidData(it.id).guessRestId() }
val labelTitles = labels.nullize()?.map { it.title }
val iid = api.rest.createMergeRequest(
projectCoordinates,
sourceBranch,
@@ -192,7 +195,8 @@ class GitLabLazyProject(
title,
description,
reviewerIds,
assigneeIds
assigneeIds,
labelTitles
).body().iid
val attempts = GitLabRegistry.getRequestPollingAttempts()
repeat(attempts) {
@@ -146,6 +146,7 @@ internal object GitLabMergeRequestCreateComponentFactory {
val lists = buildList {
add(GitLabMergeRequestCreateMetadataComponentFactory.createAssigneesListPanelHandle(createVm))
add(GitLabMergeRequestCreateMetadataComponentFactory.createReviewersListPanelHandle(createVm))
add(GitLabMergeRequestCreateMetadataComponentFactory.createLabelsListPanelHandle(createVm))
}
return LabeledListComponentsFactory.createGrid(lists)
@@ -2,6 +2,7 @@
package org.jetbrains.plugins.gitlab.mergerequest.ui.create
import com.intellij.collaboration.async.mapState
import com.intellij.collaboration.ui.CollaborationToolsUIUtil
import com.intellij.collaboration.ui.LabeledListComponentsFactory
import com.intellij.collaboration.ui.codereview.avatar.Avatar
import com.intellij.collaboration.ui.codereview.list.search.ShowDirection
@@ -12,6 +13,7 @@ import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.flow.StateFlow
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabLabel
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
@@ -97,6 +99,35 @@ internal object GitLabMergeRequestCreateMetadataComponentFactory {
}
return newList
}
fun createLabelsListPanelHandle(
vm: GitLabMergeRequestCreateViewModel,
): Pair<JComponent, JComponent> {
val label = LabeledListComponentsFactory.createLabelPanel(
vm.labels.mapState { it.isEmpty() },
GitLabBundle.message("merge.request.create.no.labels"),
GitLabBundle.message("merge.request.create.labels")
)
val list = LabeledListComponentsFactory.createListPanel(
vm.labels,
{ comp, _ -> chooseLabels(comp, vm) },
{ LabelLabel(it) }
)
return label to list
}
private suspend fun chooseLabels(
parentComponent: JComponent,
vm: GitLabMergeRequestCreateViewModel,
) {
val point = RelativePoint.getNorthEastOf(parentComponent)
val currentLabels = vm.labels.value
val potentialLabels = vm.projectLabels
val newLabels = GitLabMergeRequestChoosersUtil.chooseLabels(point, currentLabels, potentialLabels, ShowDirection.ABOVE)
vm.setLabels(newLabels)
}
}
@Suppress("FunctionName")
@@ -104,3 +135,12 @@ private fun UserLabel(user: GitLabUserDTO, avatarIconsProvider: IconsProvider<Gi
JLabel(user.name, avatarIconsProvider.getIcon(user, Avatar.Sizes.BASE), SwingConstants.LEFT).apply {
border = JBUI.Borders.empty(0, UIUtil.DEFAULT_HGAP / 2)
}
@Suppress("FunctionName")
private fun LabelLabel(label: GitLabLabel): JComponent {
val background = CollaborationToolsUIUtil.getLabelBackground(label.colorHex)
val foreground = CollaborationToolsUIUtil.getLabelForeground(background)
return CollaborationToolsUIUtil.createTagLabel(label.title, foreground, background, compact = false).apply {
border = JBUI.Borders.empty(0, UIUtil.DEFAULT_HGAP / 2)
}
}
@@ -44,6 +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.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
@@ -52,6 +53,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.plugins.gitlab.GitLabProjectsManager
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabLabel
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestState
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabProject
import org.jetbrains.plugins.gitlab.util.GitLabBundle
@@ -82,12 +84,16 @@ internal interface GitLabMergeRequestCreateViewModel : CodeReviewTitleDescriptio
val reviewers: StateFlow<List<GitLabUserDTO>>
val assignees: StateFlow<List<GitLabUserDTO>>
val projectLabels: StateFlow<IncrementallyComputedValue<List<GitLabLabel>>>
val labels: StateFlow<List<GitLabLabel>>
val openReviewTabAction: suspend (mrIid: String) -> Unit
fun updateBranchState(state: BranchState?)
fun setReviewers(reviewers: List<GitLabUserDTO>)
fun setAssignees(assignees: List<GitLabUserDTO>)
fun setLabels(labels: List<GitLabLabel>)
fun createMergeRequest()
}
@@ -203,6 +209,14 @@ internal class GitLabMergeRequestCreateViewModelImpl(
private val _assignees: MutableStateFlow<List<GitLabUserDTO>> = MutableStateFlow(listOf())
override val assignees: StateFlow<List<GitLabUserDTO>> = _assignees.asStateFlow()
override val projectLabels: StateFlow<IncrementallyComputedValue<List<GitLabLabel>>> =
projectData.dataReloadSignal.withInitial(Unit).transformLatest {
projectData.getLabelsBatches().collectIncrementallyTo(this)
}.stateIn(cs, SharingStarted.Lazily, IncrementallyComputedValue.loading())
private val _labels: MutableStateFlow<List<GitLabLabel>> = MutableStateFlow(listOf())
override val labels: StateFlow<List<GitLabLabel>> = _labels.asStateFlow()
private val _title: MutableStateFlow<String> = MutableStateFlow("")
override val titleText: StateFlow<String> = _title.asStateFlow()
@@ -272,6 +286,10 @@ internal class GitLabMergeRequestCreateViewModelImpl(
_assignees.value = assignees
}
override fun setLabels(labels: List<GitLabLabel>) {
_labels.value = labels
}
override fun createMergeRequest() {
taskLauncher.launch {
GitLabStatistics.logMrCreationStarted(project)
@@ -285,7 +303,8 @@ internal class GitLabMergeRequestCreateViewModelImpl(
title = titleText.value.ifBlank { gitRemoteBranch.nameForRemoteOperations },
description = descriptionText.value.ifBlank { null },
reviewers = reviewers.value,
assignees = assignees.value
assignees = assignees.value,
labels = labels.value
)
openReviewTabAction(mergeRequest.iid)
onReviewCreated()
@@ -44,6 +44,19 @@ internal object GitLabMergeRequestChoosersUtil {
PopupConfig.DEFAULT.copy(showDirection = showDirection)
)
suspend fun chooseLabels(
point: RelativePoint,
chosenLabels: List<GitLabLabel>,
potentialLabels: StateFlow<IncrementallyComputedValue<List<GitLabLabel>>>,
showDirection: ShowDirection = ShowDirection.BELOW,
): List<GitLabLabel> = ChooserPopupUtil.showMultipleChooserPopupWithIncrementalLoading(
point,
chosenLabels,
potentialLabels,
presenter = getLabelPresenter(),
PopupConfig.DEFAULT.copy(showDirection = showDirection)
)
fun getUserPresenter(avatarIconsProvider: IconsProvider<GitLabUserDTO>): (GitLabUserDTO) -> PopupItemPresentation {
return {
PopupItemPresentation.Simple(