diff --git a/src/GitHub.App/SampleData/IssueListItemViewModelDesigner.cs b/src/GitHub.App/SampleData/IssueListItemViewModelDesigner.cs new file mode 100644 index 0000000000..22ed978128 --- /dev/null +++ b/src/GitHub.App/SampleData/IssueListItemViewModelDesigner.cs @@ -0,0 +1,18 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using GitHub.ViewModels; +using GitHub.ViewModels.GitHubPane; + +namespace GitHub.SampleData +{ + [ExcludeFromCodeCoverage] + public class IssueListItemViewModelDesigner : ViewModelBase, IIssueListItemViewModel + { + public string Id { get; set; } + public IActorViewModel Author { get; set; } + public int CommentCount { get; set; } + public int Number { get; set; } + public string Title { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + } +} diff --git a/src/GitHub.App/SampleData/IssueListViewModelDesigner.cs b/src/GitHub.App/SampleData/IssueListViewModelDesigner.cs new file mode 100644 index 0000000000..c75bbec671 --- /dev/null +++ b/src/GitHub.App/SampleData/IssueListViewModelDesigner.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Reactive; +using System.Threading.Tasks; +using System.Windows.Data; +using GitHub.Models; +using GitHub.ViewModels; +using GitHub.ViewModels.GitHubPane; +using ReactiveUI; + +namespace GitHub.SampleData +{ + [ExcludeFromCodeCoverage] + public class IssueListViewModelDesigner : PanePageViewModelBase, IIssueListViewModel + { + public IssueListViewModelDesigner() + { + Items = new[] + { + new IssueListItemViewModelDesigner + { + Number = 1855, + Title = "Do something sensible when things go wrong", + Author = new ActorViewModelDesigner("jcansdale"), + UpdatedAt = DateTimeOffset.Now - TimeSpan.FromDays(1), + }, + new IssueListItemViewModelDesigner + { + Number = 1865, + Title = "Installer hangs after download of GHfVS is complete", + Author = new ActorViewModelDesigner("meaghanlewis"), + CommentCount = 7, + UpdatedAt = DateTimeOffset.Now - TimeSpan.FromMinutes(2), + }, + new IssueListItemViewModelDesigner + { + Number = 1908, + Title = "Unicode error can occur when viewing diff for PR files", + Author = new ActorViewModelDesigner("StanleyGoldman"), + CommentCount = 0, + UpdatedAt = DateTimeOffset.Now - TimeSpan.FromHours(5), + }, + }; + + ItemsView = CollectionViewSource.GetDefaultView(Items); + States = new[] { "Open", "Closed", "All" }; + SelectedState = "Open"; + } + + public IUserFilterViewModel AuthorFilter { get; set; } + public IReadOnlyList Items { get; } + public ICollectionView ItemsView { get; } + public LocalRepositoryModel LocalRepository { get; set; } + public IssueListMessage Message { get; set; } + public RepositoryModel RemoteRepository { get; set; } + public IReadOnlyList Forks { get; } + public string SearchQuery { get; set; } + public string SelectedState { get; set; } + public string StateCaption { get; set; } + public IReadOnlyList States { get; } + public Uri WebUrl => null; + public ReactiveCommand OpenItem { get; } + public ReactiveCommand OpenItemInBrowser { get; } + + public Task InitializeAsync(LocalRepositoryModel repository, IConnection connection) => Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/GitHub.App/Services/FromGraphQlExtensions.cs b/src/GitHub.App/Services/FromGraphQlExtensions.cs index 06a1c46e2e..d9b350652d 100644 --- a/src/GitHub.App/Services/FromGraphQlExtensions.cs +++ b/src/GitHub.App/Services/FromGraphQlExtensions.cs @@ -4,6 +4,7 @@ using CheckAnnotationLevel = GitHub.Models.CheckAnnotationLevel; using CheckConclusionState = GitHub.Models.CheckConclusionState; using CheckStatusState = GitHub.Models.CheckStatusState; +using IssueState = GitHub.Models.IssueState; using PullRequestReviewState = GitHub.Models.PullRequestReviewState; using StatusState = GitHub.Models.StatusState; @@ -38,6 +39,19 @@ public static class FromGraphQlExtensions } } + public static IssueState FromGraphQl(this Octokit.GraphQL.Model.IssueState value) + { + switch (value) + { + case Octokit.GraphQL.Model.IssueState.Open: + return IssueState.Open; + case Octokit.GraphQL.Model.IssueState.Closed: + return IssueState.Closed; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } + public static Models.PullRequestState FromGraphQl(this Octokit.GraphQL.Model.PullRequestState value) { switch (value) @@ -121,5 +135,18 @@ public static CheckAnnotationLevel FromGraphQl(this Octokit.GraphQL.Model.CheckA throw new ArgumentOutOfRangeException(nameof(value), value, null); } } + + public static Octokit.GraphQL.Model.IssueState ToGraphQL(this IssueState value) + { + switch (value) + { + case IssueState.Open: + return Octokit.GraphQL.Model.IssueState.Open; + case IssueState.Closed: + return Octokit.GraphQL.Model.IssueState.Closed; + default: + throw new ArgumentOutOfRangeException(nameof(value), value, null); + } + } } } \ No newline at end of file diff --git a/src/GitHub.App/Services/IssueService.cs b/src/GitHub.App/Services/IssueService.cs new file mode 100644 index 0000000000..0c6d44c9e8 --- /dev/null +++ b/src/GitHub.App/Services/IssueService.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Api; +using GitHub.Models; +using GitHub.Primitives; +using Octokit.GraphQL; +using Octokit.GraphQL.Model; +using static Octokit.GraphQL.Variable; +using IssueState = GitHub.Models.IssueState; + +namespace GitHub.Services +{ + [Export(typeof(IIssueService))] + [PartCreationPolicy(CreationPolicy.Shared)] + public class IssueService : IIssueService + { + static ICompiledQuery> readAssignableUsers; + static ICompiledQuery> readIssues; + static ICompiledQuery readIssue; + + readonly IGraphQLClientFactory graphqlFactory; + + [ImportingConstructor] + public IssueService(IGraphQLClientFactory graphqlFactory) + { + this.graphqlFactory = graphqlFactory; + } + + public async Task> ReadIssues( + HostAddress address, + string owner, + string name, + string after, + IssueState[] states) + { + if (readIssues == null) + { + readIssues = new Query() + .Repository(owner: Var(nameof(owner)), name: Var(nameof(name))) + .Issues( + first: 100, + after: Var(nameof(after)), + orderBy: new IssueOrder { Direction = OrderDirection.Desc, Field = IssueOrderField.CreatedAt }, + states: Var(nameof(states))) + .Select(page => new Page + { + EndCursor = page.PageInfo.EndCursor, + HasNextPage = page.PageInfo.HasNextPage, + TotalCount = page.TotalCount, + Items = page.Nodes.Select(issue => new IssueListItemModel + { + Id = issue.Id.Value, + Author = new ActorModel + { + Login = issue.Author.Login, + AvatarUrl = issue.Author.AvatarUrl(null), + }, + CommentCount = issue.Comments(0, null, null, null).TotalCount, + Number = issue.Number, + State = issue.State.FromGraphQl(), + Title = issue.Title, + UpdatedAt = issue.UpdatedAt, + }).ToList(), + }).Compile(); + } + + var graphql = await graphqlFactory.CreateConnection(address).ConfigureAwait(false); + var vars = new Dictionary + { + { nameof(owner), owner }, + { nameof(name), name }, + { nameof(after), after }, + { nameof(states), states.Select(x => x.ToGraphQL()).ToList() }, + }; + + return await graphql.Run(readIssues, vars).ConfigureAwait(false); + } + + public async Task ReadIssue(HostAddress address, string owner, string name, int number) + { + if (readIssue == null) + { + readIssue = new Query() + .Repository(owner: Var(nameof(owner)), name: Var(nameof(name))) + .Issue(Var(nameof(number))) + .Select(issue => new IssueDetailModel + { + Id = issue.Id.Value, + Number = issue.Number, + Author = new ActorModel + { + Login = issue.Author.Login, + AvatarUrl = issue.Author.AvatarUrl(null), + }, + Title = issue.Title, + Body = issue.Body, + UpdatedAt = issue.UpdatedAt, + Comments = issue.Comments(null, null, null, null).AllPages().Select(comment => new CommentModel + { + Id = comment.Id.Value, + DatabaseId = comment.DatabaseId.Value, + Author = new ActorModel + { + Login = comment.Author.Login, + AvatarUrl = comment.Author.AvatarUrl(null), + }, + Body = comment.Body, + CreatedAt = comment.CreatedAt, + Url = comment.Url, + }).ToList(), + }).Compile(); + } + + var graphql = await graphqlFactory.CreateConnection(address).ConfigureAwait(false); + var vars = new Dictionary + { + { nameof(owner), owner }, + { nameof(name), name }, + { nameof(number), number }, + }; + + return await graphql.Run(readIssue, vars).ConfigureAwait(false); + } + + public async Task> ReadAssignableUsers( + HostAddress address, + string owner, + string name, + string after) + { + if (readAssignableUsers == null) + { + readAssignableUsers = new Query() + .Repository(owner: Var(nameof(owner)), name: Var(nameof(name))) + .AssignableUsers(first: 100, after: Var(nameof(after))) + .Select(connection => new Page + { + EndCursor = connection.PageInfo.EndCursor, + HasNextPage = connection.PageInfo.HasNextPage, + TotalCount = connection.TotalCount, + Items = connection.Nodes.Select(user => new ActorModel + { + AvatarUrl = user.AvatarUrl(30), + Login = user.Login, + }).ToList(), + }).Compile(); + } + + var graphql = await graphqlFactory.CreateConnection(address).ConfigureAwait(false); + var vars = new Dictionary + { + { nameof(owner), owner }, + { nameof(name), name }, + { nameof(after), after }, + }; + + return await graphql.Run(readAssignableUsers, vars).ConfigureAwait(false); + } + } +} diff --git a/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs index f161ed36c2..e3bf6d6047 100644 --- a/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs +++ b/src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs @@ -54,6 +54,7 @@ public sealed class GitHubPaneViewModel : ViewModelBase, IGitHubPaneViewModel, I readonly ObservableAsPropertyHelper title; readonly ReactiveCommand refresh; readonly ReactiveCommand showPullRequests; + readonly ReactiveCommand showIssues; readonly ReactiveCommand openInBrowser; readonly ReactiveCommand help; IDisposable connectionSubscription; @@ -152,6 +153,9 @@ public GitHubPaneViewModel( ShowPullRequests, this.WhenAny(x => x.Content, x => x.Value == navigator)); + showIssues = ReactiveCommand.CreateFromTask( + ShowIssues, + this.WhenAny(x => x.Content, x => x.Value == navigator)); openInBrowser = ReactiveCommand.Create( () => { @@ -303,6 +307,12 @@ public Task ShowCreatePullRequest() return NavigateTo(x => x.InitializeAsync(LocalRepository, Connection)); } + /// + public Task ShowIssues() + { + return NavigateTo(x => x.InitializeAsync(LocalRepository, Connection)); + } + /// public Task ShowPullRequests() { @@ -373,6 +383,7 @@ async Task CreateInitializeTask(IServiceProvider paneServiceProvider) var menuService = (IMenuCommandService)paneServiceProvider.GetService(typeof(IMenuCommandService)); BindNavigatorCommand(menuService, PkgCmdIDList.pullRequestCommand, showPullRequests); + BindNavigatorCommand(menuService, PkgCmdIDList.issuesCommand, showIssues); BindNavigatorCommand(menuService, PkgCmdIDList.backCommand, navigator.NavigateBack); BindNavigatorCommand(menuService, PkgCmdIDList.forwardCommand, navigator.NavigateForward); BindNavigatorCommand(menuService, PkgCmdIDList.refreshCommand, refresh); diff --git a/src/GitHub.App/ViewModels/GitHubPane/IssueListItemViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/IssueListItemViewModel.cs new file mode 100644 index 0000000000..adc12a12a3 --- /dev/null +++ b/src/GitHub.App/ViewModels/GitHubPane/IssueListItemViewModel.cs @@ -0,0 +1,43 @@ +using System; +using GitHub.Models; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + /// A view model which displays an item in a . + /// + public class IssueListItemViewModel : ViewModelBase, IIssueListItemViewModel + { + /// + /// Initializes a new instance of the class. + /// + /// The underlying issue item model. + public IssueListItemViewModel(IssueListItemModel model) + { + Id = model.Id; + Author = new ActorViewModel(model.Author); + CommentCount = model.CommentCount; + Number = model.Number; + Title = model.Title; + UpdatedAt = model.UpdatedAt; + } + + /// + public string Id { get; } + + /// + public IActorViewModel Author { get; } + + /// + public int CommentCount { get; } + + /// + public int Number { get; } + + /// + public string Title { get; } + + /// + public DateTimeOffset UpdatedAt { get; } + } +} diff --git a/src/GitHub.App/ViewModels/GitHubPane/IssueListViewModel.cs b/src/GitHub.App/ViewModels/GitHubPane/IssueListViewModel.cs new file mode 100644 index 0000000000..cd8d1f8a0a --- /dev/null +++ b/src/GitHub.App/ViewModels/GitHubPane/IssueListViewModel.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using GitHub.Collections; +using GitHub.Commands; +using GitHub.Extensions; +using GitHub.Models; +using GitHub.Primitives; +using GitHub.Services; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + /// A view model which displays an issue list. + /// + [Export(typeof(IIssueListViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public class IssueListViewModel : IssueListViewModelBase, IIssueListViewModel + { + static readonly IReadOnlyList states = new[] { "Open", "Closed", "All" }; + readonly IIssueService service; + readonly IOpenIssueishDocumentCommand showIssueDetails; + ObservableAsPropertyHelper webUrl; + + /// + /// Initializes a new instance of the class. + /// + /// The session manager. + /// The repository service. + /// The issue service. + [ImportingConstructor] + public IssueListViewModel( + IRepositoryService repositoryService, + IIssueService service, + IOpenIssueishDocumentCommand showIssueDetails) + : base(repositoryService) + { + Guard.ArgumentNotNull(service, nameof(service)); + + this.service = service; + this.showIssueDetails = showIssueDetails; + + webUrl = this.WhenAnyValue(x => x.RemoteRepository) + .Select(x => x?.CloneUrl?.ToRepositoryUrl().Append("issue")) + .ToProperty(this, x => x.WebUrl); + OpenItemInBrowser = ReactiveCommand.Create(x => x); + } + + /// + public override IReadOnlyList States => states; + + /// + public Uri WebUrl => webUrl.Value; + + /// + public ReactiveCommand OpenItemInBrowser { get; } + + /// + protected override IVirtualizingListSource CreateItemSource() + { + return new ItemSource(this); + } + + /// + protected override Task DoOpenItem(IIssueListItemViewModelBase item) + { + var i = (IIssueListItemViewModel)item; + return showIssueDetails.Execute(new OpenIssueishParams( + HostAddress.Create(LocalRepository.CloneUrl), + RemoteRepository.Owner, + RemoteRepository.Name, + i.Number)); + } + + /// + protected override Task> LoadAuthors(string after) + { + return service.ReadAssignableUsers( + HostAddress.Create(LocalRepository.CloneUrl), + LocalRepository.Owner, + LocalRepository.Name, + after); + } + + class ItemSource : SequentialListSource + { + readonly IssueListViewModel owner; + + public ItemSource(IssueListViewModel owner) + { + this.owner = owner; + } + + protected override IIssueListItemViewModelBase CreateViewModel(IssueListItemModel model) + { + return new IssueListItemViewModel(model); + } + + protected override async Task> LoadPage(string after) + { + IssueState[] states; + + switch (owner.SelectedState) + { + case "Open": + states = new[] { IssueState.Open }; + break; + case "Closed": + states = new[] { IssueState.Closed}; + break; + default: + states = new[] { IssueState.Open, IssueState.Closed }; + break; + } + + var result = await owner.service.ReadIssues( + HostAddress.Create(owner.RemoteRepository.CloneUrl), + owner.RemoteRepository.Owner, + owner.RemoteRepository.Name, + after, + states).ConfigureAwait(false); + return result; + } + } + } +} diff --git a/src/GitHub.Exports.Reactive/Services/IIssueService.cs b/src/GitHub.Exports.Reactive/Services/IIssueService.cs new file mode 100644 index 0000000000..3557010843 --- /dev/null +++ b/src/GitHub.Exports.Reactive/Services/IIssueService.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using GitHub.Models; +using GitHub.Primitives; + +namespace GitHub.Services +{ + /// + /// Provides services for interacting with issues. + /// + public interface IIssueService + { + /// + /// Reads a page of issue items. + /// + /// The host address. + /// The repository owner. + /// The repository name. + /// The end cursor of the previous page, or null for the first page. + /// The issue states to filter by + /// A page of issue item models. + Task> ReadIssues( + HostAddress address, + string owner, + string name, + string after, + IssueState[] states); + + /// + /// Reads the details of a specified issue. + /// + /// The host address. + /// The repository owner. + /// The repository name. + /// The issue number. + /// A task returning the issue model. + Task ReadIssue( + HostAddress address, + string owner, + string name, + int number); + + /// + /// Reads a page of users that can be assigned to issues. + /// + /// The host address. + /// The repository owner. + /// The repository name. + /// The end cursor of the previous page, or null for the first page. + /// A page of author models. + Task> ReadAssignableUsers( + HostAddress address, + string owner, + string name, + string after); + } +} diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IIssueListItemViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IIssueListItemViewModel.cs new file mode 100644 index 0000000000..9eee36602f --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IIssueListItemViewModel.cs @@ -0,0 +1,25 @@ +using System; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + /// Represents an item in the issue list. + /// + public interface IIssueListItemViewModel : IIssueListItemViewModelBase + { + /// + /// Gets the ID of the issue. + /// + string Id { get; } + + /// + /// Gets the number of comments in the issue. + /// + int CommentCount { get; } + + /// + /// Gets the last updated time of the issue. + /// + DateTimeOffset UpdatedAt { get; } + } +} diff --git a/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IIssueListViewModel.cs b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IIssueListViewModel.cs new file mode 100644 index 0000000000..2e1fdf0f11 --- /dev/null +++ b/src/GitHub.Exports.Reactive/ViewModels/GitHubPane/IIssueListViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Reactive; +using ReactiveUI; + +namespace GitHub.ViewModels.GitHubPane +{ + /// + /// Represents a view model which displays an issue list. + /// + public interface IIssueListViewModel : IIssueListViewModelBase, IOpenInBrowser + { + /// + /// Gets a command that opens an issue on GitHub. + /// + ReactiveCommand OpenItemInBrowser { get; } + } +} diff --git a/src/GitHub.Exports/Models/IssueDetailModel.cs b/src/GitHub.Exports/Models/IssueDetailModel.cs new file mode 100644 index 0000000000..049b83c066 --- /dev/null +++ b/src/GitHub.Exports/Models/IssueDetailModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace GitHub.Models +{ + /// + /// Holds the details of an issue. + /// + public class IssueDetailModel : IssueishDetailModel + { + /// + /// Gets or sets the issue comments. + /// + public IReadOnlyList Comments { get; set; } + } +} diff --git a/src/GitHub.Exports/Models/IssueListItemModel.cs b/src/GitHub.Exports/Models/IssueListItemModel.cs new file mode 100644 index 0000000000..f4142a6ede --- /dev/null +++ b/src/GitHub.Exports/Models/IssueListItemModel.cs @@ -0,0 +1,51 @@ +using System; + +namespace GitHub.Models +{ + public enum IssueState + { + Open, + Closed, + } + + /// + /// Holds an overview of an issue for display in the issue list. + /// + public class IssueListItemModel + { + /// + /// Gets or sets the GraphQL ID of the issue. + /// + public string Id { get; set; } + + /// + /// Gets or sets the issue number. + /// + public int Number { get; set; } + + /// + /// Gets or sets the issue author. + /// + public ActorModel Author { get; set; } + + /// + /// Gets or sets the number of comments on the issue. + /// + public int CommentCount { get; set; } + + /// + /// Gets or sets the issue title. + /// + public string Title { get; set; } + + /// + /// Gets or sets the issue state (open, closed). + /// + public IssueState State { get; set; } + + /// + /// Gets or sets the date/time at which the issue was last updated. + /// + public DateTimeOffset UpdatedAt { get; set; } + } +} diff --git a/src/GitHub.Exports/Settings/PkgCmdID.cs b/src/GitHub.Exports/Settings/PkgCmdID.cs index 64160c738e..f941ed2b0b 100644 --- a/src/GitHub.Exports/Settings/PkgCmdID.cs +++ b/src/GitHub.Exports/Settings/PkgCmdID.cs @@ -19,6 +19,7 @@ public static class PkgCmdIDList public const int forwardCommand = 0x301; public const int refreshCommand = 0x302; public const int pullRequestCommand = 0x310; + public const int issuesCommand = 0x311; public const int createGistCommand = 0x400; public const int createGistEnterpriseCommand = 0x401; public const int openLinkCommand = 0x100; diff --git a/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs b/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs index 69eb5c1c2f..61e065e7c0 100644 --- a/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs +++ b/src/GitHub.Exports/ViewModels/GitHubPane/IGitHubPaneViewModel.cs @@ -78,6 +78,11 @@ public interface IGitHubPaneViewModel : IViewModel /// The URL. Task NavigateTo(Uri uri); + /// + /// Shows the issue list in the GitHub pane. + /// + Task ShowIssues(); + /// /// Shows the pull reqest list in the GitHub pane. /// diff --git a/src/GitHub.Resources/Resources.Designer.cs b/src/GitHub.Resources/Resources.Designer.cs index 0db38b220b..2e60f145f2 100644 --- a/src/GitHub.Resources/Resources.Designer.cs +++ b/src/GitHub.Resources/Resources.Designer.cs @@ -2090,6 +2090,15 @@ public static string TeamExplorerWelcomeMessage { } } + /// + /// Looks up a localized string similar to There aren't any open issues.. + /// + public static string ThereArentAnyOpenIssues { + get { + return ResourceManager.GetString("ThereArentAnyOpenIssues", resourceCulture); + } + } + /// /// Looks up a localized string similar to There aren't any open pull requests. /// diff --git a/src/GitHub.Resources/Resources.resx b/src/GitHub.Resources/Resources.resx index eef3e7efed..5a592e6ce4 100644 --- a/src/GitHub.Resources/Resources.resx +++ b/src/GitHub.Resources/Resources.resx @@ -866,6 +866,8 @@ https://git-scm.com/download/win and others + + There aren't any open issues. This conversation was marked as resolved @@ -879,8 +881,7 @@ https://git-scm.com/download/win Contributed to repositories - Search or enter a URL - + Search or enter a URL Browse... diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListItemView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListItemView.xaml new file mode 100644 index 0000000000..7010c457b4 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListItemView.xaml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + by + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListItemView.xaml.cs b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListItemView.xaml.cs new file mode 100644 index 0000000000..0a6002aa5f --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListItemView.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace GitHub.VisualStudio.Views.GitHubPane +{ + public partial class IssueListItemView : UserControl + { + public IssueListItemView() + { + InitializeComponent(); + } + } +} diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListView.xaml b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListView.xaml new file mode 100644 index 0000000000..5be0d42463 --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListView.xaml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + / + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListView.xaml.cs b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListView.xaml.cs new file mode 100644 index 0000000000..615447ddbc --- /dev/null +++ b/src/GitHub.VisualStudio.UI/Views/GitHubPane/IssueListView.xaml.cs @@ -0,0 +1,139 @@ +using System; +using System.ComponentModel.Composition; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using GitHub.Exports; +using GitHub.Extensions; +using GitHub.Services; +using GitHub.UI.Helpers; +using GitHub.ViewModels.GitHubPane; +using ReactiveUI; + +namespace GitHub.VisualStudio.Views.GitHubPane +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] + [ExportViewFor(typeof(IIssueListViewModel))] + [PartCreationPolicy(CreationPolicy.NonShared)] + public partial class IssueListView : UserControl + { + IDisposable subscription; + + [ImportingConstructor] + public IssueListView() + { + InitializeComponent(); + + DataContextChanged += (s, e) => + { + var vm = DataContext as IIssueListViewModel; + subscription?.Dispose(); + subscription = null; + + if (vm != null) + { + subscription = new CompositeDisposable( + vm.AuthorFilter.WhenAnyValue(x => x.Selected) + .Skip(1) + .Subscribe(_ => authorFilterDropDown.IsOpen = false), + vm.OpenItemInBrowser.Subscribe(OpenInBrowser)); + } + }; + + Unloaded += (s, e) => + { + subscription?.Dispose(); + subscription = null; + }; + } + + [Import] + IVisualStudioBrowser VisualStudioBrowser { get; set; } + + void OpenInBrowser(IIssueListItemViewModel item) + { + var vm = DataContext as IIssueListViewModel; + + if (vm != null) + { + var uri = vm.RemoteRepository.CloneUrl.ToRepositoryUrl().Append("issue/" + item.Number); + VisualStudioBrowser.OpenUrl(uri); + } + } + + void ListBox_KeyDown(object sender, KeyEventArgs e) + { + var listBox = (ListBox)sender; + + if (listBox.SelectedItem != null && e.Key == Key.Enter) + { + var vm = DataContext as IIssueListViewModel; + var pr = (IIssueListItemViewModel)listBox.SelectedItem; + vm.OpenItem.Execute(pr); + } + } + + void ListBoxItem_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + var control = sender as ListBoxItem; + var pr = control?.DataContext as IIssueListItemViewModel; + var vm = DataContext as IIssueListViewModel; + + if (pr != null && vm != null) + { + vm.OpenItem.Execute(pr); + } + } + + void authorFilterDropDown_PopupOpened(object sender, EventArgs e) + { + authorFilter.FocusSearchBox(); + } + + void ListBox_ContextMenuOpening(object sender, ContextMenuEventArgs e) + { + ApplyContextMenuBinding(sender, e); + } + + void ApplyContextMenuBinding(object sender, ContextMenuEventArgs e) where TItem : Control + { + var container = (Control)sender; + var item = GetVisual(e.OriginalSource)?.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + + e.Handled = true; + + if (item?.DataContext is IIssueListItemViewModel listItem) + { + container.ContextMenu.DataContext = this.DataContext; + + foreach (var menuItem in container.ContextMenu.Items.OfType()) + { + menuItem.CommandParameter = listItem; + } + + e.Handled = false; + } + } + + Visual GetVisual(object element) + { + if (element is Visual v) + { + return v; + } + else if (element is TextElement e) + { + return e.Parent as Visual; + } + else + { + return null; + } + } + } +} diff --git a/src/GitHub.VisualStudio.Vsix/GitHub.VisualStudio.imagemanifest b/src/GitHub.VisualStudio.Vsix/GitHub.VisualStudio.imagemanifest index 590146df45..3f899e1847 100644 --- a/src/GitHub.VisualStudio.Vsix/GitHub.VisualStudio.imagemanifest +++ b/src/GitHub.VisualStudio.Vsix/GitHub.VisualStudio.imagemanifest @@ -15,6 +15,7 @@ + @@ -45,5 +46,8 @@ + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj index 214527e64c..0c12b84b67 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.csproj @@ -201,6 +201,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile Designer diff --git a/src/GitHub.VisualStudio/GitHub.VisualStudio.vsct b/src/GitHub.VisualStudio/GitHub.VisualStudio.vsct index df253ef9a2..5eb71d0b0b 100644 --- a/src/GitHub.VisualStudio/GitHub.VisualStudio.vsct +++ b/src/GitHub.VisualStudio/GitHub.VisualStudio.vsct @@ -173,6 +173,15 @@ + +