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

Commit fecdccf

Browse files
committed
Added drafts to PR creation.
1 parent e0c04fa commit fecdccf

File tree

3 files changed

+196
-8
lines changed

3 files changed

+196
-8
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using GitHub.ViewModels.GitHubPane;
2+
3+
namespace GitHub.Models.Drafts
4+
{
5+
/// <summary>
6+
/// Stores a draft for a <see cref="PullRequestCreationViewModel"/>.
7+
/// </summary>
8+
public class PullRequestDraft
9+
{
10+
/// <summary>
11+
/// Gets or sets the draft pull request title.
12+
/// </summary>
13+
public string Title { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets the draft pull request body.
17+
/// </summary>
18+
public string Body { get; set; }
19+
}
20+
}

src/GitHub.App/ViewModels/GitHubPane/PullRequestCreationViewModel.cs

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Globalization;
66
using System.Linq;
77
using System.Reactive;
8+
using System.Reactive.Concurrency;
89
using System.Reactive.Disposables;
910
using System.Reactive.Linq;
1011
using System.Threading.Tasks;
@@ -14,12 +15,15 @@
1415
using GitHub.Factories;
1516
using GitHub.Logging;
1617
using GitHub.Models;
18+
using GitHub.Models.Drafts;
19+
using GitHub.Primitives;
1720
using GitHub.Services;
1821
using GitHub.Validation;
1922
using Octokit;
2023
using ReactiveUI;
2124
using Serilog;
2225
using IConnection = GitHub.Models.IConnection;
26+
using static System.FormattableString;
2327

2428
namespace GitHub.ViewModels.GitHubPane
2529
{
@@ -33,6 +37,8 @@ public class PullRequestCreationViewModel : PanePageViewModelBase, IPullRequestC
3337
readonly ObservableAsPropertyHelper<bool> isExecuting;
3438
readonly IPullRequestService service;
3539
readonly IModelServiceFactory modelServiceFactory;
40+
readonly IMessageDraftStore draftStore;
41+
readonly IScheduler timerScheduler;
3642
readonly CompositeDisposable disposables = new CompositeDisposable();
3743
ILocalRepositoryModel activeLocalRepo;
3844
ObservableAsPropertyHelper<IRemoteRepositoryModel> githubRepository;
@@ -42,14 +48,29 @@ public class PullRequestCreationViewModel : PanePageViewModelBase, IPullRequestC
4248
public PullRequestCreationViewModel(
4349
IModelServiceFactory modelServiceFactory,
4450
IPullRequestService service,
45-
INotificationService notifications)
51+
INotificationService notifications,
52+
IMessageDraftStore draftStore)
53+
: this(modelServiceFactory, service, notifications, draftStore, DefaultScheduler.Instance)
54+
{
55+
}
56+
57+
public PullRequestCreationViewModel(
58+
IModelServiceFactory modelServiceFactory,
59+
IPullRequestService service,
60+
INotificationService notifications,
61+
IMessageDraftStore draftStore,
62+
IScheduler timerScheduler)
4663
{
4764
Guard.ArgumentNotNull(modelServiceFactory, nameof(modelServiceFactory));
4865
Guard.ArgumentNotNull(service, nameof(service));
4966
Guard.ArgumentNotNull(notifications, nameof(notifications));
67+
Guard.ArgumentNotNull(draftStore, nameof(draftStore));
68+
Guard.ArgumentNotNull(timerScheduler, nameof(timerScheduler));
5069

5170
this.service = service;
5271
this.modelServiceFactory = modelServiceFactory;
72+
this.draftStore = draftStore;
73+
this.timerScheduler = timerScheduler;
5374

5475
this.WhenAnyValue(x => x.Branches)
5576
.WhereNotNull()
@@ -93,15 +114,21 @@ public PullRequestCreationViewModel(
93114
TargetBranch.Repository.CloneUrl.ToRepositoryUrl().Append("pull/" + pr.Number)));
94115
NavigateTo("/pulls?refresh=true");
95116
Cancel.Execute();
117+
draftStore.DeleteDraft(GetDraftKey(), string.Empty).Forget();
96118
});
97119

98120
Cancel = ReactiveCommand.Create(() => { });
99-
Cancel.Subscribe(_ => Close());
121+
Cancel.Subscribe(_ =>
122+
{
123+
Close();
124+
draftStore.DeleteDraft(GetDraftKey(), string.Empty).Forget();
125+
});
100126

101127
isExecuting = CreatePullRequest.IsExecuting.ToProperty(this, x => x.IsExecuting);
102128

103129
this.WhenAnyValue(x => x.Initialized, x => x.GitHubRepository, x => x.IsExecuting)
104130
.Select(x => !(x.Item1 && x.Item2 != null && !x.Item3))
131+
.ObserveOn(RxApp.MainThreadScheduler)
105132
.Subscribe(x => IsBusy = x);
106133
}
107134

@@ -146,6 +173,39 @@ public async Task InitializeAsync(ILocalRepositoryModel repository, IConnection
146173
Initialized = true;
147174
});
148175

176+
var draftKey = GetDraftKey();
177+
await LoadInitialState(draftKey).ConfigureAwait(true);
178+
179+
this.WhenAnyValue(
180+
x => x.PRTitle,
181+
x => x.Description,
182+
(t, d) => new PullRequestDraft { Title = t, Body = d })
183+
.Throttle(TimeSpan.FromSeconds(1), timerScheduler)
184+
.Subscribe(x => draftStore.UpdateDraft(draftKey, string.Empty, x));
185+
186+
Initialized = true;
187+
}
188+
189+
async Task LoadInitialState(string draftKey)
190+
{
191+
if (activeLocalRepo.CloneUrl == null)
192+
return;
193+
194+
var draft = await draftStore.GetDraft<PullRequestDraft>(draftKey, string.Empty).ConfigureAwait(true);
195+
196+
if (draft != null)
197+
{
198+
PRTitle = draft.Title;
199+
Description = draft.Body;
200+
}
201+
else
202+
{
203+
LoadDescriptionFromCommits();
204+
}
205+
}
206+
207+
void LoadDescriptionFromCommits()
208+
{
149209
SourceBranch = activeLocalRepo.CurrentBranch;
150210

151211
var uniqueCommits = this.WhenAnyValue(
@@ -176,7 +236,7 @@ public async Task InitializeAsync(ILocalRepositoryModel repository, IConnection
176236
Observable.CombineLatest(
177237
this.WhenAnyValue(x => x.SourceBranch),
178238
uniqueCommits,
179-
service.GetPullRequestTemplate(repository).DefaultIfEmpty(string.Empty),
239+
service.GetPullRequestTemplate(activeLocalRepo).DefaultIfEmpty(string.Empty),
180240
(compare, commits, template) => new { compare, commits, template })
181241
.Subscribe(x =>
182242
{
@@ -203,8 +263,6 @@ public async Task InitializeAsync(ILocalRepositoryModel repository, IConnection
203263
PRTitle = prTitle;
204264
Description = prDescription;
205265
});
206-
207-
Initialized = true;
208266
}
209267

210268
void SetupValidators()
@@ -239,6 +297,20 @@ protected override void Dispose(bool disposing)
239297
}
240298
}
241299

300+
public static string GetDraftKey(
301+
UriString cloneUri,
302+
string branchName)
303+
{
304+
return Invariant($"pr|{cloneUri}|{branchName}");
305+
}
306+
307+
protected string GetDraftKey()
308+
{
309+
return GetDraftKey(
310+
activeLocalRepo.CloneUrl,
311+
SourceBranch.Name);
312+
}
313+
242314
public IRemoteRepositoryModel GitHubRepository { get { return githubRepository?.Value; } }
243315
bool IsExecuting { get { return isExecuting.Value; } }
244316

test/GitHub.App.UnitTests/ViewModels/Dialog/PullRequestCreationViewModelTests.cs renamed to test/GitHub.App.UnitTests/ViewModels/GitHubPane/PullRequestCreationViewModelTests.cs

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using IConnection = GitHub.Models.IConnection;
1616
using ReactiveUI.Testing;
1717
using System.Reactive.Concurrency;
18+
using GitHub.Models.Drafts;
1819

1920
/// <summary>
2021
/// All the tests in this class are split in subclasses so that when they run
@@ -129,7 +130,7 @@ public async Task TargetBranchDisplayNameIncludesRepoOwnerWhenForkAsync()
129130
var data = PrepareTestData("octokit.net", "shana", "master", "octokit", "master", "origin", true, true);
130131
var prservice = new PullRequestService(data.GitClient, data.GitService, Substitute.For<IVSGitExt>(), Substitute.For<IGraphQLClientFactory>(), data.ServiceProvider.GetOperatingSystem(), Substitute.For<IUsageTracker>());
131132
prservice.GetPullRequestTemplate(data.ActiveRepo).Returns(Observable.Empty<string>());
132-
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService);
133+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, Substitute.For<IMessageDraftStore>());
133134
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
134135
Assert.That("octokit/master", Is.EqualTo(vm.TargetBranch.DisplayName));
135136
}
@@ -166,7 +167,7 @@ public async Task CreatingPRsAsync(
166167
var ms = data.ModelService;
167168

168169
var prservice = new PullRequestService(data.GitClient, data.GitService, Substitute.For<IVSGitExt>(), Substitute.For<IGraphQLClientFactory>(), data.ServiceProvider.GetOperatingSystem(), Substitute.For<IUsageTracker>());
169-
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService);
170+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, Substitute.For<IMessageDraftStore>());
170171
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
171172

172173
// the TargetBranch property gets set to whatever the repo default is (we assume master here),
@@ -209,9 +210,104 @@ public async Task TemplateIsUsedIfPresentAsync()
209210
var prservice = Substitute.For<IPullRequestService>();
210211
prservice.GetPullRequestTemplate(data.ActiveRepo).Returns(Observable.Return("Test PR template"));
211212

212-
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService);
213+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService,
214+
Substitute.For<IMessageDraftStore>());
213215
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
214216

215217
Assert.That("Test PR template", Is.EqualTo(vm.Description));
216218
}
219+
220+
[Test]
221+
public async Task LoadsDraft()
222+
{
223+
var data = PrepareTestData("repo", "owner", "feature-branch", "owner", "master", "origin", false, false);
224+
var draftStore = Substitute.For<IMessageDraftStore>();
225+
draftStore.GetDraft<PullRequestDraft>("pr|http://github.com/owner/repo|feature-branch", string.Empty)
226+
.Returns(new PullRequestDraft
227+
{
228+
Title = "This is a Title.",
229+
Body = "This is a PR.",
230+
});
231+
232+
var prservice = Substitute.For<IPullRequestService>();
233+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, draftStore);
234+
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
235+
236+
Assert.That(vm.PRTitle, Is.EqualTo("This is a Title."));
237+
Assert.That(vm.Description, Is.EqualTo("This is a PR."));
238+
}
239+
240+
[Test]
241+
public async Task UpdatesDraftWhenDescriptionChanges()
242+
{
243+
var data = PrepareTestData("repo", "owner", "feature-branch", "owner", "master", "origin", false, false);
244+
var scheduler = new HistoricalScheduler();
245+
var draftStore = Substitute.For<IMessageDraftStore>();
246+
var prservice = Substitute.For<IPullRequestService>();
247+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, draftStore, scheduler);
248+
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
249+
250+
vm.Description = "Body changed.";
251+
252+
await draftStore.DidNotReceiveWithAnyArgs().UpdateDraft<PullRequestDraft>(null, null, null);
253+
254+
scheduler.AdvanceBy(TimeSpan.FromSeconds(1));
255+
256+
await draftStore.Received().UpdateDraft(
257+
"pr|http://github.com/owner/repo|feature-branch",
258+
string.Empty,
259+
Arg.Is<PullRequestDraft>(x => x.Body == "Body changed."));
260+
}
261+
262+
[Test]
263+
public async Task UpdatesDraftWhenTitleChanges()
264+
{
265+
var data = PrepareTestData("repo", "owner", "feature-branch", "owner", "master", "origin", false, false);
266+
var scheduler = new HistoricalScheduler();
267+
var draftStore = Substitute.For<IMessageDraftStore>();
268+
var prservice = Substitute.For<IPullRequestService>();
269+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, draftStore, scheduler);
270+
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
271+
272+
vm.PRTitle = "Title changed.";
273+
274+
await draftStore.DidNotReceiveWithAnyArgs().UpdateDraft<PullRequestDraft>(null, null, null);
275+
276+
scheduler.AdvanceBy(TimeSpan.FromSeconds(1));
277+
278+
await draftStore.Received().UpdateDraft(
279+
"pr|http://github.com/owner/repo|feature-branch",
280+
string.Empty,
281+
Arg.Is<PullRequestDraft>(x => x.Title == "Title changed."));
282+
}
283+
284+
[Test]
285+
public async Task DeletesDraftWhenPullRequestSubmitted()
286+
{
287+
var data = PrepareTestData("repo", "owner", "feature-branch", "owner", "master", "origin", false, false);
288+
var scheduler = new HistoricalScheduler();
289+
var draftStore = Substitute.For<IMessageDraftStore>();
290+
var prservice = Substitute.For<IPullRequestService>();
291+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, draftStore, scheduler);
292+
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
293+
294+
await vm.CreatePullRequest.Execute();
295+
296+
await draftStore.Received().DeleteDraft("pr|http://github.com/owner/repo|feature-branch", string.Empty);
297+
}
298+
299+
[Test]
300+
public async Task DeletesDraftWhenCanceled()
301+
{
302+
var data = PrepareTestData("repo", "owner", "feature-branch", "owner", "master", "origin", false, false);
303+
var scheduler = new HistoricalScheduler();
304+
var draftStore = Substitute.For<IMessageDraftStore>();
305+
var prservice = Substitute.For<IPullRequestService>();
306+
var vm = new PullRequestCreationViewModel(data.GetModelServiceFactory(), prservice, data.NotificationService, draftStore, scheduler);
307+
await vm.InitializeAsync(data.ActiveRepo, data.Connection);
308+
309+
await vm.Cancel.Execute();
310+
311+
await draftStore.Received().DeleteDraft("pr|http://github.com/owner/repo|feature-branch", string.Empty);
312+
}
217313
}

0 commit comments

Comments
 (0)