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

Commit 818cdd7

Browse files
authored
Merge pull request #438 from github/fixes/386-remember-pr-list-state
Remember PR list filter settings.
2 parents 9823e82 + 5f36b1a commit 818cdd7

File tree

6 files changed

+141
-34
lines changed

6 files changed

+141
-34
lines changed

src/GitHub.App/ViewModels/PullRequestListViewModel.cs

+65-20
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
using System.Reactive.Linq;
77
using System.Windows.Input;
88
using System.Windows.Media.Imaging;
9+
using System.Windows.Threading;
910
using GitHub.Collections;
1011
using GitHub.Exports;
1112
using GitHub.Models;
1213
using GitHub.Services;
14+
using GitHub.Settings;
1315
using GitHub.UI;
1416
using NullGuard;
1517
using ReactiveUI;
@@ -25,17 +27,31 @@ public class PullRequestListViewModel : BaseViewModel, IPullRequestListViewModel
2527
readonly ISimpleRepositoryModel repository;
2628
readonly TrackingCollection<IAccount> trackingAuthors;
2729
readonly TrackingCollection<IAccount> trackingAssignees;
30+
readonly IPackageSettings settings;
31+
readonly PullRequestListUIState listSettings;
32+
bool pullRequestsLoaded;
2833

2934
[ImportingConstructor]
3035
PullRequestListViewModel(
31-
IConnectionRepositoryHostMap connectionRepositoryHostMap, ITeamExplorerServiceHolder teservice)
32-
: this(connectionRepositoryHostMap.CurrentRepositoryHost, teservice.ActiveRepo)
33-
{ }
36+
IConnectionRepositoryHostMap connectionRepositoryHostMap,
37+
ITeamExplorerServiceHolder teservice,
38+
IPackageSettings settings)
39+
: this(connectionRepositoryHostMap.CurrentRepositoryHost, teservice.ActiveRepo, settings)
40+
{
41+
}
3442

35-
public PullRequestListViewModel(IRepositoryHost repositoryHost, ISimpleRepositoryModel repository)
43+
public PullRequestListViewModel(
44+
IRepositoryHost repositoryHost,
45+
ISimpleRepositoryModel repository,
46+
IPackageSettings settings)
3647
{
3748
this.repositoryHost = repositoryHost;
3849
this.repository = repository;
50+
this.settings = settings;
51+
52+
this.listSettings = settings.UIState
53+
.GetOrCreateRepositoryState(repository.CloneUrl)
54+
.PullRequests;
3955

4056
openPullRequestCommand = ReactiveCommand.Create();
4157
openPullRequestCommand.Subscribe(_ =>
@@ -48,23 +64,10 @@ public PullRequestListViewModel(IRepositoryHost repositoryHost, ISimpleRepositor
4864
new PullRequestState { IsOpen = false, Name = "Closed" },
4965
new PullRequestState { Name = "All" }
5066
};
51-
SelectedState = States[0];
52-
53-
this.WhenAny(x => x.SelectedState, x => x.Value)
54-
.Where(x => PullRequests != null)
55-
.Subscribe(s => UpdateFilter(s, SelectedAssignee, SelectedAuthor));
56-
57-
this.WhenAny(x => x.SelectedAssignee, x => x.Value)
58-
.Where(x => PullRequests != null && x != EmptyUser)
59-
.Subscribe(a => UpdateFilter(SelectedState, a, SelectedAuthor));
60-
61-
this.WhenAny(x => x.SelectedAuthor, x => x.Value)
62-
.Where(x => PullRequests != null && x != EmptyUser)
63-
.Subscribe(a => UpdateFilter(SelectedState, SelectedAssignee, a));
6467

6568
trackingAuthors = new TrackingCollection<IAccount>(Observable.Empty<IAccount>(),
6669
OrderedComparer<IAccount>.OrderByDescending(x => x.Login).Compare);
67-
trackingAssignees = new TrackingCollection<IAccount>(Observable.Empty<IAccount>(),
70+
trackingAssignees = new TrackingCollection<IAccount>(Observable.Empty<IAccount>(),
6871
OrderedComparer<IAccount>.OrderByDescending(x => x.Login).Compare);
6972
trackingAuthors.Subscribe();
7073
trackingAssignees.Subscribe();
@@ -74,20 +77,53 @@ public PullRequestListViewModel(IRepositoryHost repositoryHost, ISimpleRepositor
7477

7578
PullRequests = new TrackingCollection<IPullRequestModel>();
7679
pullRequests.Comparer = OrderedComparer<IPullRequestModel>.OrderByDescending(x => x.UpdatedAt).Compare;
77-
pullRequests.Filter = (pr, i, l) => pr.IsOpen;
7880
pullRequests.NewerComparer = OrderedComparer<IPullRequestModel>.OrderByDescending(x => x.UpdatedAt).Compare;
81+
82+
this.WhenAny(x => x.SelectedState, x => x.Value)
83+
.Where(x => PullRequests != null)
84+
.Subscribe(s => UpdateFilter(s, SelectedAssignee, SelectedAuthor));
85+
86+
this.WhenAny(x => x.SelectedAssignee, x => x.Value)
87+
.Where(x => PullRequests != null && x != EmptyUser && pullRequestsLoaded)
88+
.Subscribe(a => UpdateFilter(SelectedState, a, SelectedAuthor));
89+
90+
this.WhenAny(x => x.SelectedAuthor, x => x.Value)
91+
.Where(x => PullRequests != null && x != EmptyUser && pullRequestsLoaded)
92+
.Subscribe(a => UpdateFilter(SelectedState, SelectedAssignee, a));
93+
94+
SelectedState = States.FirstOrDefault(x => x.Name == listSettings.SelectedState) ?? States[0];
7995
}
8096

8197
public override void Initialize([AllowNull] ViewWithData data)
8298
{
8399
base.Initialize(data);
84100

101+
pullRequestsLoaded = false;
102+
85103
PullRequests = repositoryHost.ModelService.GetPullRequests(repository, pullRequests);
86104
pullRequests.Subscribe(pr =>
87105
{
88106
trackingAssignees.AddItem(pr.Assignee);
89107
trackingAuthors.AddItem(pr.Author);
90108
}, () => { });
109+
110+
pullRequests.OriginalCompleted
111+
.ObserveOn(RxApp.MainThreadScheduler)
112+
.Subscribe(_ =>
113+
{
114+
if (listSettings.SelectedAuthor != null)
115+
{
116+
SelectedAuthor = Authors.FirstOrDefault(x => x.Login == listSettings.SelectedAuthor);
117+
}
118+
119+
if (listSettings.SelectedAssignee != null)
120+
{
121+
SelectedAssignee = Assignees.FirstOrDefault(x => x.Login == listSettings.SelectedAssignee);
122+
}
123+
124+
pullRequestsLoaded = true;
125+
UpdateFilter(SelectedState, SelectedAssignee, SelectedAuthor);
126+
});
91127
}
92128

93129
void UpdateFilter(PullRequestState state, [AllowNull]IAccount ass, [AllowNull]IAccount aut)
@@ -132,6 +168,7 @@ public IReadOnlyList<PullRequestState> States
132168
PullRequestState selectedState;
133169
public PullRequestState SelectedState
134170
{
171+
[return: AllowNull]
135172
get { return selectedState; }
136173
set { this.RaiseAndSetIfChanged(ref selectedState, value); }
137174
}
@@ -174,7 +211,6 @@ public IAccount EmptyUser
174211
get { return emptyUser; }
175212
}
176213

177-
178214
bool disposed;
179215
protected void Dispose(bool disposing)
180216
{
@@ -184,6 +220,7 @@ protected void Dispose(bool disposing)
184220
pullRequests.Dispose();
185221
trackingAuthors.Dispose();
186222
trackingAssignees.Dispose();
223+
SaveSettings();
187224
disposed = true;
188225
}
189226
}
@@ -193,5 +230,13 @@ public void Dispose()
193230
Dispose(true);
194231
GC.SuppressFinalize(this);
195232
}
233+
234+
void SaveSettings()
235+
{
236+
listSettings.SelectedState = SelectedState.Name;
237+
listSettings.SelectedAssignee = SelectedAssignee?.Login;
238+
listSettings.SelectedAuthor = SelectedAuthor?.Login;
239+
settings.Save();
240+
}
196241
}
197242
}

src/GitHub.Exports/GitHub.Exports.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@
149149
<DependentUpon>IPackageSettings.tt</DependentUpon>
150150
</Compile>
151151
<Compile Include="Settings\GitHubConnectSectionState.cs" />
152+
<Compile Include="Settings\PullRequestListUIState.cs" />
153+
<Compile Include="Settings\RepositoryUIState.cs" />
152154
<Compile Include="Settings\UIState.cs" />
153155
<Compile Include="SimpleJson.cs" />
154156
<Compile Include="ViewModels\IServiceProviderAware.cs" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace GitHub.Settings
4+
{
5+
public class PullRequestListUIState
6+
{
7+
public string SelectedState { get; set; }
8+
public string SelectedAuthor { get; set; }
9+
public string SelectedAssignee { get; set; }
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace GitHub.Settings
5+
{
6+
public class RepositoryUIState
7+
{
8+
public string RepositoryUrl { get; set; }
9+
10+
public PullRequestListUIState PullRequests { get; set; }
11+
= new PullRequestListUIState();
12+
}
13+
}

src/GitHub.Exports/Settings/UIState.cs

+24
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ public class UIState
1717
public List<GitHubConnectSectionState> GitHubConnectSections { get; set; }
1818
= new List<GitHubConnectSectionState>();
1919

20+
/// <summary>
21+
/// Gets or sets a a collection of UI state objects for repositories.
22+
/// </summary>
23+
public List<RepositoryUIState> RepositoryState { get; set; }
24+
= new List<RepositoryUIState>();
25+
26+
/// <summary>
27+
/// Gets or creates the UI state for a repository.
28+
/// </summary>
29+
/// <param name="repositoryUrl">The URL of the repository.</param>
30+
/// <returns>A <see cref="RepositoryUIState"/> object.</returns>
31+
public RepositoryUIState GetOrCreateRepositoryState(string repositoryUrl)
32+
{
33+
var result = RepositoryState.FirstOrDefault(x => x.RepositoryUrl == repositoryUrl);
34+
35+
if (result == null)
36+
{
37+
result = new RepositoryUIState { RepositoryUrl = repositoryUrl };
38+
RepositoryState.Add(result);
39+
}
40+
41+
return result;
42+
}
43+
2044
/// <summary>
2145
/// Gets or creates the UI state for a named <see cref="IGitHubConnectSection"/>.
2246
/// </summary>

src/UnitTests/GitHub.App/ViewModels/PullRequestListViewModelTests.cs

+26-14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using GitHub.Collections;
66
using GitHub.Models;
77
using GitHub.Services;
8+
using GitHub.Settings;
89
using GitHub.ViewModels;
910
using NSubstitute;
1011
using Xunit;
@@ -18,67 +19,71 @@ public void SelectingAssigneeShouldTriggerFilter()
1819
{
1920
var repositoryHost = CreateRepositoryHost();
2021
var repository = Substitute.For<ISimpleRepositoryModel>();
21-
var prViewModel = new PullRequestListViewModel(repositoryHost, repository);
22+
var settings = CreateSettings();
23+
var prViewModel = new PullRequestListViewModel(repositoryHost, repository, settings);
2224

2325
prViewModel.Initialize(null);
24-
prViewModel.PullRequests.DidNotReceive().Filter = AnyFilter;
26+
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
2527

2628
prViewModel.SelectedAssignee = prViewModel.PullRequests[0].Assignee;
27-
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
29+
prViewModel.PullRequests.Received(2).Filter = AnyFilter;
2830
}
2931

3032
[Fact]
3133
public void ResettingAssigneeToNoneShouldNotTriggerFilter()
3234
{
3335
var repositoryHost = CreateRepositoryHost();
3436
var repository = Substitute.For<ISimpleRepositoryModel>();
35-
var prViewModel = new PullRequestListViewModel(repositoryHost, repository);
37+
var settings = CreateSettings();
38+
var prViewModel = new PullRequestListViewModel(repositoryHost, repository, settings);
3639

3740
prViewModel.Initialize(null);
38-
prViewModel.PullRequests.DidNotReceive().Filter = AnyFilter;
41+
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
3942

4043
prViewModel.SelectedAssignee = prViewModel.PullRequests[0].Assignee;
41-
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
44+
prViewModel.PullRequests.Received(2).Filter = AnyFilter;
4245

4346
// Setting the Assignee filter to [None] should not trigger a filter:
4447
// doing this will remove the [None] entry from Assignees, which will cause
4548
// the selection in the view to be set to null which will reset the filter.
4649
prViewModel.SelectedAssignee = prViewModel.EmptyUser;
47-
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
50+
prViewModel.PullRequests.Received(2).Filter = AnyFilter;
4851
}
4952

5053
[Fact]
5154
public void SelectingAuthorShouldTriggerFilter()
5255
{
5356
var repositoryHost = CreateRepositoryHost();
5457
var repository = Substitute.For<ISimpleRepositoryModel>();
55-
var prViewModel = new PullRequestListViewModel(repositoryHost, repository);
58+
var settings = CreateSettings();
59+
var prViewModel = new PullRequestListViewModel(repositoryHost, repository, settings);
5660

5761
prViewModel.Initialize(null);
58-
prViewModel.PullRequests.DidNotReceive().Filter = AnyFilter;
62+
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
5963

6064
prViewModel.SelectedAuthor = prViewModel.PullRequests[0].Author;
61-
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
65+
prViewModel.PullRequests.Received(2).Filter = AnyFilter;
6266
}
6367

6468
[Fact]
6569
public void ResettingAuthorToNoneShouldNotTriggerFilter()
6670
{
6771
var repositoryHost = CreateRepositoryHost();
6872
var repository = Substitute.For<ISimpleRepositoryModel>();
69-
var prViewModel = new PullRequestListViewModel(repositoryHost, repository);
73+
var settings = CreateSettings();
74+
var prViewModel = new PullRequestListViewModel(repositoryHost, repository, settings);
7075

7176
prViewModel.Initialize(null);
72-
prViewModel.PullRequests.DidNotReceive().Filter = AnyFilter;
77+
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
7378

7479
prViewModel.SelectedAuthor = prViewModel.PullRequests[0].Author;
75-
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
80+
prViewModel.PullRequests.Received(2).Filter = AnyFilter;
7681

7782
// Setting the Author filter to [None] should not trigger a filter:
7883
// doing this will remove the [None] entry from Authors, which will cause
7984
// the selection in the view to be set to null which will reset the filter.
8085
prViewModel.SelectedAuthor = prViewModel.EmptyUser;
81-
prViewModel.PullRequests.Received(1).Filter = AnyFilter;
86+
prViewModel.PullRequests.Received(2).Filter = AnyFilter;
8287
}
8388

8489
Func<IPullRequestModel, int, IList<IPullRequestModel>, bool> AnyFilter =>
@@ -108,5 +113,12 @@ IRepositoryHost CreateRepositoryHost()
108113

109114
return result;
110115
}
116+
117+
IPackageSettings CreateSettings()
118+
{
119+
var settings = Substitute.For<IPackageSettings>();
120+
settings.UIState.Returns(new UIState());
121+
return settings;
122+
}
111123
}
112124
}

0 commit comments

Comments
 (0)