Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1312 from github/feature/pull-request-filtering
Browse files Browse the repository at this point in the history
Pull Request Filtering
  • Loading branch information
grokys committed Nov 20, 2017
2 parents 0f2f016 + 1bed9e3 commit 51aedf2
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 33 deletions.
4 changes: 4 additions & 0 deletions src/GitHub.App/SampleData/PullRequestListViewModelDesigner.cs
Expand Up @@ -53,6 +53,8 @@ public PullRequestListViewModelDesigner()
Authors = new ObservableCollection<IAccount>(prs.Select(x => x.Author));
SelectedAssignee = Assignees.ElementAt(1);
SelectedAuthor = Authors.ElementAt(1);

IsLoaded = true;
}

public IReadOnlyList<IRemoteRepositoryModel> Repositories { get; }
Expand All @@ -68,6 +70,8 @@ public PullRequestListViewModelDesigner()
public IAccount SelectedAuthor { get; set; }
public bool RepositoryIsFork { get; set; } = true;
public bool ShowPullRequestsForFork { get; set; }
public string SearchQuery { get; set; }
public bool IsLoaded { get; }

public ObservableCollection<IAccount> Assignees { get; set; }
public IAccount SelectedAssignee { get; set; }
Expand Down
63 changes: 49 additions & 14 deletions src/GitHub.App/ViewModels/PullRequestListViewModel.cs
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Globalization;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
Expand All @@ -11,6 +12,7 @@
using GitHub.Collections;
using GitHub.Exports;
using GitHub.Extensions;
using GitHub.Helpers;
using GitHub.Logging;
using GitHub.Models;
using GitHub.Services;
Expand Down Expand Up @@ -93,19 +95,19 @@ public class PullRequestListViewModel : PanePageViewModelBase, IPullRequestListV

this.WhenAny(x => x.SelectedState, x => x.Value)
.Where(x => PullRequests != null)
.Subscribe(s => UpdateFilter(s, SelectedAssignee, SelectedAuthor));
.Subscribe(s => UpdateFilter(s, SelectedAssignee, SelectedAuthor, SearchQuery));

this.WhenAny(x => x.SelectedAssignee, x => x.Value)
.Where(x => PullRequests != null && x != EmptyUser)
.Subscribe(a => UpdateFilter(SelectedState, a, SelectedAuthor));
.Subscribe(a => UpdateFilter(SelectedState, a, SelectedAuthor, SearchQuery));

this.WhenAny(x => x.SelectedAuthor, x => x.Value)
.Where(x => PullRequests != null && x != EmptyUser)
.Subscribe(a => UpdateFilter(SelectedState, SelectedAssignee, a));
.Subscribe(a => UpdateFilter(SelectedState, SelectedAssignee, a, SearchQuery));

this.WhenAnyValue(x => x.SelectedRepository)
.Skip(1)
.Subscribe(_ => ResetAndLoad());
this.WhenAny(x => x.SearchQuery, x => x.Value)
.Where(x => PullRequests != null)
.Subscribe(f => UpdateFilter(SelectedState, SelectedAssignee, SelectedAuthor, f));

SelectedState = States.FirstOrDefault(x => x.Name == listSettings.SelectedState) ?? States[0];
OpenPullRequest = ReactiveCommand.Create();
Expand Down Expand Up @@ -173,19 +175,50 @@ async Task Load()
}
IsBusy = false;
UpdateFilter(SelectedState, SelectedAssignee, SelectedAuthor);
UpdateFilter(SelectedState, SelectedAssignee, SelectedAuthor, SearchQuery);
});
}

void UpdateFilter(PullRequestState state, IAccount ass, IAccount aut)
void UpdateFilter(PullRequestState state, IAccount ass, IAccount aut, string filText)
{
if (PullRequests == null)
return;
pullRequests.Filter = (pr, i, l) =>
(!state.IsOpen.HasValue || state.IsOpen == pr.IsOpen) &&
(ass == null || ass.Equals(pr.Assignee)) &&
(aut == null || aut.Equals(pr.Author));
SaveSettings();

var filterTextIsNumber = false;
var filterTextIsString = false;
var filterPullRequestNumber = 0;

if (filText != null)
{
filText = filText.Trim();

var hasText = !string.IsNullOrEmpty(filText);

if (hasText && filText.StartsWith("#", StringComparison.CurrentCultureIgnoreCase))
{
filterTextIsNumber = int.TryParse(filText.Substring(1), out filterPullRequestNumber);
}
else
{
filterTextIsNumber = int.TryParse(filText, out filterPullRequestNumber);
}

filterTextIsString = hasText && !filterTextIsNumber;
}

pullRequests.Filter = (pullRequest, index, list) =>
(!state.IsOpen.HasValue || state.IsOpen == pullRequest.IsOpen) &&
(ass == null || ass.Equals(pullRequest.Assignee)) &&
(aut == null || aut.Equals(pullRequest.Author)) &&
(filterTextIsNumber == false || pullRequest.Number == filterPullRequestNumber) &&
(filterTextIsString == false || pullRequest.Title.ToUpperInvariant().Contains(filText.ToUpperInvariant()));
}

string searchQuery;
public string SearchQuery
{
get { return searchQuery; }
set { this.RaiseAndSetIfChanged(ref searchQuery, value); }
}

bool isBusy;
Expand Down Expand Up @@ -271,6 +304,8 @@ public IAccount EmptyUser
get { return emptyUser; }
}

public bool IsSearchEnabled => true;

readonly Subject<ViewWithData> navigate = new Subject<ViewWithData>();
public IObservable<ViewWithData> Navigate => navigate;

Expand Down Expand Up @@ -308,7 +343,7 @@ void CreatePullRequests()
void ResetAndLoad()
{
CreatePullRequests();
UpdateFilter(SelectedState, SelectedAssignee, SelectedAuthor);
UpdateFilter(SelectedState, SelectedAssignee, SelectedAuthor, SearchQuery);
Load().Forget();
}

Expand Down
Expand Up @@ -27,7 +27,7 @@ public override string ToString()
}
}

public interface IPullRequestListViewModel : IViewModel, ICanNavigate, IHasBusy
public interface IPullRequestListViewModel : ISearchablePanePageViewModel, ICanNavigate, IHasBusy
{
IReadOnlyList<IRemoteRepositoryModel> Repositories { get; }
IRemoteRepositoryModel SelectedRepository { get; set; }
Expand Down
3 changes: 2 additions & 1 deletion src/GitHub.Exports/GitHub.Exports.csproj
Expand Up @@ -152,8 +152,10 @@
<Compile Include="Models\UsageData.cs" />
<Compile Include="Services\IUsageService.cs" />
<Compile Include="Settings\PkgCmdID.cs" />
<Compile Include="ViewModels\IGitHubPaneViewModel.cs" />
<Compile Include="ViewModels\IHasErrorState.cs" />
<Compile Include="ViewModels\IHasLoading.cs" />
<Compile Include="ViewModels\ISearchablePageViewModel.cs" />
<Compile Include="ViewModels\IPanePageViewModel.cs" />
<Compile Include="ViewModels\IViewModel.cs" />
<None Include="..\common\settings.json">
Expand Down Expand Up @@ -258,7 +260,6 @@
<Compile Include="Services\IGitHubServiceProvider.cs" />
<Compile Include="Services\IWikiProbe.cs" />
<Compile Include="Services\WikiProbe.cs" />
<Compile Include="ViewModels\IGitHubPaneViewModel.cs" />
<Compile Include="Primitives\HostAddress.cs" />
<Compile Include="UI\IUIController.cs" />
<Compile Include="Models\IPullRequestModel.cs" />
Expand Down
14 changes: 11 additions & 3 deletions src/GitHub.Exports/ViewModels/IGitHubPaneViewModel.cs
@@ -1,6 +1,4 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using GitHub.UI;
using GitHub.UI;

namespace GitHub.ViewModels
{
Expand All @@ -10,5 +8,15 @@ public interface IGitHubPaneViewModel : IViewModel
IView Control { get; }
string Message { get; }
MessageType MessageType { get; }

/// <summary>
/// Gets a value indicating whether search is available on the current page.
/// </summary>
bool IsSearchEnabled { get; }

/// <summary>
/// Gets or sets the search query for the current page.
/// </summary>
string SearchQuery { get; set; }
}
}
15 changes: 15 additions & 0 deletions src/GitHub.Exports/ViewModels/ISearchablePageViewModel.cs
@@ -0,0 +1,15 @@
using System;

namespace GitHub.ViewModels
{
/// <summary>
/// A view model that represents a searchable page in the GitHub pane.
/// </summary>
public interface ISearchablePanePageViewModel : IPanePageViewModel
{
/// <summary>
/// Gets or sets the current search query.
/// </summary>
string SearchQuery { get; set; }
}
}
121 changes: 111 additions & 10 deletions src/GitHub.VisualStudio/UI/GitHubPane.cs
@@ -1,17 +1,16 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using System.ComponentModel.Design;
using System.Windows.Controls;
using GitHub.Services;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using GitHub.Extensions;
using GitHub.Models;
using GitHub.Logging;
using GitHub.Services;
using GitHub.UI;
using GitHub.ViewModels;
using System.Diagnostics;
using GitHub.Logging;
using GitHub.VisualStudio.UI.Views;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using ReactiveUI;

namespace GitHub.VisualStudio.UI
{
Expand All @@ -32,11 +31,27 @@ public class GitHubPane : ToolWindowPane, IServiceProviderAware, IViewHost
{
public const string GitHubPaneGuid = "6b0fdc0a-f28e-47a0-8eed-cc296beff6d2";
bool initialized = false;
IDisposable viewSubscription;

IView View
{
get { return Content as IView; }
set { Content = value; }
set
{
viewSubscription?.Dispose();
viewSubscription = null;

Content = value;

viewSubscription = value.WhenAnyValue(x => x.ViewModel)
.SelectMany(x =>
{
var pane = x as IGitHubPaneViewModel;
return pane?.WhenAnyValue(p => p.IsSearchEnabled, p => p.SearchQuery)
?? Observable.Return(Tuple.Create<bool, string>(false, null));
})
.Subscribe(x => UpdateSearchHost(x.Item1, x.Item2));
}
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
Expand All @@ -56,6 +71,8 @@ public GitHubPane() : base(null)
View = uiProvider.GetView(Exports.UIViewType.GitHubPane);
}

public override bool SearchEnabled => true;

protected override void Initialize()
{
base.Initialize();
Expand All @@ -78,5 +95,89 @@ public void ShowView(ViewWithData data)
{
View.ViewModel?.Initialize(data);
}

[SuppressMessage("Microsoft.Design", "CA1061:DoNotHideBaseClassMethods", Justification = "WTF CA, I'm overriding!")]
public override IVsSearchTask CreateSearch(uint dwCookie, IVsSearchQuery pSearchQuery, IVsSearchCallback pSearchCallback)
{
var pane = View.ViewModel as IGitHubPaneViewModel;

if (pane != null)
{
return new SearchTask(pane, dwCookie, pSearchQuery, pSearchCallback);
}

return null;
}

public override void ClearSearch()
{
var pane = View.ViewModel as IGitHubPaneViewModel;

if (pane != null)
{
pane.SearchQuery = null;
}
}

public override void OnToolWindowCreated()
{
base.OnToolWindowCreated();

Marshal.ThrowExceptionForHR(((IVsWindowFrame)Frame)?.SetProperty(
(int)__VSFPROPID5.VSFPROPID_SearchPlacement,
__VSSEARCHPLACEMENT.SP_STRETCH) ?? 0);

var pane = View.ViewModel as IGitHubPaneViewModel;
UpdateSearchHost(pane?.IsSearchEnabled ?? false, pane?.SearchQuery);
}

void UpdateSearchHost(bool enabled, string query)
{
if (SearchHost != null)
{
SearchHost.IsEnabled = enabled;

if (SearchHost.SearchQuery?.SearchString != query)
{
SearchHost.SearchAsync(query != null ? new SearchQuery(query) : null);
}
}
}

class SearchTask : VsSearchTask
{
readonly IGitHubPaneViewModel viewModel;

public SearchTask(
IGitHubPaneViewModel viewModel,
uint dwCookie,
IVsSearchQuery pSearchQuery,
IVsSearchCallback pSearchCallback)
: base(dwCookie, pSearchQuery, pSearchCallback)
{
this.viewModel = viewModel;
}

protected override void OnStartSearch()
{
viewModel.SearchQuery = SearchQuery.SearchString;
base.OnStartSearch();
}

protected override void OnStopSearch() => viewModel.SearchQuery = null;
}

class SearchQuery : IVsSearchQuery
{
public SearchQuery(string query)
{
SearchString = query;
}

public uint ParseError => 0;
public string SearchString { get; }

public uint GetTokens(uint dwMaxTokens, IVsSearchToken[] rgpSearchTokens) => 0;
}
}
}

0 comments on commit 51aedf2

Please sign in to comment.