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

Commit 4464697

Browse files
Merge pull request #2017 from github/feature/pr-conversation
PR conversation view
2 parents 9b9a90a + 89f33ac commit 4464697

File tree

99 files changed

+3059
-288
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+3059
-288
lines changed

src/GitHub.App/Models/PullRequestModel.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ public string Title
110110
}
111111
}
112112

113-
PullRequestStateEnum status;
114-
public PullRequestStateEnum State
113+
PullRequestState status;
114+
public PullRequestState State
115115
{
116116
get { return status; }
117117
set
@@ -126,8 +126,8 @@ public PullRequestStateEnum State
126126
}
127127

128128
// TODO: Remove these property once maintainer workflow has been merged to master.
129-
public bool IsOpen => State == PullRequestStateEnum.Open;
130-
public bool Merged => State == PullRequestStateEnum.Merged;
129+
public bool IsOpen => State == PullRequestState.Open;
130+
public bool Merged => State == PullRequestState.Merged;
131131

132132
int commentCount;
133133
public int CommentCount

src/GitHub.App/Properties/AssemblyInfo.cs

+2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData")]
44
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Dialog.Clone")]
5+
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Documents")]
56
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels")]
67
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog")]
78
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog.Clone")]
9+
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Documents")]
810
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.GitHubPane")]

src/GitHub.App/SampleData/CommentViewModelDesigner.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Diagnostics.CodeAnalysis;
33
using System.Reactive;
4+
using System.Threading.Tasks;
5+
using GitHub.Models;
46
using GitHub.ViewModels;
57
using ReactiveUI;
68

@@ -22,7 +24,9 @@ public CommentViewModelDesigner()
2224
public CommentEditState EditState { get; set; }
2325
public bool IsReadOnly { get; set; }
2426
public bool IsSubmitting { get; set; }
27+
public bool CanCancel { get; } = true;
2528
public bool CanDelete { get; } = true;
29+
public string CommitCaption { get; set; } = "Comment";
2630
public ICommentThreadViewModel Thread { get; }
2731
public DateTimeOffset CreatedAt => DateTime.Now.Subtract(TimeSpan.FromDays(3));
2832
public IActorViewModel Author { get; set; }
@@ -31,7 +35,12 @@ public CommentViewModelDesigner()
3135
public ReactiveCommand<Unit, Unit> BeginEdit { get; }
3236
public ReactiveCommand<Unit, Unit> CancelEdit { get; }
3337
public ReactiveCommand<Unit, Unit> CommitEdit { get; }
34-
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
38+
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; } = ReactiveCommand.Create(() => { });
3539
public ReactiveCommand<Unit, Unit> Delete { get; }
40+
41+
public Task InitializeAsync(ICommentThreadViewModel thread, ActorModel currentUser, CommentModel comment, CommentEditState state)
42+
{
43+
return Task.CompletedTask;
44+
}
3645
}
3746
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using GitHub.Models;
4+
using GitHub.ViewModels;
5+
using GitHub.ViewModels.Documents;
6+
using ReactiveUI;
7+
8+
namespace GitHub.SampleData.Documents
9+
{
10+
public class IssueishCommentThreadViewModelDesigner : ViewModelBase, IIssueishCommentThreadViewModel
11+
{
12+
public IActorViewModel CurrentUser { get; } = new ActorViewModelDesigner("grokys");
13+
public Task InitializeAsync(ActorModel currentUser, IssueishDetailModel model, bool addPlaceholder) => Task.CompletedTask;
14+
public Task DeleteComment(ICommentViewModel comment) => Task.CompletedTask;
15+
public Task EditComment(ICommentViewModel comment) => Task.CompletedTask;
16+
public Task PostComment(ICommentViewModel comment) => Task.CompletedTask;
17+
public Task CloseOrReopen(ICommentViewModel comment) => Task.CompletedTask;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reactive;
4+
using System.Threading.Tasks;
5+
using GitHub.Models;
6+
using GitHub.ViewModels;
7+
using GitHub.ViewModels.Documents;
8+
using ReactiveUI;
9+
10+
namespace GitHub.SampleData.Documents
11+
{
12+
public class PullRequestPageViewModelDesigner : ViewModelBase, IPullRequestPageViewModel
13+
{
14+
public PullRequestPageViewModelDesigner()
15+
{
16+
Body = @"Save drafts of inline comments, PR reviews and PRs.
17+
18+
> Note: This feature required a refactoring of the comment view models because they now need async initialization and to be available from GitHub.App. This part of the PR has been submitted separately as #1993 to ease review. The two PRs can alternatively be reviewed as one if that's more convenient.
19+
20+
As described in #1905, it is easy to lose a comment that you're working on if you close the diff view accidentally. This PR saves drafts of comments as they are being written to an SQLite database.
21+
22+
In addition to saving drafts of inline comments, it also saves comments to PR reviews and PRs themselves.
23+
24+
The comments are written to an SQLite database directly instead of going through Akavache because in the case of inline reviews, there can be many drafts in progress on a separate file. When a diff is opened we need to look for any comments present on that file and show the most recent. That use-case didn't fit well with Akavache (being a pure key/value store).
25+
26+
## Testing
27+
28+
### Inline Comments
29+
30+
- Open a PR
31+
- Open the diff of a file
32+
- Start adding a comment
33+
- Close the comment by closing the peek view, or the document tab
34+
- Reopen the diff
35+
- You should see the comment displayed in edit mode with the draft of the comment you were previously writing
36+
37+
### PR reviews
38+
39+
- Open a PR
40+
- Click ""Add your review""
41+
- Start adding a review
42+
- Click the ""Back"" button and navigate to a different PR
43+
- Click the ""Back"" button and navigate to the original PR
44+
- Click ""Add your review""
45+
- You should see the the draft of the review you were previously writing
46+
47+
### PRs
48+
49+
-Click ""Create new"" at the top of the PR list
50+
- Start adding a PR title/ description
51+
- Close VS
52+
- Restart VS and click ""Create new"" again
53+
- You should see the the draft of the PR you were previously writing
54+
55+
Depends on #1993
56+
Fixes #1905";
57+
Timeline = new IViewModel[]
58+
{
59+
new CommitListViewModel(
60+
new CommitSummaryViewModel(new CommitModel
61+
{
62+
Author = new ActorModel { Login = "grokys" },
63+
AbbreviatedOid = "c7c7d25",
64+
MessageHeadline = "Refactor comment view models."
65+
}),
66+
new CommitSummaryViewModel(new CommitModel
67+
{
68+
Author = new ActorModel { Login = "shana" },
69+
AbbreviatedOid = "04e6a90",
70+
MessageHeadline = "Refactor comment view models.",
71+
})),
72+
new CommentViewModelDesigner
73+
{
74+
Author = new ActorViewModelDesigner("meaghanlewis"),
75+
Body = @"This is looking great! Really enjoying using this feature so far.
76+
77+
When leaving an inline comment, the comment posts successfully and then a new comment is drafted with the same text.",
78+
},
79+
new CommentViewModelDesigner
80+
{
81+
Author = new ActorViewModelDesigner("grokys"),
82+
Body = @"Oops, sorry about that @meaghanlewis - I was sure I tested those things, but must have got messed up again at some point. Should be fixed now.",
83+
},
84+
};
85+
}
86+
87+
public string Id { get; set; }
88+
public PullRequestState State { get; set; } = PullRequestState.Open;
89+
public IReadOnlyList<IViewModel> Timeline { get; }
90+
public string SourceBranchDisplayName { get; set; } = "feature/save-drafts";
91+
public string TargetBranchDisplayName { get; set; } = "master";
92+
public IActorViewModel Author { get; set; } = new ActorViewModelDesigner("grokys");
93+
public int CommitCount { get; set; } = 2;
94+
public string Body { get; set; }
95+
public int Number { get; set; } = 1994;
96+
public LocalRepositoryModel LocalRepository { get; }
97+
public RemoteRepositoryModel Repository { get; set; }
98+
public string Title { get; set; } = "Save drafts of comments";
99+
public Uri WebUrl { get; set; }
100+
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
101+
public ReactiveCommand<string, Unit> ShowCommit { get; }
102+
103+
104+
public Task InitializeAsync(RemoteRepositoryModel repository, LocalRepositoryModel localRepository, ActorModel currentUser, PullRequestDetailModel model)
105+
{
106+
throw new NotImplementedException();
107+
}
108+
}
109+
}

src/GitHub.App/SampleData/PullRequestDetailViewModelDesigner.cs

+1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ public PullRequestDetailViewModelDesigner()
124124
public ReactiveCommand<Unit, Unit> Pull { get; }
125125
public ReactiveCommand<Unit, Unit> Push { get; }
126126
public ReactiveCommand<Unit, Unit> SyncSubmodules { get; }
127+
public ReactiveCommand<Unit, Unit> OpenConversation { get; }
127128
public ReactiveCommand<Unit, Unit> OpenOnGitHub { get; }
128129
public ReactiveCommand<IPullRequestReviewSummaryViewModel, Unit> ShowReview { get; }
129130
public ReactiveCommand<IPullRequestCheckViewModel, Unit> ShowAnnotations { get; }

src/GitHub.App/SampleData/PullRequestListViewModelDesigner.cs

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public PullRequestListViewModelDesigner()
6363
public IReadOnlyList<string> States { get; }
6464
public Uri WebUrl => null;
6565
public ReactiveCommand<Unit, Unit> CreatePullRequest { get; }
66+
public ReactiveCommand<IPullRequestListItemViewModel, Unit> OpenConversation { get; }
6667
public ReactiveCommand<IIssueListItemViewModelBase, Unit> OpenItem { get; }
6768
public ReactiveCommand<IPullRequestListItemViewModel, IPullRequestListItemViewModel> OpenItemInBrowser { get; }
6869

src/GitHub.App/Services/FromGraphQlExtensions.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,16 @@ public static class FromGraphQlExtensions
3434
}
3535
}
3636

37-
public static PullRequestStateEnum FromGraphQl(this PullRequestState value)
37+
public static Models.PullRequestState FromGraphQl(this Octokit.GraphQL.Model.PullRequestState value)
3838
{
3939
switch (value)
4040
{
41-
case PullRequestState.Open:
42-
return PullRequestStateEnum.Open;
43-
case PullRequestState.Closed:
44-
return PullRequestStateEnum.Closed;
45-
case PullRequestState.Merged:
46-
return PullRequestStateEnum.Merged;
41+
case Octokit.GraphQL.Model.PullRequestState.Open:
42+
return Models.PullRequestState.Open;
43+
case Octokit.GraphQL.Model.PullRequestState.Closed:
44+
return Models.PullRequestState.Closed;
45+
case Octokit.GraphQL.Model.PullRequestState.Merged:
46+
return Models.PullRequestState.Merged;
4747
default:
4848
throw new ArgumentOutOfRangeException(nameof(value), value, null);
4949
}

src/GitHub.App/Services/GitClient.cs

+5
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ public Task Checkout(IRepository repository, string branchName)
181181
});
182182
}
183183

184+
public async Task<bool> CommitExists(IRepository repository, string sha)
185+
{
186+
return await Task.Run(() => repository.Lookup<Commit>(sha) != null).ConfigureAwait(false);
187+
}
188+
184189
public Task CreateBranch(IRepository repository, string branchName)
185190
{
186191
Guard.ArgumentNotNull(repository, nameof(repository));
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using GitHub.Api;
5+
using GitHub.Factories;
6+
using GitHub.Models;
7+
using GitHub.Primitives;
8+
using Octokit;
9+
using Octokit.GraphQL;
10+
using Octokit.GraphQL.Model;
11+
using static Octokit.GraphQL.Variable;
12+
13+
namespace GitHub.Services
14+
{
15+
/// <summary>
16+
/// Base class for issue and pull request services.
17+
/// </summary>
18+
public abstract class IssueishService : IIssueishService
19+
{
20+
static ICompiledQuery<CommentModel> postComment;
21+
readonly IApiClientFactory apiClientFactory;
22+
readonly IGraphQLClientFactory graphqlFactory;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="IssueishService"/> class.
26+
/// </summary>
27+
/// <param name="apiClientFactory">The API client factory.</param>
28+
/// <param name="graphqlFactory">The GraphQL client factory.</param>
29+
public IssueishService(
30+
IApiClientFactory apiClientFactory,
31+
IGraphQLClientFactory graphqlFactory)
32+
{
33+
this.apiClientFactory = apiClientFactory;
34+
this.graphqlFactory = graphqlFactory;
35+
}
36+
37+
/// <inheritdoc/>
38+
public async Task CloseIssueish(HostAddress address, string owner, string repository, int number)
39+
{
40+
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
41+
var update = new IssueUpdate { State = ItemState.Closed };
42+
await client.Issue.Update(owner, repository, number, update).ConfigureAwait(false);
43+
}
44+
45+
/// <inheritdoc/>
46+
public async Task ReopenIssueish(HostAddress address, string owner, string repository, int number)
47+
{
48+
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
49+
var update = new IssueUpdate { State = ItemState.Open };
50+
await client.Issue.Update(owner, repository, number, update).ConfigureAwait(false);
51+
}
52+
53+
/// <inheritdoc/>
54+
public async Task<CommentModel> PostComment(HostAddress address, string issueishId, string body)
55+
{
56+
var input = new AddCommentInput
57+
{
58+
Body = body,
59+
SubjectId = new ID(issueishId),
60+
};
61+
62+
if (postComment == null)
63+
{
64+
postComment = new Mutation()
65+
.AddComment(Var(nameof(input)))
66+
.CommentEdge
67+
.Node
68+
.Select(comment => new CommentModel
69+
{
70+
Author = new ActorModel
71+
{
72+
Login = comment.Author.Login,
73+
AvatarUrl = comment.Author.AvatarUrl(null),
74+
},
75+
Body = comment.Body,
76+
CreatedAt = comment.CreatedAt,
77+
DatabaseId = comment.DatabaseId.Value,
78+
Id = comment.Id.Value,
79+
Url = comment.Url,
80+
}).Compile();
81+
}
82+
83+
var vars = new Dictionary<string, object>
84+
{
85+
{ nameof(input), input },
86+
};
87+
88+
var graphql = await graphqlFactory.CreateConnection(address).ConfigureAwait(false);
89+
return await graphql.Run(postComment, vars).ConfigureAwait(false);
90+
}
91+
92+
public async Task DeleteComment(
93+
HostAddress address,
94+
string owner,
95+
string repository,
96+
int commentId)
97+
{
98+
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
99+
await client.Issue.Comment.Delete(owner, repository, commentId).ConfigureAwait(false);
100+
}
101+
102+
public async Task EditComment(
103+
HostAddress address,
104+
string owner,
105+
string repository,
106+
int commentId,
107+
string body)
108+
{
109+
var client = await apiClientFactory.CreateGitHubClient(address).ConfigureAwait(false);
110+
await client.Issue.Comment.Update(owner, repository, commentId, body).ConfigureAwait(false);
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)