mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 06:50:54 +07:00
[gitlab] Add a button to MR creation to generate a title
GitOrigin-RevId: 53a9cb6bc2e0827d80d434d47482a2584d4ccb4c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
34dc69e116
commit
aa2c2605c5
@@ -3,20 +3,29 @@ package com.intellij.collaboration.ui.codereview.create
|
||||
|
||||
import com.intellij.collaboration.ui.CollaborationToolsUIUtil
|
||||
import com.intellij.collaboration.ui.codereview.comment.CodeReviewMarkdownEditor
|
||||
import com.intellij.openapi.actionSystem.ActionToolbar
|
||||
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.colors.EditorColorsManager
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.ui.JBColor
|
||||
import com.intellij.ui.components.JBLayeredPane
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.Insets
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.SpringLayout
|
||||
import javax.swing.border.AbstractBorder
|
||||
import javax.swing.text.AttributeSet
|
||||
import javax.swing.text.PlainDocument
|
||||
|
||||
@@ -67,6 +76,53 @@ object CodeReviewCreateReviewUIUtil {
|
||||
}
|
||||
|
||||
fun createCommitListCellRenderer(): ListCellRenderer<VcsCommitMetadata> = CodeReviewTwoLinesCommitRenderer()
|
||||
|
||||
@ApiStatus.Internal
|
||||
fun createGenerationToolbarOverlay(editorPanel: JComponent, toolbar: ActionToolbar, getSpacerWidth: (() -> Int)? = null): JComponent {
|
||||
val buttonPanel = toolbar.component.apply {
|
||||
border = JBUI.Borders.empty()
|
||||
isOpaque = false
|
||||
putClientProperty(ActionToolbarImpl.IMPORTANT_TOOLBAR_KEY, true)
|
||||
}
|
||||
|
||||
return JBLayeredPane().apply {
|
||||
val layout = SpringLayout()
|
||||
this.layout = layout
|
||||
|
||||
layout.putConstraint(SpringLayout.NORTH, editorPanel, 0, SpringLayout.NORTH, this)
|
||||
layout.putConstraint(SpringLayout.SOUTH, editorPanel, 0, SpringLayout.SOUTH, this)
|
||||
layout.putConstraint(SpringLayout.EAST, editorPanel, 0, SpringLayout.EAST, this)
|
||||
layout.putConstraint(SpringLayout.WEST, editorPanel, 0, SpringLayout.WEST, this)
|
||||
add(editorPanel, 1 as Any)
|
||||
|
||||
layout.putConstraint(SpringLayout.SOUTH, buttonPanel, 0, SpringLayout.SOUTH, this)
|
||||
|
||||
if (getSpacerWidth != null) {
|
||||
// Needed to avoid overlapping with the editor's vertical scrollbar
|
||||
val spacer = JPanel().apply {
|
||||
isOpaque = false
|
||||
isEnabled = false
|
||||
border = object : AbstractBorder() {
|
||||
override fun getBorderInsets(c: Component?, insets: Insets): Insets {
|
||||
super.getBorderInsets(c, insets)
|
||||
insets.right = getSpacerWidth()
|
||||
return insets
|
||||
}
|
||||
}
|
||||
}
|
||||
layout.putConstraint(SpringLayout.SOUTH, spacer, 0, SpringLayout.SOUTH, this)
|
||||
layout.putConstraint(SpringLayout.EAST, spacer, 0, SpringLayout.EAST, this)
|
||||
add(spacer, 0 as Any)
|
||||
|
||||
layout.putConstraint(SpringLayout.EAST, buttonPanel, 0, SpringLayout.WEST, spacer)
|
||||
}
|
||||
else {
|
||||
layout.putConstraint(SpringLayout.EAST, buttonPanel, 0, SpringLayout.EAST, this)
|
||||
}
|
||||
|
||||
add(buttonPanel, 2 as Any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
|
||||
@@ -43,6 +43,12 @@
|
||||
<module name="intellij.platform.collaborationTools"/>
|
||||
</dependencies>
|
||||
|
||||
<extensionPoints>
|
||||
<extensionPoint qualifiedName="intellij.vcs.gitlab.titleGenerator"
|
||||
interface="org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.GitLabTitleGeneratorExtension"
|
||||
dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<applicationService serviceImplementation="org.jetbrains.plugins.gitlab.authentication.accounts.GitLabPersistentAccounts"/>
|
||||
<applicationService serviceInterface="org.jetbrains.plugins.gitlab.authentication.accounts.GitLabAccountManager"
|
||||
@@ -243,5 +249,7 @@
|
||||
</action>
|
||||
<add-to-group group-id="Git.Experimental.Branch.Popup.Actions"/>
|
||||
</group>
|
||||
|
||||
<group id="GitLab.Merge.Request.Create.Title.Actions"/>
|
||||
</actions>
|
||||
</idea-plugin>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.create
|
||||
|
||||
import com.intellij.collaboration.async.extensionListFlow
|
||||
import com.intellij.collaboration.async.launchNow
|
||||
import com.intellij.collaboration.ui.CollaborationToolsUIUtil
|
||||
import com.intellij.collaboration.ui.SingleValueModel
|
||||
@@ -8,16 +9,20 @@ import com.intellij.collaboration.ui.bindValueIn
|
||||
import com.intellij.collaboration.ui.codereview.commits.CommitsBrowserComponentBuilder
|
||||
import com.intellij.collaboration.ui.codereview.create.CodeReviewCreateReviewLayoutBuilder
|
||||
import com.intellij.collaboration.ui.codereview.create.CodeReviewCreateReviewUIUtil
|
||||
import com.intellij.collaboration.ui.util.bindContentIn
|
||||
import com.intellij.collaboration.ui.util.bindTextIn
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.UiDataProvider
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.ui.components.panels.Wrapper
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import git4idea.ui.branch.MergeDirectionComponentFactory
|
||||
import git4idea.ui.branch.MergeDirectionModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.BranchState
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.GitLabMergeRequestCreateDirectionModel
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.GitLabMergeRequestCreateViewModel
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.*
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabBundle
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabProjectMapping
|
||||
import org.jetbrains.plugins.gitlab.util.GitLabStatistics
|
||||
@@ -45,14 +50,7 @@ internal object GitLabMergeRequestCreateComponentFactory {
|
||||
|
||||
val directionSelector = createDirectionSelector(directionModel)
|
||||
val commitsLoadingPanel = createCommitsPanel(project, cs, createVm)
|
||||
val titleField = CodeReviewCreateReviewUIUtil.createTitleEditor(GitLabBundle.message("merge.request.create.title.placeholder")).apply {
|
||||
document.addDocumentListener(object : DocumentListener {
|
||||
fun updateText() = createVm.updateTitle(text)
|
||||
override fun insertUpdate(e: DocumentEvent?) = updateText()
|
||||
override fun removeUpdate(e: DocumentEvent?) = updateText()
|
||||
override fun changedUpdate(e: DocumentEvent?) = updateText()
|
||||
})
|
||||
}
|
||||
val titlePanel = cs.createTitleEditorPanel(createVm)
|
||||
val reviewersPanel = GitLabMergeRequestCreateReviewersComponentFactory.create(cs, createVm)
|
||||
val statusPanel = GitLabMergeRequestCreateStatusComponentFactory.create(cs, createVm)
|
||||
val actionsPanel = GitLabMergeRequestCreateActionsComponentFactory.create(project, cs, createVm)
|
||||
@@ -61,15 +59,70 @@ internal object GitLabMergeRequestCreateComponentFactory {
|
||||
.addComponent(directionSelector, zeroMinWidth = true)
|
||||
.addComponent(commitsLoadingPanel, stretchYWithWeight = 0.5f, withoutBorder = true)
|
||||
.addSeparator()
|
||||
.addComponent(titleField, zeroMinWidth = true)
|
||||
.addComponent(titlePanel, stretchYWithWeight = 0.3f, zeroMinWidth = true)
|
||||
.addSeparator()
|
||||
.addComponent(reviewersPanel, zeroMinWidth = true, stretchYWithWeight = 0.3f)
|
||||
.addComponent(reviewersPanel, zeroMinWidth = true, stretchYWithWeight = 0.2f)
|
||||
.addSeparator()
|
||||
.addComponent(statusPanel)
|
||||
.addComponent(actionsPanel, withListBackground = false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun CoroutineScope.createTitleEditorPanel(createVm: GitLabMergeRequestCreateViewModel): JComponent {
|
||||
val cs = this
|
||||
val editor = CodeReviewCreateReviewUIUtil.createTitleEditor(GitLabBundle.message("merge.request.create.title.placeholder")).apply {
|
||||
document.addDocumentListener(object : DocumentListener {
|
||||
fun updateText() = createVm.updateTitle(text)
|
||||
override fun insertUpdate(e: DocumentEvent?) = updateText()
|
||||
override fun removeUpdate(e: DocumentEvent?) = updateText()
|
||||
override fun changedUpdate(e: DocumentEvent?) = updateText()
|
||||
})
|
||||
bindTextIn(cs, createVm.title)
|
||||
}
|
||||
|
||||
return Wrapper().apply {
|
||||
bindContentIn(cs, GitLabTitleGeneratorExtension.EP_NAME.extensionListFlow()) { extensions ->
|
||||
if (extensions.isEmpty()) {
|
||||
editor
|
||||
}
|
||||
else {
|
||||
wrapTitleEditorWithGenerateActions(createVm, editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.wrapTitleEditorWithGenerateActions(
|
||||
createVm: GitLabMergeRequestCreateViewModel,
|
||||
editor: JComponent,
|
||||
): JComponent {
|
||||
val wrappedEditor = UiDataProvider.wrapComponent(editor) {
|
||||
it[GitLabMergeRequestCreateTitleGenerationViewModel.DATA_KEY] = createVm.titleGenerationVm.value
|
||||
}
|
||||
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val actionGroup = actionManager.getAction("GitLab.Merge.Request.Create.Title.Actions") as ActionGroup
|
||||
val toolbar = actionManager.createActionToolbar("MrCreationTitle", actionGroup, true).apply {
|
||||
targetComponent = wrappedEditor
|
||||
}
|
||||
|
||||
// Force an action's update with new values for commits and generating state
|
||||
launchNow {
|
||||
createVm.titleGenerationVm.collect {
|
||||
if (it == null) {
|
||||
toolbar.updateActionsAsync()
|
||||
return@collect
|
||||
}
|
||||
|
||||
it.isGenerating.collect {
|
||||
toolbar.updateActionsAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CodeReviewCreateReviewUIUtil.createGenerationToolbarOverlay(wrappedEditor, toolbar)
|
||||
}
|
||||
|
||||
private fun createDirectionSelector(directionModel: MergeDirectionModel<GitLabProjectMapping>): JComponent {
|
||||
return MergeDirectionComponentFactory(
|
||||
directionModel,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2000-2024 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.model
|
||||
|
||||
import com.intellij.collaboration.util.SingleCoroutineLauncher
|
||||
import com.intellij.openapi.actionSystem.DataKey
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.vcs.log.VcsCommitMetadata
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.GitLabTitleGeneratorExtension.GenerationError
|
||||
import org.jetbrains.plugins.gitlab.mergerequest.ui.create.model.GitLabTitleGeneratorExtension.GenerationStep
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface GitLabTitleGeneratorExtension {
|
||||
companion object {
|
||||
val EP_NAME = ExtensionPointName<GitLabTitleGeneratorExtension>("intellij.vcs.gitlab.titleGenerator")
|
||||
}
|
||||
|
||||
sealed interface GenerationState
|
||||
data class GenerationError(val e: Exception) : GenerationState
|
||||
data class GenerationStep(val title: String) : GenerationState
|
||||
|
||||
fun generate(project: Project, commits: List<VcsCommitMetadata>): Flow<GenerationState>
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
interface GitLabMergeRequestCreateTitleGenerationViewModel {
|
||||
companion object {
|
||||
val DATA_KEY = DataKey.create<GitLabMergeRequestCreateTitleGenerationViewModel>("GitLabCreateTitleGenerationViewModel")
|
||||
}
|
||||
|
||||
val isGenerating: StateFlow<Boolean>
|
||||
|
||||
fun stopGeneration()
|
||||
fun startGeneration()
|
||||
}
|
||||
|
||||
internal class GitLabMergeRequestCreateTitleGenerationViewModelImpl(
|
||||
parentCs: CoroutineScope,
|
||||
private val project: Project,
|
||||
private val extension: GitLabTitleGeneratorExtension,
|
||||
private val commits: List<VcsCommitMetadata>,
|
||||
private val setTitle: (String) -> Unit,
|
||||
) : GitLabMergeRequestCreateTitleGenerationViewModel {
|
||||
private val taskLauncher = SingleCoroutineLauncher(parentCs.childScope("Generate Title"))
|
||||
override val isGenerating: StateFlow<Boolean> = taskLauncher.busy
|
||||
|
||||
override fun stopGeneration() {
|
||||
taskLauncher.cancel()
|
||||
}
|
||||
|
||||
override fun startGeneration() {
|
||||
taskLauncher.launch {
|
||||
extension.generate(project, commits).collect {
|
||||
when (it) {
|
||||
is GenerationError -> throw it.e
|
||||
is GenerationStep -> {
|
||||
setTitle(it.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@
|
||||
package org.jetbrains.plugins.gitlab.mergerequest.ui.create.model
|
||||
|
||||
import com.intellij.collaboration.api.HttpStatusErrorException
|
||||
import com.intellij.collaboration.async.launchNow
|
||||
import com.intellij.collaboration.async.modelFlow
|
||||
import com.intellij.collaboration.async.withInitial
|
||||
import com.intellij.collaboration.async.*
|
||||
import com.intellij.collaboration.ui.ListenableProgressIndicator
|
||||
import com.intellij.collaboration.ui.icon.IconsProvider
|
||||
import com.intellij.collaboration.util.ResultUtil.runCatchingUser
|
||||
@@ -43,6 +41,9 @@ internal interface GitLabMergeRequestCreateViewModel {
|
||||
val projectData: GitLabProject
|
||||
val avatarIconProvider: IconsProvider<GitLabUserDTO>
|
||||
|
||||
val title: StateFlow<String>
|
||||
val titleGenerationVm: StateFlow<GitLabMergeRequestCreateTitleGenerationViewModel?>
|
||||
|
||||
val isBusy: Flow<Boolean>
|
||||
|
||||
val allowsMultipleReviewers: Flow<Boolean>
|
||||
@@ -50,7 +51,7 @@ internal interface GitLabMergeRequestCreateViewModel {
|
||||
|
||||
val existingMergeRequest: Flow<String?>
|
||||
val creatingProgressText: Flow<String?>
|
||||
val commits: SharedFlow<Result<List<VcsCommitMetadata>>?>
|
||||
val commits: StateFlow<Result<List<VcsCommitMetadata>>?>
|
||||
|
||||
val reviewRequirementsErrorState: Flow<MergeRequestRequirementsErrorType?>
|
||||
val reviewCreatingError: Flow<Throwable?>
|
||||
@@ -76,7 +77,7 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
override val projectData: GitLabProject,
|
||||
override val avatarIconProvider: IconsProvider<GitLabUserDTO>,
|
||||
override val openReviewTabAction: suspend (mrIid: String) -> Unit,
|
||||
private val onReviewCreated: () -> Unit
|
||||
private val onReviewCreated: () -> Unit,
|
||||
) : GitLabMergeRequestCreateViewModel {
|
||||
private val cs: CoroutineScope = parentCs.childScope()
|
||||
private val taskLauncher = SingleCoroutineLauncher(cs)
|
||||
@@ -128,8 +129,8 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
}
|
||||
.distinctUntilChangedBy { it }
|
||||
|
||||
override val commits: SharedFlow<Result<List<VcsCommitMetadata>>?> = commitRevisionComparisonFlow.transformLatest { revisionComparison ->
|
||||
if (revisionComparison == null || revisionComparison.baseRevision == null || revisionComparison.headRevision == null) {
|
||||
override val commits: StateFlow<Result<List<VcsCommitMetadata>>?> = commitRevisionComparisonFlow.transformLatest { revisionComparison ->
|
||||
if (revisionComparison?.baseRevision == null || revisionComparison.headRevision == null) {
|
||||
emit(Result.success(emptyList()))
|
||||
return@transformLatest
|
||||
}
|
||||
@@ -144,6 +145,7 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
}
|
||||
emit(result)
|
||||
}.modelFlow(cs, thisLogger())
|
||||
.stateInNow(cs, null)
|
||||
|
||||
override val reviewRequirementsErrorState: Flow<MergeRequestRequirementsErrorType?> =
|
||||
combine(branchState, commits) { branchState, commits ->
|
||||
@@ -164,7 +166,19 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
private val _adjustedReviewers: MutableStateFlow<List<GitLabUserDTO>> = MutableStateFlow(listOf())
|
||||
override val adjustedReviewers: StateFlow<List<GitLabUserDTO>> = _adjustedReviewers.asStateFlow()
|
||||
|
||||
private val title: MutableStateFlow<String> = MutableStateFlow("")
|
||||
private val _title: MutableStateFlow<String> = MutableStateFlow("")
|
||||
override val title: StateFlow<String> = _title.asStateFlow()
|
||||
override val titleGenerationVm: StateFlow<GitLabMergeRequestCreateTitleGenerationViewModel?> =
|
||||
commits.combine(GitLabTitleGeneratorExtension.EP_NAME.extensionListFlow()) { commits, extensions ->
|
||||
val commits = commits?.getOrNull() ?: return@combine null
|
||||
if (commits.isEmpty()) return@combine null
|
||||
|
||||
val extension = extensions.firstOrNull() ?: return@combine null
|
||||
|
||||
commits to extension
|
||||
}.mapNullableScoped { (commits, extension) ->
|
||||
GitLabMergeRequestCreateTitleGenerationViewModelImpl(this, project, extension, commits, ::updateTitle)
|
||||
}.stateIn(cs, SharingStarted.Lazily, null)
|
||||
|
||||
init {
|
||||
cs.launch {
|
||||
@@ -182,18 +196,19 @@ internal class GitLabMergeRequestCreateViewModelImpl(
|
||||
val commits = commitsResult?.getOrNull()
|
||||
if (commits != null && commits.size == 1 && title.value.isEmpty()) {
|
||||
updateTitle(commits.first().subject.lines().firstOrNull() ?: return@combine)
|
||||
} else if (title.value.isEmpty()) {
|
||||
}
|
||||
else if (title.value.isEmpty()) {
|
||||
updateTitle(when (val branch = branchState?.headBranch ?: return@combine) {
|
||||
is GitRemoteBranch -> branch.nameForRemoteOperations
|
||||
else -> branch.name
|
||||
})
|
||||
is GitRemoteBranch -> branch.nameForRemoteOperations
|
||||
else -> branch.name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateTitle(text: String) {
|
||||
title.value = text
|
||||
_title.value = text
|
||||
}
|
||||
|
||||
override fun updateBranchState(state: BranchState?) {
|
||||
@@ -269,7 +284,7 @@ internal data class BranchState(
|
||||
val baseRepo: GitLabProjectMapping,
|
||||
val baseBranch: GitRemoteBranch,
|
||||
val headRepo: GitLabProjectMapping,
|
||||
val headBranch: GitBranch
|
||||
val headBranch: GitBranch,
|
||||
) {
|
||||
companion object {
|
||||
internal fun fromDirectionModel(directionModel: GitLabMergeRequestCreateDirectionModel): BranchState? = with(directionModel) {
|
||||
|
||||
Reference in New Issue
Block a user