mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[collab] implement review list quick filters
GitOrigin-RevId: 294f0921ff5f8ec5bc7b41e23a0317bfbfceb882
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9e8ee409fe
commit
2386c62d18
@@ -22,4 +22,6 @@ login.token.generate=Generate\u2026
|
||||
login.progress=Logging in\u2026
|
||||
login.dialog.title=Log In with Access Token
|
||||
review.list.info={0} \u00B7 created {1}
|
||||
review.list.info.author={0} \u00B7 created {1}, by {2}
|
||||
review.list.info.author={0} \u00B7 created {1}, by {2}
|
||||
review.list.filter.quick.title=Quick Filters
|
||||
review.list.filter.quick.clear=Clear {0} Filters
|
||||
+92
-10
@@ -1,21 +1,31 @@
|
||||
// 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.codereview.list.search
|
||||
|
||||
import com.intellij.collaboration.messages.CollaborationToolsBundle
|
||||
import com.intellij.collaboration.ui.codereview.InlineIconButton
|
||||
import com.intellij.collaboration.ui.codereview.list.search.ChooserPopupUtil.showAndAwaitListSubmission
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.DataManager
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DefaultActionGroup
|
||||
import com.intellij.openapi.actionSystem.Separator
|
||||
import com.intellij.openapi.actionSystem.Toggleable
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||
import com.intellij.ui.IdeBorderFactory
|
||||
import com.intellij.ui.ScrollPaneFactory
|
||||
import com.intellij.ui.SideBorder
|
||||
import com.intellij.ui.SimpleListCellRenderer
|
||||
import com.intellij.ui.*
|
||||
import com.intellij.ui.components.GradientViewport
|
||||
import com.intellij.ui.components.JBThinOverlappingScrollBar
|
||||
import com.intellij.ui.components.panels.HorizontalLayout
|
||||
import com.intellij.ui.scale.JBUIScale
|
||||
import com.intellij.util.ui.JBUI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.awt.Adjustable
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.*
|
||||
import java.awt.event.ActionListener
|
||||
import java.awt.geom.Ellipse2D
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
@@ -24,7 +34,7 @@ abstract class ReviewListSearchPanelFactory<S : ReviewListSearchValue, VM : Revi
|
||||
protected val vm: VM
|
||||
) {
|
||||
|
||||
fun create(viewScope: CoroutineScope): JComponent {
|
||||
fun create(viewScope: CoroutineScope, quickFilters: List<Pair<@Nls String, S>>): JComponent {
|
||||
val searchField = ReviewListSearchTextFieldFactory(vm.queryState).create(viewScope, chooseFromHistory = { point ->
|
||||
val value = JBPopupFactory.getInstance()
|
||||
.createPopupChooserBuilder(vm.getSearchHistory().reversed())
|
||||
@@ -41,11 +51,11 @@ abstract class ReviewListSearchPanelFactory<S : ReviewListSearchValue, VM : Revi
|
||||
val filters = createFilters(viewScope)
|
||||
|
||||
val filtersPanel = JPanel(HorizontalLayout(4)).apply {
|
||||
border = JBUI.Borders.emptyTop(10)
|
||||
isOpaque = false
|
||||
filters.forEach { add(it, HorizontalLayout.LEFT) }
|
||||
}.let {
|
||||
ScrollPaneFactory.createScrollPane(it, true).apply {
|
||||
viewport = GradientViewport(it, JBUI.insetsRight(10), false)
|
||||
viewport = GradientViewport(it, JBUI.insets(0, 10), false)
|
||||
|
||||
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS
|
||||
@@ -53,10 +63,19 @@ abstract class ReviewListSearchPanelFactory<S : ReviewListSearchValue, VM : Revi
|
||||
}
|
||||
}
|
||||
|
||||
val quickFilterButton = QuickFilterButtonFactory().create(viewScope, quickFilters)
|
||||
|
||||
val filterPanel = JPanel(BorderLayout()).apply {
|
||||
border = JBUI.Borders.emptyTop(10)
|
||||
isOpaque = false
|
||||
add(quickFilterButton, BorderLayout.WEST)
|
||||
add(filtersPanel, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
val searchPanel = JPanel(BorderLayout()).apply {
|
||||
border = JBUI.Borders.compound(IdeBorderFactory.createBorder(SideBorder.BOTTOM), JBUI.Borders.empty(8, 10, 0, 10))
|
||||
add(searchField, BorderLayout.CENTER)
|
||||
add(filtersPanel, BorderLayout.SOUTH)
|
||||
add(filterPanel, BorderLayout.SOUTH)
|
||||
}
|
||||
|
||||
return searchPanel
|
||||
@@ -65,4 +84,67 @@ abstract class ReviewListSearchPanelFactory<S : ReviewListSearchValue, VM : Revi
|
||||
protected abstract fun getShortText(searchValue: S): @Nls String
|
||||
|
||||
protected abstract fun createFilters(viewScope: CoroutineScope): List<JComponent>
|
||||
|
||||
private inner class QuickFilterButtonFactory {
|
||||
|
||||
fun create(viewScope: CoroutineScope, quickFilters: List<Pair<String, S>>): JComponent {
|
||||
val button = InlineIconButton(AllIcons.General.Filter).apply {
|
||||
border = JBUI.Borders.empty(3)
|
||||
}.also {
|
||||
it.actionListener = ActionListener { _ ->
|
||||
showQuickFiltersPopup(it, quickFilters)
|
||||
}
|
||||
}
|
||||
|
||||
viewScope.launch {
|
||||
vm.searchState.collect {
|
||||
button.icon = if (it.filterCount == 0) AllIcons.General.Filter else IconWithNotifyDot(AllIcons.General.Filter)
|
||||
}
|
||||
}
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
private fun showQuickFiltersPopup(parentComponent: JComponent, quickFilters: List<Pair<@Nls String, S>>) {
|
||||
val quickFiltersActions =
|
||||
quickFilters.map { (name, search) -> QuickFilterAction(name, search) } +
|
||||
Separator() +
|
||||
ClearFiltersAction()
|
||||
|
||||
|
||||
JBPopupFactory.getInstance()
|
||||
.createActionGroupPopup(CollaborationToolsBundle.message("review.list.filter.quick.title"), DefaultActionGroup(quickFiltersActions),
|
||||
DataManager.getInstance().getDataContext(parentComponent),
|
||||
JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
|
||||
false)
|
||||
.showUnderneathOf(parentComponent)
|
||||
}
|
||||
|
||||
private inner class QuickFilterAction(name: @Nls String, private val search: S)
|
||||
: DumbAwareAction(name), Toggleable {
|
||||
override fun update(e: AnActionEvent) = Toggleable.setSelected(e.presentation, vm.searchState.value == search)
|
||||
override fun actionPerformed(e: AnActionEvent) = vm.searchState.update { search }
|
||||
}
|
||||
|
||||
private inner class ClearFiltersAction
|
||||
: DumbAwareAction(CollaborationToolsBundle.message("review.list.filter.quick.clear", vm.searchState.value.filterCount)) {
|
||||
override fun update(e: AnActionEvent) {
|
||||
e.presentation.isEnabledAndVisible = vm.searchState.value.filterCount > 0
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) = vm.searchState.update { vm.emptySearch }
|
||||
}
|
||||
|
||||
//TODO: request a ready-made icon from UI and also a proper icon for old UI
|
||||
private inner class IconWithNotifyDot(private val originalIcon: Icon) : Icon by originalIcon {
|
||||
override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) {
|
||||
originalIcon.paintIcon(c, g, x, y)
|
||||
g as Graphics2D
|
||||
val dotSize = JBUIScale.scale(6)
|
||||
val notifyDotShape = Ellipse2D.Float((iconWidth - dotSize).toFloat(), 0f, dotSize.toFloat(), dotSize.toFloat())
|
||||
g.color = ColorUtil.fromHex("#3574F0")
|
||||
g.fill(notifyDotShape)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -7,5 +7,8 @@ interface ReviewListSearchPanelViewModel<S : ReviewListSearchValue> {
|
||||
val searchState: MutableStateFlow<S>
|
||||
val queryState: MutableStateFlow<String?>
|
||||
|
||||
val emptySearch: S
|
||||
val defaultSearch: S
|
||||
|
||||
fun getSearchHistory(): List<S>
|
||||
}
|
||||
+4
-3
@@ -9,7 +9,8 @@ import kotlinx.coroutines.launch
|
||||
abstract class ReviewListSearchPanelViewModelBase<S : ReviewListSearchValue>(
|
||||
private val scope: CoroutineScope,
|
||||
private val historyModel: ReviewListSearchHistoryModel<S>,
|
||||
private val defaultSearch: S
|
||||
final override val emptySearch: S,
|
||||
final override val defaultSearch: S
|
||||
) : ReviewListSearchPanelViewModel<S> {
|
||||
|
||||
final override val searchState = MutableStateFlow(historyModel.getHistory().lastOrNull() ?: defaultSearch)
|
||||
@@ -18,7 +19,7 @@ abstract class ReviewListSearchPanelViewModelBase<S : ReviewListSearchValue>(
|
||||
withQuery(it)
|
||||
}
|
||||
|
||||
override fun getSearchHistory(): List<S> = historyModel.getHistory()
|
||||
final override fun getSearchHistory(): List<S> = historyModel.getHistory()
|
||||
|
||||
init {
|
||||
updateHistoryOnSearchChanges()
|
||||
@@ -32,7 +33,7 @@ abstract class ReviewListSearchPanelViewModelBase<S : ReviewListSearchValue>(
|
||||
return@collectLatestWithPrevious
|
||||
}
|
||||
|
||||
if (new.isEmpty || new == defaultSearch) {
|
||||
if (new.filterCount == 0 || new == defaultSearch) {
|
||||
return@collectLatestWithPrevious
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -3,6 +3,6 @@ package com.intellij.collaboration.ui.codereview.list.search
|
||||
|
||||
interface ReviewListSearchValue {
|
||||
val searchQuery: String?
|
||||
val isEmpty: Boolean
|
||||
get() = searchQuery == null
|
||||
val filterCount: Int
|
||||
get() = if(searchQuery != null) 1 else 0
|
||||
}
|
||||
@@ -219,6 +219,9 @@ pull.request.list.filter.review.awaiting.short=Awaiting review
|
||||
pull.request.list.filter.review.awaiting.full=Awaiting review from you
|
||||
pull.request.list.filter.author=Author
|
||||
pull.request.list.filter.label=Label
|
||||
pull.request.list.filter.quick.open=Open
|
||||
pull.request.list.filter.quick.yours=Your pull requests
|
||||
pull.request.list.filter.quick.assigned=Assigned to you
|
||||
pull.request.open.action=View Pull Request
|
||||
pull.request.select.action=Select Opened Pull Request
|
||||
pull.request.back.to.list=Back to List
|
||||
|
||||
+19
-9
@@ -22,7 +22,6 @@ import com.intellij.vcs.log.ui.frame.ProgressStripe
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -34,6 +33,7 @@ import org.jetbrains.plugins.github.pullrequest.data.GHListLoader
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRListLoader
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRListUpdatesChecker
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GHPRRepositoryDataService
|
||||
import org.jetbrains.plugins.github.pullrequest.data.service.GHPRSecurityService
|
||||
import org.jetbrains.plugins.github.pullrequest.ui.GHApiLoadingErrorHandler
|
||||
import org.jetbrains.plugins.github.ui.avatars.GHAvatarIconsProvider
|
||||
import org.jetbrains.plugins.github.ui.component.GHHandledErrorPanelModel
|
||||
@@ -47,6 +47,7 @@ import javax.swing.event.ChangeEvent
|
||||
|
||||
internal class GHPRListPanelFactory(private val project: Project,
|
||||
private val repositoryDataService: GHPRRepositoryDataService,
|
||||
private val securityService: GHPRSecurityService,
|
||||
private val listLoader: GHPRListLoader,
|
||||
private val listUpdatesChecker: GHPRListUpdatesChecker,
|
||||
private val account: GithubAccount,
|
||||
@@ -66,9 +67,9 @@ internal class GHPRListPanelFactory(private val project: Project,
|
||||
}
|
||||
}
|
||||
|
||||
ListEmptyTextController(scope, listLoader, searchVm.searchState, list.emptyText, disposable)
|
||||
ListEmptyTextController(scope, listLoader, searchVm, list.emptyText, disposable)
|
||||
|
||||
val searchPanel = GHPRSearchPanelFactory(searchVm).create(scope)
|
||||
val searchPanel = GHPRSearchPanelFactory(searchVm).create(scope, createQuickFilters())
|
||||
|
||||
val outdatedStatePanel = JPanel(FlowLayout(FlowLayout.LEFT, JBUIScale.scale(5), 0)).apply {
|
||||
background = UIUtil.getPanelBackground()
|
||||
@@ -111,6 +112,15 @@ internal class GHPRListPanelFactory(private val project: Project,
|
||||
}
|
||||
}
|
||||
|
||||
private fun createQuickFilters() = listOf(
|
||||
GithubBundle.message("pull.request.list.filter.quick.open") to
|
||||
GHPRListSearchValue(state = GHPRListSearchValue.State.OPEN),
|
||||
GithubBundle.message("pull.request.list.filter.quick.yours") to
|
||||
GHPRListSearchValue(state = GHPRListSearchValue.State.OPEN, author = securityService.currentUser.login),
|
||||
GithubBundle.message("pull.request.list.filter.quick.assigned") to
|
||||
GHPRListSearchValue(state = GHPRListSearchValue.State.OPEN, assignee = securityService.currentUser.login)
|
||||
)
|
||||
|
||||
private fun createListLoaderPanel(loader: GHListLoader<*>, list: JComponent, disposable: Disposable): JComponent {
|
||||
|
||||
val scrollPane = ScrollPaneFactory.createScrollPane(list, true).apply {
|
||||
@@ -149,13 +159,13 @@ internal class GHPRListPanelFactory(private val project: Project,
|
||||
|
||||
private class ListEmptyTextController(scope: CoroutineScope,
|
||||
private val listLoader: GHListLoader<*>,
|
||||
private val searchState: MutableStateFlow<GHPRListSearchValue>,
|
||||
private val searchVm: GHPRSearchPanelViewModel,
|
||||
private val emptyText: StatusText,
|
||||
listenersDisposable: Disposable) {
|
||||
init {
|
||||
listLoader.addLoadingStateChangeListener(listenersDisposable, ::update)
|
||||
scope.launch {
|
||||
searchState.collect {
|
||||
searchVm.searchState.collect {
|
||||
update()
|
||||
}
|
||||
}
|
||||
@@ -166,15 +176,15 @@ internal class GHPRListPanelFactory(private val project: Project,
|
||||
if (listLoader.loading || listLoader.error != null) return
|
||||
|
||||
|
||||
val search = searchState.value
|
||||
val search = searchVm.searchState.value
|
||||
if (search == GHPRListSearchValue.DEFAULT) {
|
||||
emptyText.appendText(GithubBundle.message("pull.request.list.no.matches"))
|
||||
.appendSecondaryText(GithubBundle.message("pull.request.list.reset.filters"),
|
||||
SimpleTextAttributes.LINK_ATTRIBUTES) {
|
||||
searchState.update { GHPRListSearchValue.EMPTY }
|
||||
searchVm.searchState.update { GHPRListSearchValue.EMPTY }
|
||||
}
|
||||
}
|
||||
else if (search.isEmpty) {
|
||||
else if (search.filterCount == 0) {
|
||||
emptyText.appendText(GithubBundle.message("pull.request.list.nothing.loaded"))
|
||||
}
|
||||
else {
|
||||
@@ -182,7 +192,7 @@ internal class GHPRListPanelFactory(private val project: Project,
|
||||
.appendSecondaryText(GithubBundle.message("pull.request.list.reset.filters.to.default",
|
||||
GHPRListSearchValue.DEFAULT.toQuery().toString()),
|
||||
SimpleTextAttributes.LINK_ATTRIBUTES) {
|
||||
searchState.update { GHPRListSearchValue.DEFAULT }
|
||||
searchVm.searchState.update { GHPRListSearchValue.DEFAULT }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -3,6 +3,7 @@ package org.jetbrains.plugins.github.pullrequest.ui.toolwindow
|
||||
|
||||
import com.intellij.collaboration.ui.codereview.list.search.ReviewListSearchValue
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
import org.jetbrains.plugins.github.api.data.GithubIssueState
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRSearchQuery
|
||||
import org.jetbrains.plugins.github.pullrequest.data.GHPRSearchQuery.QualifierName
|
||||
@@ -17,7 +18,20 @@ internal data class GHPRListSearchValue(override val searchQuery: String? = null
|
||||
val reviewState: ReviewState? = null,
|
||||
val author: String? = null,
|
||||
val label: String? = null) : ReviewListSearchValue {
|
||||
override val isEmpty = searchQuery == null && state == null && assignee == null && reviewState == null && author == null && label == null
|
||||
|
||||
@Transient
|
||||
override val filterCount: Int = calcFilterCount()
|
||||
|
||||
private fun calcFilterCount(): Int {
|
||||
var count = 0
|
||||
if (searchQuery != null) count++
|
||||
if (state != null) count++
|
||||
if (assignee != null) count++
|
||||
if (reviewState != null) count++
|
||||
if (author != null) count++
|
||||
if (label != null) count++
|
||||
return count
|
||||
}
|
||||
|
||||
fun toQuery(): GHPRSearchQuery? {
|
||||
val terms = mutableListOf<Term<*>>()
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ internal class GHPRSearchPanelViewModel(
|
||||
private val repositoryDataService: GHPRRepositoryDataService,
|
||||
historyViewModel: GHPRSearchHistoryModel,
|
||||
val avatarIconsProvider: GHAvatarIconsProvider
|
||||
) : ReviewListSearchPanelViewModelBase<GHPRListSearchValue>(scope, historyViewModel, GHPRListSearchValue.DEFAULT) {
|
||||
) : ReviewListSearchPanelViewModelBase<GHPRListSearchValue>(scope, historyViewModel, GHPRListSearchValue.EMPTY, GHPRListSearchValue.DEFAULT) {
|
||||
|
||||
override fun GHPRListSearchValue.withQuery(query: String?) = copy(searchQuery = query)
|
||||
|
||||
|
||||
+1
@@ -342,6 +342,7 @@ internal class GHPRToolWindowTabControllerImpl(private val project: Project,
|
||||
|
||||
return GHPRListPanelFactory(project,
|
||||
dataContext.repositoryDataService,
|
||||
dataContext.securityService,
|
||||
dataContext.listLoader,
|
||||
dataContext.listUpdatesChecker,
|
||||
dataContext.securityService.account,
|
||||
|
||||
Reference in New Issue
Block a user