diff --git a/platform/collaboration-tools/api-dump-experimental.txt b/platform/collaboration-tools/api-dump-experimental.txt index b3d763758133..6773786df763 100644 --- a/platform/collaboration-tools/api-dump-experimental.txt +++ b/platform/collaboration-tools/api-dump-experimental.txt @@ -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 diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/CollaborationToolsUIUtil.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/CollaborationToolsUIUtil.kt index 45f2014f1156..c0b107a1f6f6 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/CollaborationToolsUIUtil.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/CollaborationToolsUIUtil.kt @@ -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) } } diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/LabeledListComponentsFactory.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/LabeledListComponentsFactory.kt index bb45787ed777..a1e923c93d1c 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/LabeledListComponentsFactory.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/LabeledListComponentsFactory.kt @@ -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() diff --git a/platform/platform-api/src/com/intellij/ui/components/panels/BackgroundRoundedPanel.kt b/platform/platform-api/src/com/intellij/ui/components/panels/BackgroundRoundedPanel.kt index 991fd49b14da..f90fd0fe59bf 100644 --- a/platform/platform-api/src/com/intellij/ui/components/panels/BackgroundRoundedPanel.kt +++ b/platform/platform-api/src/com/intellij/ui/components/panels/BackgroundRoundedPanel.kt @@ -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() diff --git a/plugins/gitlab/gitlab-core/resources/messages/GitLabBundle.properties b/plugins/gitlab/gitlab-core/resources/messages/GitLabBundle.properties index 15435827fba3..bbd0fc45b637 100644 --- a/plugins/gitlab/gitlab-core/resources/messages/GitLabBundle.properties +++ b/plugins/gitlab/gitlab-core/resources/messages/GitLabBundle.properties @@ -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 diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt index 98d35f34afbc..9aef7e45fd31 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/api/request/GitLabMergeRequestsApi.kt @@ -49,6 +49,7 @@ suspend fun GitLabApi.Rest.createMergeRequest( description: String? = null, reviewerIds: List? = null, assigneeIds: List? = null, + labels: List? = null ): HttpResponse { 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) { diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabLabel.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabLabel.kt index 32f4d6fe6525..586088f21788 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabLabel.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabLabel.kt @@ -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 { diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt index b29e92e64687..788ed0c45c51 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/data/GitLabProject.kt @@ -90,6 +90,7 @@ interface GitLabProject { description: String?, reviewers: List = emptyList(), assignees: List = emptyList(), + labels: List = emptyList(), ): GitLabMergeRequestDTO fun reloadData() @@ -181,10 +182,12 @@ class GitLabLazyProject( description: String?, reviewers: List, assignees: List, + labels: List, ): 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) { diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateComponentFactory.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateComponentFactory.kt index f151655e34ef..94ff53c898fd 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateComponentFactory.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateComponentFactory.kt @@ -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) diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateMetadataComponentFactory.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateMetadataComponentFactory.kt index 640cba1e7be4..18eb0521b25d 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateMetadataComponentFactory.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/ui/create/GitLabMergeRequestCreateMetadataComponentFactory.kt @@ -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 { + 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> val assignees: StateFlow> + val projectLabels: StateFlow>> + val labels: StateFlow> + val openReviewTabAction: suspend (mrIid: String) -> Unit fun updateBranchState(state: BranchState?) fun setReviewers(reviewers: List) fun setAssignees(assignees: List) + fun setLabels(labels: List) fun createMergeRequest() } @@ -203,6 +209,14 @@ internal class GitLabMergeRequestCreateViewModelImpl( private val _assignees: MutableStateFlow> = MutableStateFlow(listOf()) override val assignees: StateFlow> = _assignees.asStateFlow() + override val projectLabels: StateFlow>> = + projectData.dataReloadSignal.withInitial(Unit).transformLatest { + projectData.getLabelsBatches().collectIncrementallyTo(this) + }.stateIn(cs, SharingStarted.Lazily, IncrementallyComputedValue.loading()) + + private val _labels: MutableStateFlow> = MutableStateFlow(listOf()) + override val labels: StateFlow> = _labels.asStateFlow() + private val _title: MutableStateFlow = MutableStateFlow("") override val titleText: StateFlow = _title.asStateFlow() @@ -272,6 +286,10 @@ internal class GitLabMergeRequestCreateViewModelImpl( _assignees.value = assignees } + override fun setLabels(labels: List) { + _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() diff --git a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/util/GitLabMergeRequestChoosersUtil.kt b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/util/GitLabMergeRequestChoosersUtil.kt index fc5a0a22f263..5b587eea0cfe 100644 --- a/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/util/GitLabMergeRequestChoosersUtil.kt +++ b/plugins/gitlab/gitlab-core/src/org/jetbrains/plugins/gitlab/mergerequest/util/GitLabMergeRequestChoosersUtil.kt @@ -44,6 +44,19 @@ internal object GitLabMergeRequestChoosersUtil { PopupConfig.DEFAULT.copy(showDirection = showDirection) ) + suspend fun chooseLabels( + point: RelativePoint, + chosenLabels: List, + potentialLabels: StateFlow>>, + showDirection: ShowDirection = ShowDirection.BELOW, + ): List = ChooserPopupUtil.showMultipleChooserPopupWithIncrementalLoading( + point, + chosenLabels, + potentialLabels, + presenter = getLabelPresenter(), + PopupConfig.DEFAULT.copy(showDirection = showDirection) + ) + fun getUserPresenter(avatarIconsProvider: IconsProvider): (GitLabUserDTO) -> PopupItemPresentation { return { PopupItemPresentation.Simple(