From 2386c62d187f5661ee5cc8166d2d361fae83c2fe Mon Sep 17 00:00:00 2001 From: Ivan Semenov Date: Sat, 25 Jun 2022 15:11:33 +0300 Subject: [PATCH] [collab] implement review list quick filters GitOrigin-RevId: 294f0921ff5f8ec5bc7b41e23a0317bfbfceb882 --- .../CollaborationToolsBundle.properties | 4 +- .../search/ReviewListSearchPanelFactory.kt | 102 ++++++++++++++++-- .../search/ReviewListSearchPanelViewModel.kt | 3 + .../ReviewListSearchPanelViewModelBase.kt | 7 +- .../list/search/ReviewListSearchValue.kt | 4 +- .../messages/GithubBundle.properties | 3 + .../ui/toolwindow/GHPRListPanelFactory.kt | 28 +++-- .../ui/toolwindow/GHPRListSearchValue.kt | 16 ++- .../ui/toolwindow/GHPRSearchPanelViewModel.kt | 2 +- .../GHPRToolWindowTabControllerImpl.kt | 1 + 10 files changed, 143 insertions(+), 27 deletions(-) diff --git a/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties b/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties index fae8d23f321e..0d3a45448439 100644 --- a/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties +++ b/platform/collaboration-tools/resources/messages/CollaborationToolsBundle.properties @@ -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} \ No newline at end of file +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 \ No newline at end of file diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelFactory.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelFactory.kt index 89f0b4c241ad..b60d4542631e 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelFactory.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelFactory.kt @@ -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>): 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 + + private inner class QuickFilterButtonFactory { + + fun create(viewScope: CoroutineScope, quickFilters: List>): 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>) { + 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) + } + } + } } \ No newline at end of file diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModel.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModel.kt index e91fa74855a0..5c78f7acca8f 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModel.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModel.kt @@ -7,5 +7,8 @@ interface ReviewListSearchPanelViewModel { val searchState: MutableStateFlow val queryState: MutableStateFlow + val emptySearch: S + val defaultSearch: S + fun getSearchHistory(): List } \ No newline at end of file diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModelBase.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModelBase.kt index 3b911988b4ae..a26ee89104f5 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModelBase.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchPanelViewModelBase.kt @@ -9,7 +9,8 @@ import kotlinx.coroutines.launch abstract class ReviewListSearchPanelViewModelBase( private val scope: CoroutineScope, private val historyModel: ReviewListSearchHistoryModel, - private val defaultSearch: S + final override val emptySearch: S, + final override val defaultSearch: S ) : ReviewListSearchPanelViewModel { final override val searchState = MutableStateFlow(historyModel.getHistory().lastOrNull() ?: defaultSearch) @@ -18,7 +19,7 @@ abstract class ReviewListSearchPanelViewModelBase( withQuery(it) } - override fun getSearchHistory(): List = historyModel.getHistory() + final override fun getSearchHistory(): List = historyModel.getHistory() init { updateHistoryOnSearchChanges() @@ -32,7 +33,7 @@ abstract class ReviewListSearchPanelViewModelBase( return@collectLatestWithPrevious } - if (new.isEmpty || new == defaultSearch) { + if (new.filterCount == 0 || new == defaultSearch) { return@collectLatestWithPrevious } diff --git a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchValue.kt b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchValue.kt index 2d5888532d21..98ed7f9487df 100644 --- a/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchValue.kt +++ b/platform/collaboration-tools/src/com/intellij/collaboration/ui/codereview/list/search/ReviewListSearchValue.kt @@ -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 } \ No newline at end of file diff --git a/plugins/github/resources/messages/GithubBundle.properties b/plugins/github/resources/messages/GithubBundle.properties index 2c84d0f0810a..dd7f690c27e3 100644 --- a/plugins/github/resources/messages/GithubBundle.properties +++ b/plugins/github/resources/messages/GithubBundle.properties @@ -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 diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListPanelFactory.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListPanelFactory.kt index 64631932e566..d571ff53017c 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListPanelFactory.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListPanelFactory.kt @@ -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, + 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 } } } } diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListSearchValue.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListSearchValue.kt index f234335b3cd5..68d23979d0dd 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListSearchValue.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRListSearchValue.kt @@ -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>() diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRSearchPanelViewModel.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRSearchPanelViewModel.kt index 4031851b372f..12bc7433c899 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRSearchPanelViewModel.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRSearchPanelViewModel.kt @@ -14,7 +14,7 @@ internal class GHPRSearchPanelViewModel( private val repositoryDataService: GHPRRepositoryDataService, historyViewModel: GHPRSearchHistoryModel, val avatarIconsProvider: GHAvatarIconsProvider -) : ReviewListSearchPanelViewModelBase(scope, historyViewModel, GHPRListSearchValue.DEFAULT) { +) : ReviewListSearchPanelViewModelBase(scope, historyViewModel, GHPRListSearchValue.EMPTY, GHPRListSearchValue.DEFAULT) { override fun GHPRListSearchValue.withQuery(query: String?) = copy(searchQuery = query) diff --git a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt index c1f2710ddb43..fb896a63c613 100644 --- a/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt +++ b/plugins/github/src/org/jetbrains/plugins/github/pullrequest/ui/toolwindow/GHPRToolWindowTabControllerImpl.kt @@ -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,