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

Pull Request Filtering #1312

Merged
merged 21 commits into from Nov 20, 2017
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b67cbb7
Initial filtering functionality
StanleyGoldman Dec 12, 2016
123e392
Treating the pull request id as a string leads to a better user exper…
StanleyGoldman Dec 12, 2016
0bc343b
Using FilterTextBox
StanleyGoldman Dec 12, 2016
040a7b5
Disabling filterText while !IsLoaded
StanleyGoldman Dec 13, 2016
d69e8eb
Adding delay to the binding
StanleyGoldman Dec 13, 2016
b633251
When Filter Text starts with '#' followed by digits, treat filter tex…
StanleyGoldman Dec 13, 2016
6f1a602
Putting the binding delay attribute in the right place
StanleyGoldman Dec 14, 2016
aaf95ee
A modification of pull request filtering with exact PR number matching
StanleyGoldman Dec 14, 2016
290c5ae
Merge branch 'release/2.3' into enhancement/pull-request-filtering
grokys Feb 13, 2017
956ae11
Merge branch 'master' into pr/732-pull-request-filtering
grokys Aug 29, 2017
50b9207
Moved filter textbox above dropdowns.
grokys Aug 29, 2017
4d5a72b
Merge branch 'master' into pr/732-pull-request-filtering
grokys Nov 8, 2017
d201359
Use built-in VS search box.
grokys Nov 10, 2017
30cc14d
Use stretch placement for search box.
grokys Nov 10, 2017
4d93b2a
Merge branch 'master' into pr/732-pull-request-filtering
grokys Nov 10, 2017
2e2bfdd
FIx/suppress CA errors.
grokys Nov 10, 2017
8cb0f59
Merge branch 'master' into enhancement/pull-request-filtering
grokys Nov 10, 2017
1bee546
Merge branch 'master' into feature/pull-request-filtering
grokys Nov 13, 2017
715bb39
Initialize SearchHost in OnToolWindowCreated
grokys Nov 13, 2017
56abda2
Merge branch 'master' into feature/pull-request-filtering
grokys Nov 17, 2017
1bed9e3
Merge branch 'master' into feature/pull-request-filtering
grokys Nov 20, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -150,8 +150,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 @@ -254,7 +256,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;
}
}
}