diff --git a/GitHubExtension.Test/Controls/CreateFormsTests.cs b/GitHubExtension.Test/Controls/CreateFormsTests.cs new file mode 100644 index 0000000..8877bc3 --- /dev/null +++ b/GitHubExtension.Test/Controls/CreateFormsTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using GitHubExtension.Controls; +using GitHubExtension.Controls.Forms; +using GitHubExtension.DataManager; +using GitHubExtension.Helpers; +using Moq; + +namespace GitHubExtension.Test.Controls; + +[TestClass] +public class CreateFormsTests +{ + private static Mock CreateResources() + { + var resources = new Mock(); + resources.Setup(x => x.GetResource(It.IsAny(), null)).Returns((key, _) => key); + return resources; + } + + [TestMethod] + [TestCategory("Unit")] + public void CreateIssueForm_TemplateJson_IsValidJson() + { + var createManager = new Mock(); + var form = new CreateIssueForm(createManager.Object, CreateResources().Object, "owner/repo"); + + var json = form.TemplateJson; + + var parsed = JsonDocument.Parse(json); + Assert.AreEqual("AdaptiveCard", parsed.RootElement.GetProperty("type").GetString()); + } + + [TestMethod] + [TestCategory("Unit")] + public void CreatePullRequestForm_TemplateJson_IsValidJson() + { + var createManager = new Mock(); + var form = new CreatePullRequestForm(createManager.Object, CreateResources().Object); + + var json = form.TemplateJson; + + var parsed = JsonDocument.Parse(json); + Assert.AreEqual("AdaptiveCard", parsed.RootElement.GetProperty("type").GetString()); + } + + [TestMethod] + [TestCategory("Unit")] + public void CreateBranchForm_TemplateJson_IsValidJson() + { + var createManager = new Mock(); + var form = new CreateBranchForm(createManager.Object, CreateResources().Object); + + var json = form.TemplateJson; + + var parsed = JsonDocument.Parse(json); + Assert.AreEqual("AdaptiveCard", parsed.RootElement.GetProperty("type").GetString()); + } + + [TestMethod] + [TestCategory("Unit")] + public void AddCommentForm_TemplateJson_IsValidJson() + { + var createManager = new Mock(); + var issue = new Mock(); + issue.Setup(x => x.Title).Returns("An issue title"); + var form = new AddCommentForm(issue.Object, createManager.Object, CreateResources().Object); + + var json = form.TemplateJson; + + var parsed = JsonDocument.Parse(json); + Assert.AreEqual("AdaptiveCard", parsed.RootElement.GetProperty("type").GetString()); + } +} diff --git a/GitHubExtension.Test/Controls/GitHubCreateManagerTests.cs b/GitHubExtension.Test/Controls/GitHubCreateManagerTests.cs new file mode 100644 index 0000000..b59007d --- /dev/null +++ b/GitHubExtension.Test/Controls/GitHubCreateManagerTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using GitHubExtension.DataManager.Data; + +namespace GitHubExtension.Test.Controls; + +[TestClass] +public class GitHubCreateManagerTests +{ + [TestMethod] + [TestCategory("Unit")] + public void ParseOwnerRepo_ShortForm_ParsesOwnerAndRepo() + { + var (owner, repo) = GitHubCreateManager.ParseOwnerRepo("octocat/hello-world"); + + Assert.AreEqual("octocat", owner); + Assert.AreEqual("hello-world", repo); + } + + [TestMethod] + [TestCategory("Unit")] + public void ParseOwnerRepo_WithSurroundingWhitespace_Trims() + { + var (owner, repo) = GitHubCreateManager.ParseOwnerRepo(" octocat / hello-world "); + + Assert.AreEqual("octocat", owner); + Assert.AreEqual("hello-world", repo); + } + + [TestMethod] + [TestCategory("Unit")] + public void ParseOwnerRepo_FullUrl_ParsesOwnerAndRepo() + { + var (owner, repo) = GitHubCreateManager.ParseOwnerRepo("https://github.com/octocat/hello-world"); + + Assert.AreEqual("octocat", owner); + Assert.AreEqual("hello-world", repo); + } + + [TestMethod] + [TestCategory("Unit")] + [ExpectedException(typeof(ArgumentException))] + public void ParseOwnerRepo_Empty_Throws() + { + GitHubCreateManager.ParseOwnerRepo(" "); + } + + [TestMethod] + [TestCategory("Unit")] + [ExpectedException(typeof(ArgumentException))] + public void ParseOwnerRepo_MissingRepo_Throws() + { + GitHubCreateManager.ParseOwnerRepo("octocat"); + } +} diff --git a/GitHubExtension.Test/Controls/MutationCommandsFactoryTests.cs b/GitHubExtension.Test/Controls/MutationCommandsFactoryTests.cs index 6647306..7a021ef 100644 --- a/GitHubExtension.Test/Controls/MutationCommandsFactoryTests.cs +++ b/GitHubExtension.Test/Controls/MutationCommandsFactoryTests.cs @@ -16,10 +16,11 @@ public class MutationCommandsFactoryTests private static (MutationCommandsFactory Factory, Mock Manager, MutationMediator Mediator) CreateFactory() { var manager = new Mock(); + var createManager = new Mock(); var mediator = new MutationMediator(); var resources = new Mock(); resources.Setup(x => x.GetResource(It.IsAny(), null)).Returns((key, _) => key); - var factory = new MutationCommandsFactory(manager.Object, mediator, resources.Object); + var factory = new MutationCommandsFactory(manager.Object, createManager.Object, mediator, resources.Object); return (factory, manager, mediator); } @@ -51,7 +52,7 @@ public void GetIssueCommands_OpenIssue_ReturnsCloseCommand() var (factory, _, _) = CreateFactory(); var commands = factory.GetIssueCommands(CreateIssue("Open").Object).ToList(); - Assert.AreEqual(1, commands.Count); + Assert.AreEqual(2, commands.Count); Assert.AreEqual("Commands_CloseIssue", commands[0].Command!.Name); Assert.IsTrue(commands[0].IsCritical); } @@ -63,7 +64,7 @@ public void GetIssueCommands_ClosedIssue_ReturnsReopenCommand() var (factory, _, _) = CreateFactory(); var commands = factory.GetIssueCommands(CreateIssue("Closed").Object).ToList(); - Assert.AreEqual(1, commands.Count); + Assert.AreEqual(2, commands.Count); Assert.AreEqual("Commands_ReopenIssue", commands[0].Command!.Name); Assert.IsFalse(commands[0].IsCritical); } @@ -75,7 +76,7 @@ public void GetPullRequestCommands_OpenPullRequest_ReturnsMergeAndCloseCommands( var (factory, _, _) = CreateFactory(); var commands = factory.GetPullRequestCommands(CreatePullRequest("Open").Object).ToList(); - Assert.AreEqual(2, commands.Count); + Assert.AreEqual(3, commands.Count); Assert.AreEqual("Commands_MergePullRequest", commands[0].Command!.Name); Assert.AreEqual("Commands_ClosePullRequest", commands[1].Command!.Name); Assert.IsTrue(commands[0].IsCritical); @@ -89,7 +90,7 @@ public void GetPullRequestCommands_ClosedPullRequest_ReturnsReopenCommand() var (factory, _, _) = CreateFactory(); var commands = factory.GetPullRequestCommands(CreatePullRequest("Closed").Object).ToList(); - Assert.AreEqual(1, commands.Count); + Assert.AreEqual(2, commands.Count); Assert.AreEqual("Commands_ReopenPullRequest", commands[0].Command!.Name); Assert.IsFalse(commands[0].IsCritical); } diff --git a/GitHubExtension.Test/Controls/SearchPagesTests.cs b/GitHubExtension.Test/Controls/SearchPagesTests.cs index 246eb94..0a764e8 100644 --- a/GitHubExtension.Test/Controls/SearchPagesTests.cs +++ b/GitHubExtension.Test/Controls/SearchPagesTests.cs @@ -20,8 +20,9 @@ public class SearchPagesTests private static (MutationCommandsFactory Factory, MutationMediator Mediator) CreateMutationDeps(Mock resources) { var mutationManager = new Mock(); + var createManager = new Mock(); var mediator = new MutationMediator(); - var factory = new MutationCommandsFactory(mutationManager.Object, mediator, resources.Object); + var factory = new MutationCommandsFactory(mutationManager.Object, createManager.Object, mediator, resources.Object); return (factory, mediator); } diff --git a/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs b/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs index 62d2754..9241375 100644 --- a/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs +++ b/GitHubExtension.Test/Controls/TopLevelSearchesTest.cs @@ -113,8 +113,9 @@ public async Task Integration_AddNewTopLevelCommand(string searchString, string var mockDeveloperIdProvider = TestHelpers.CreateMockDeveloperIdProvider(); var mockCacheDataManager = new Mock().Object; var mutationManager = new Mock().Object; + var createManager = new Mock().Object; var mutationMediator = new MutationMediator(); - var mutationCommandsFactory = new MutationCommandsFactory(mutationManager, mutationMediator, resources); + var mutationCommandsFactory = new MutationCommandsFactory(mutationManager, createManager, mutationMediator, resources); var searchPageFactory = new SearchPageFactory(mockCacheDataManager, persistentDataManager, resources, mediator, mutationCommandsFactory, mutationMediator); var addSearchForm = new SaveSearchForm(persistentDataManager, resources, mediator); diff --git a/GitHubExtension.Test/Helpers/TestSetupHelpers.cs b/GitHubExtension.Test/Helpers/TestSetupHelpers.cs index 464dcb0..9a4083d 100644 --- a/GitHubExtension.Test/Helpers/TestSetupHelpers.cs +++ b/GitHubExtension.Test/Helpers/TestSetupHelpers.cs @@ -175,6 +175,7 @@ public static GitHubExtensionCommandsProvider CreateGitHubExtensionCommandsProvi var mockNotificationsDataManager = new Mock(); mockNotificationsDataManager.Setup(x => x.GetUnreadCountAsync()).ReturnsAsync(0); var notificationsPage = new NotificationsPage(mockNotificationsDataManager.Object, notificationsMediator, mockResources); - return new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, notificationsPage, mockDeveloperIdProvider, persistentDataManager, mockResources, searchPageFactory, savedSearchesMediator, mockAuthenticationMediator, notificationsMediator); + var mockCreateManager = new Mock().Object; + return new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, notificationsPage, mockDeveloperIdProvider, persistentDataManager, mockResources, searchPageFactory, savedSearchesMediator, mockAuthenticationMediator, notificationsMediator, mockCreateManager); } } diff --git a/GitHubExtension/Controls/Commands/MutationCommandsFactory.cs b/GitHubExtension/Controls/Commands/MutationCommandsFactory.cs index 7f3813b..f16049d 100644 --- a/GitHubExtension/Controls/Commands/MutationCommandsFactory.cs +++ b/GitHubExtension/Controls/Commands/MutationCommandsFactory.cs @@ -2,6 +2,8 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using GitHubExtension.Controls.Forms; +using GitHubExtension.Controls.Pages; using GitHubExtension.DataManager; using GitHubExtension.Helpers; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -13,6 +15,7 @@ namespace GitHubExtension.Controls.Commands; public sealed class MutationCommandsFactory { private readonly IGitHubMutationManager _mutationManager; + private readonly IGitHubCreateManager _createManager; private readonly MutationMediator _mediator; private readonly IResources _resources; @@ -20,15 +23,29 @@ public sealed class MutationCommandsFactory private static readonly IconInfo ReopenIcon = new("\uE72C"); private static readonly IconInfo MergeIcon = new("\uE8FB"); - public MutationCommandsFactory(IGitHubMutationManager mutationManager, MutationMediator mediator, IResources resources) + public MutationCommandsFactory(IGitHubMutationManager mutationManager, IGitHubCreateManager createManager, MutationMediator mediator, IResources resources) { _mutationManager = mutationManager; + _createManager = createManager; _mediator = mediator; _resources = resources; } private static bool IsOpen(IIssue issue) => string.Equals(issue.State, "Open", StringComparison.OrdinalIgnoreCase); + private CommandContextItem BuildAddCommentCommand(IIssue issue) + { + var page = new GitHubFormPage( + new AddCommentForm(issue, _createManager, _resources), + _resources, + "Forms_AddComment_Title", + "\uE90A", + "Message_AddComment_Success", + "Message_AddComment_Error"); + + return new CommandContextItem(page); + } + public IEnumerable GetIssueCommands(IIssue issue) { var items = new List(); @@ -65,6 +82,8 @@ public IEnumerable GetIssueCommands(IIssue issue) items.Add(new CommandContextItem(reopen)); } + items.Add(BuildAddCommentCommand(issue)); + return items; } @@ -121,6 +140,8 @@ public IEnumerable GetPullRequestCommands(IPullRequest pullR items.Add(new CommandContextItem(reopen)); } + items.Add(BuildAddCommentCommand(pullRequest)); + return items; } } diff --git a/GitHubExtension/Controls/Forms/AddCommentForm.cs b/GitHubExtension/Controls/Forms/AddCommentForm.cs new file mode 100644 index 0000000..03bdd9c --- /dev/null +++ b/GitHubExtension/Controls/Forms/AddCommentForm.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Nodes; +using GitHubExtension.DataManager; +using GitHubExtension.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace GitHubExtension.Controls.Forms; + +public sealed partial class AddCommentForm : FormContent, IGitHubForm +{ + private readonly IGitHubCreateManager _createManager; + private readonly IResources _resources; + private readonly IIssue _issue; + + public event EventHandler? LoadingStateChanged; + + public event EventHandler? FormSubmitted; + + public AddCommentForm(IIssue issue, IGitHubCreateManager createManager, IResources resources) + { + _issue = issue; + _createManager = createManager; + _resources = resources; + } + + public Dictionary TemplateSubstitutions => new() + { + { "{{AddCommentFormTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_AddComment_Title")) }, + { "{{AddCommentSubtitle}}", JsonSerializer.Serialize(_issue.Title) }, + { "{{BodyLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_AddComment_BodyLabel")) }, + { "{{BodyPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_AddComment_BodyPlaceholder")) }, + { "{{BodyErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_AddComment_BodyError")) }, + { "{{AddCommentActionTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_AddComment_Action")) }, + }; + + public override string TemplateJson => TemplateHelper.LoadTemplateJsonFromTemplateName("AddComment", TemplateSubstitutions); + + public override ICommandResult SubmitForm(string? inputs, string data) + { + LoadingStateChanged?.Invoke(this, true); + _ = SubmitInternalAsync(inputs); + return CommandResult.KeepOpen(); + } + + private async Task SubmitInternalAsync(string? inputs) + { + try + { + var payload = JsonNode.Parse(inputs ?? string.Empty) ?? throw new InvalidOperationException("No input provided."); + var body = payload["Body"]?.ToString() ?? string.Empty; + + await _createManager.AddCommentAsync(_issue, body); + + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(true, null)); + } + catch (Exception ex) + { + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, ex)); + } + } +} diff --git a/GitHubExtension/Controls/Forms/CreateBranchForm.cs b/GitHubExtension/Controls/Forms/CreateBranchForm.cs new file mode 100644 index 0000000..d9589e6 --- /dev/null +++ b/GitHubExtension/Controls/Forms/CreateBranchForm.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Nodes; +using GitHubExtension.DataManager; +using GitHubExtension.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace GitHubExtension.Controls.Forms; + +public sealed partial class CreateBranchForm : FormContent, IGitHubForm +{ + private readonly IGitHubCreateManager _createManager; + private readonly IResources _resources; + private readonly string _initialRepository; + + public event EventHandler? LoadingStateChanged; + + public event EventHandler? FormSubmitted; + + public CreateBranchForm(IGitHubCreateManager createManager, IResources resources, string initialRepository = "") + { + _createManager = createManager; + _resources = resources; + _initialRepository = initialRepository; + } + + public Dictionary TemplateSubstitutions => new() + { + { "{{CreateBranchFormTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_Title")) }, + { "{{RepositoryLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryLabel")) }, + { "{{RepositoryPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryPlaceholder")) }, + { "{{RepositoryErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryError")) }, + { "{{RepositoryValue}}", JsonSerializer.Serialize(_initialRepository) }, + { "{{NewBranchLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_NewBranchLabel")) }, + { "{{NewBranchPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_NewBranchPlaceholder")) }, + { "{{NewBranchErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_NewBranchError")) }, + { "{{SourceBranchLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_SourceBranchLabel")) }, + { "{{SourceBranchPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_SourceBranchPlaceholder")) }, + { "{{SourceBranchErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_SourceBranchError")) }, + { "{{SourceBranchValue}}", JsonSerializer.Serialize("main") }, + { "{{CreateBranchActionTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateBranch_Action")) }, + }; + + public override string TemplateJson => TemplateHelper.LoadTemplateJsonFromTemplateName("CreateBranch", TemplateSubstitutions); + + public override ICommandResult SubmitForm(string? inputs, string data) + { + LoadingStateChanged?.Invoke(this, true); + _ = SubmitInternalAsync(inputs); + return CommandResult.KeepOpen(); + } + + private async Task SubmitInternalAsync(string? inputs) + { + try + { + var payload = JsonNode.Parse(inputs ?? string.Empty) ?? throw new InvalidOperationException("No input provided."); + var repository = payload["Repository"]?.ToString() ?? string.Empty; + var newBranch = payload["NewBranch"]?.ToString() ?? string.Empty; + var sourceBranch = payload["SourceBranch"]?.ToString() ?? string.Empty; + + await _createManager.CreateBranchAsync(repository, newBranch, sourceBranch); + + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(true, null)); + } + catch (Exception ex) + { + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, ex)); + } + } +} diff --git a/GitHubExtension/Controls/Forms/CreateIssueForm.cs b/GitHubExtension/Controls/Forms/CreateIssueForm.cs new file mode 100644 index 0000000..aa3606e --- /dev/null +++ b/GitHubExtension/Controls/Forms/CreateIssueForm.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Nodes; +using GitHubExtension.DataManager; +using GitHubExtension.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace GitHubExtension.Controls.Forms; + +public sealed partial class CreateIssueForm : FormContent, IGitHubForm +{ + private readonly IGitHubCreateManager _createManager; + private readonly IResources _resources; + private readonly string _initialRepository; + + public event EventHandler? LoadingStateChanged; + + public event EventHandler? FormSubmitted; + + public CreateIssueForm(IGitHubCreateManager createManager, IResources resources, string initialRepository = "") + { + _createManager = createManager; + _resources = resources; + _initialRepository = initialRepository; + } + + public Dictionary TemplateSubstitutions => new() + { + { "{{CreateIssueFormTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateIssue_Title")) }, + { "{{RepositoryLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryLabel")) }, + { "{{RepositoryPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryPlaceholder")) }, + { "{{RepositoryErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryError")) }, + { "{{RepositoryValue}}", JsonSerializer.Serialize(_initialRepository) }, + { "{{TitleLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_TitleLabel")) }, + { "{{TitlePlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_TitlePlaceholder")) }, + { "{{TitleErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_TitleError")) }, + { "{{BodyLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_BodyLabel")) }, + { "{{BodyPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_BodyPlaceholder")) }, + { "{{CreateIssueActionTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreateIssue_Action")) }, + }; + + public override string TemplateJson => TemplateHelper.LoadTemplateJsonFromTemplateName("CreateIssue", TemplateSubstitutions); + + public override ICommandResult SubmitForm(string? inputs, string data) + { + LoadingStateChanged?.Invoke(this, true); + _ = SubmitInternalAsync(inputs); + return CommandResult.KeepOpen(); + } + + private async Task SubmitInternalAsync(string? inputs) + { + try + { + var payload = JsonNode.Parse(inputs ?? string.Empty) ?? throw new InvalidOperationException("No input provided."); + var repository = payload["Repository"]?.ToString() ?? string.Empty; + var title = payload["Title"]?.ToString() ?? string.Empty; + var body = payload["Body"]?.ToString() ?? string.Empty; + + await _createManager.CreateIssueAsync(repository, title, body); + + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(true, null)); + } + catch (Exception ex) + { + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, ex)); + } + } +} diff --git a/GitHubExtension/Controls/Forms/CreatePullRequestForm.cs b/GitHubExtension/Controls/Forms/CreatePullRequestForm.cs new file mode 100644 index 0000000..468bf1a --- /dev/null +++ b/GitHubExtension/Controls/Forms/CreatePullRequestForm.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Nodes; +using GitHubExtension.DataManager; +using GitHubExtension.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace GitHubExtension.Controls.Forms; + +public sealed partial class CreatePullRequestForm : FormContent, IGitHubForm +{ + private readonly IGitHubCreateManager _createManager; + private readonly IResources _resources; + private readonly string _initialRepository; + + public event EventHandler? LoadingStateChanged; + + public event EventHandler? FormSubmitted; + + public CreatePullRequestForm(IGitHubCreateManager createManager, IResources resources, string initialRepository = "") + { + _createManager = createManager; + _resources = resources; + _initialRepository = initialRepository; + } + + public Dictionary TemplateSubstitutions => new() + { + { "{{CreatePullRequestFormTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_Title")) }, + { "{{RepositoryLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryLabel")) }, + { "{{RepositoryPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryPlaceholder")) }, + { "{{RepositoryErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_RepositoryError")) }, + { "{{RepositoryValue}}", JsonSerializer.Serialize(_initialRepository) }, + { "{{TitleLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_TitleLabel")) }, + { "{{TitlePlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_TitlePlaceholder")) }, + { "{{TitleErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_TitleError")) }, + { "{{HeadBranchLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_HeadLabel")) }, + { "{{HeadBranchPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_HeadPlaceholder")) }, + { "{{HeadBranchErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_HeadError")) }, + { "{{BaseBranchLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_BaseLabel")) }, + { "{{BaseBranchPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_BasePlaceholder")) }, + { "{{BaseBranchErrorMessage}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_BaseError")) }, + { "{{BaseBranchValue}}", JsonSerializer.Serialize("main") }, + { "{{BodyLabel}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_BodyLabel")) }, + { "{{BodyPlaceholder}}", JsonSerializer.Serialize(_resources.GetResource("Forms_Create_BodyPlaceholder")) }, + { "{{CreatePullRequestActionTitle}}", JsonSerializer.Serialize(_resources.GetResource("Forms_CreatePullRequest_Action")) }, + }; + + public override string TemplateJson => TemplateHelper.LoadTemplateJsonFromTemplateName("CreatePullRequest", TemplateSubstitutions); + + public override ICommandResult SubmitForm(string? inputs, string data) + { + LoadingStateChanged?.Invoke(this, true); + _ = SubmitInternalAsync(inputs); + return CommandResult.KeepOpen(); + } + + private async Task SubmitInternalAsync(string? inputs) + { + try + { + var payload = JsonNode.Parse(inputs ?? string.Empty) ?? throw new InvalidOperationException("No input provided."); + var repository = payload["Repository"]?.ToString() ?? string.Empty; + var title = payload["Title"]?.ToString() ?? string.Empty; + var headBranch = payload["HeadBranch"]?.ToString() ?? string.Empty; + var baseBranch = payload["BaseBranch"]?.ToString() ?? string.Empty; + var body = payload["Body"]?.ToString() ?? string.Empty; + + await _createManager.CreatePullRequestAsync(repository, title, headBranch, baseBranch, body); + + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(true, null)); + } + catch (Exception ex) + { + LoadingStateChanged?.Invoke(this, false); + FormSubmitted?.Invoke(this, new FormSubmitEventArgs(false, ex)); + } + } +} diff --git a/GitHubExtension/Controls/Pages/GitHubFormPage.cs b/GitHubExtension/Controls/Pages/GitHubFormPage.cs new file mode 100644 index 0000000..3d6a599 --- /dev/null +++ b/GitHubExtension/Controls/Pages/GitHubFormPage.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using GitHubExtension.Controls.Forms; +using GitHubExtension.Helpers; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace GitHubExtension.Controls.Pages; + +// Reusable ContentPage host for the create/comment forms. Wires the form's +// submit/loading events to a StatusMessage and shows a success or error toast. +public sealed partial class GitHubFormPage : ContentPage +{ + private readonly FormContent _form; + private readonly StatusMessage _statusMessage; + + public GitHubFormPage(FormContent form, IResources resources, string titleKey, string iconGlyph, string successKey, string errorKey) + { + _form = form; + _statusMessage = new StatusMessage(); + + Icon = new IconInfo(iconGlyph); + Title = resources.GetResource(titleKey); + Name = Title; + + FormEventHelper.WireFormEvents( + (IGitHubForm)form, + this, + _statusMessage, + resources.GetResource(successKey), + resources.GetResource(errorKey)); + + ExtensionHost.HideStatus(_statusMessage); + } + + public override IContent[] GetContent() + { + ExtensionHost.HideStatus(_statusMessage); + return [_form]; + } +} diff --git a/GitHubExtension/Controls/Templates/AddCommentTemplate.json b/GitHubExtension/Controls/Templates/AddCommentTemplate.json new file mode 100644 index 0000000..7b9b833 --- /dev/null +++ b/GitHubExtension/Controls/Templates/AddCommentTemplate.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": {{AddCommentFormTitle}}, + "horizontalAlignment": "Center", + "wrap": true, + "style": "heading" + }, + { + "type": "TextBlock", + "text": {{AddCommentSubtitle}}, + "wrap": true, + "isSubtle": true + }, + { + "type": "Input.Text", + "id": "Body", + "label": {{BodyLabel}}, + "placeholder": {{BodyPlaceholder}}, + "isMultiline": true, + "isRequired": true, + "errorMessage": {{BodyErrorMessage}} + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": {{AddCommentActionTitle}}, + "data": { + "id": "AddCommentAction" + } + } + ] +} diff --git a/GitHubExtension/Controls/Templates/CreateBranchTemplate.json b/GitHubExtension/Controls/Templates/CreateBranchTemplate.json new file mode 100644 index 0000000..f7a365a --- /dev/null +++ b/GitHubExtension/Controls/Templates/CreateBranchTemplate.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": {{CreateBranchFormTitle}}, + "horizontalAlignment": "Center", + "wrap": true, + "style": "heading" + }, + { + "type": "Input.Text", + "id": "Repository", + "label": {{RepositoryLabel}}, + "placeholder": {{RepositoryPlaceholder}}, + "isRequired": true, + "errorMessage": {{RepositoryErrorMessage}}, + "value": {{RepositoryValue}} + }, + { + "type": "Input.Text", + "id": "NewBranch", + "label": {{NewBranchLabel}}, + "placeholder": {{NewBranchPlaceholder}}, + "isRequired": true, + "errorMessage": {{NewBranchErrorMessage}} + }, + { + "type": "Input.Text", + "id": "SourceBranch", + "label": {{SourceBranchLabel}}, + "placeholder": {{SourceBranchPlaceholder}}, + "isRequired": true, + "errorMessage": {{SourceBranchErrorMessage}}, + "value": {{SourceBranchValue}} + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": {{CreateBranchActionTitle}}, + "data": { + "id": "CreateBranchAction" + } + } + ] +} diff --git a/GitHubExtension/Controls/Templates/CreateIssueTemplate.json b/GitHubExtension/Controls/Templates/CreateIssueTemplate.json new file mode 100644 index 0000000..3b46ee6 --- /dev/null +++ b/GitHubExtension/Controls/Templates/CreateIssueTemplate.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": {{CreateIssueFormTitle}}, + "horizontalAlignment": "Center", + "wrap": true, + "style": "heading" + }, + { + "type": "Input.Text", + "id": "Repository", + "label": {{RepositoryLabel}}, + "placeholder": {{RepositoryPlaceholder}}, + "isRequired": true, + "errorMessage": {{RepositoryErrorMessage}}, + "value": {{RepositoryValue}} + }, + { + "type": "Input.Text", + "id": "Title", + "label": {{TitleLabel}}, + "placeholder": {{TitlePlaceholder}}, + "isRequired": true, + "errorMessage": {{TitleErrorMessage}} + }, + { + "type": "Input.Text", + "id": "Body", + "label": {{BodyLabel}}, + "placeholder": {{BodyPlaceholder}}, + "isMultiline": true, + "isRequired": false + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": {{CreateIssueActionTitle}}, + "data": { + "id": "CreateIssueAction" + } + } + ] +} diff --git a/GitHubExtension/Controls/Templates/CreatePullRequestTemplate.json b/GitHubExtension/Controls/Templates/CreatePullRequestTemplate.json new file mode 100644 index 0000000..53bc26d --- /dev/null +++ b/GitHubExtension/Controls/Templates/CreatePullRequestTemplate.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.5", + "body": [ + { + "type": "TextBlock", + "size": "Medium", + "weight": "Bolder", + "text": {{CreatePullRequestFormTitle}}, + "horizontalAlignment": "Center", + "wrap": true, + "style": "heading" + }, + { + "type": "Input.Text", + "id": "Repository", + "label": {{RepositoryLabel}}, + "placeholder": {{RepositoryPlaceholder}}, + "isRequired": true, + "errorMessage": {{RepositoryErrorMessage}}, + "value": {{RepositoryValue}} + }, + { + "type": "Input.Text", + "id": "Title", + "label": {{TitleLabel}}, + "placeholder": {{TitlePlaceholder}}, + "isRequired": true, + "errorMessage": {{TitleErrorMessage}} + }, + { + "type": "Input.Text", + "id": "HeadBranch", + "label": {{HeadBranchLabel}}, + "placeholder": {{HeadBranchPlaceholder}}, + "isRequired": true, + "errorMessage": {{HeadBranchErrorMessage}} + }, + { + "type": "Input.Text", + "id": "BaseBranch", + "label": {{BaseBranchLabel}}, + "placeholder": {{BaseBranchPlaceholder}}, + "isRequired": true, + "errorMessage": {{BaseBranchErrorMessage}}, + "value": {{BaseBranchValue}} + }, + { + "type": "Input.Text", + "id": "Body", + "label": {{BodyLabel}}, + "placeholder": {{BodyPlaceholder}}, + "isMultiline": true, + "isRequired": false + } + ], + "actions": [ + { + "type": "Action.Submit", + "title": {{CreatePullRequestActionTitle}}, + "data": { + "id": "CreatePullRequestAction" + } + } + ] +} diff --git a/GitHubExtension/DataManager/Data/GitHubCreateManager.cs b/GitHubExtension/DataManager/Data/GitHubCreateManager.cs new file mode 100644 index 0000000..7360344 --- /dev/null +++ b/GitHubExtension/DataManager/Data/GitHubCreateManager.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using GitHubExtension.Client; +using GitHubExtension.Controls; +using Octokit; +using Serilog; + +namespace GitHubExtension.DataManager.Data; + +public sealed class GitHubCreateManager : IGitHubCreateManager +{ + private static readonly Lazy _logger = new(() => Serilog.Log.ForContext("SourceContext", nameof(GitHubCreateManager))); + + private static readonly ILogger _log = _logger.Value; + + private readonly GitHubClientProvider _gitHubClientProvider; + + public GitHubCreateManager(GitHubClientProvider gitHubClientProvider) + { + _gitHubClientProvider = gitHubClientProvider; + } + + public async Task CreateIssueAsync(string ownerRepo, string title, string body) + { + var (owner, repo) = ParseOwnerRepo(ownerRepo); + var client = await _gitHubClientProvider.GetClientForLoggedInDeveloper(false); + var newIssue = new NewIssue(title) { Body = body }; + var created = await client.Issue.Create(owner, repo, newIssue); + _log.Information($"Created issue {owner}/{repo}#{created.Number}."); + return created.HtmlUrl; + } + + public async Task CreatePullRequestAsync(string ownerRepo, string title, string headBranch, string baseBranch, string body) + { + var (owner, repo) = ParseOwnerRepo(ownerRepo); + var client = await _gitHubClientProvider.GetClientForLoggedInDeveloper(false); + var newPullRequest = new NewPullRequest(title, headBranch, baseBranch) { Body = body }; + var created = await client.PullRequest.Create(owner, repo, newPullRequest); + _log.Information($"Created pull request {owner}/{repo}#{created.Number}."); + return created.HtmlUrl; + } + + public async Task CreateBranchAsync(string ownerRepo, string newBranch, string sourceBranch) + { + var (owner, repo) = ParseOwnerRepo(ownerRepo); + var client = await _gitHubClientProvider.GetClientForLoggedInDeveloper(false); + + var sourceReference = await client.Git.Reference.Get(owner, repo, $"heads/{sourceBranch}"); + var newReference = new NewReference($"refs/heads/{newBranch}", sourceReference.Object.Sha); + var created = await client.Git.Reference.Create(owner, repo, newReference); + _log.Information($"Created branch {newBranch} in {owner}/{repo} from {sourceBranch}."); + return created.Ref; + } + + public async Task AddCommentAsync(IIssue issue, string comment) + { + var owner = Validation.ParseOwnerFromGitHubURL(issue.HtmlUrl); + var repo = Validation.ParseRepositoryFromGitHubURL(issue.HtmlUrl); + var client = await _gitHubClientProvider.GetClientForLoggedInDeveloper(false); + await client.Issue.Comment.Create(owner, repo, (int)issue.Number, comment); + _log.Information($"Added comment to {owner}/{repo}#{issue.Number}."); + } + + internal static (string Owner, string Repo) ParseOwnerRepo(string ownerRepo) + { + if (string.IsNullOrWhiteSpace(ownerRepo)) + { + throw new ArgumentException("Repository must be provided as owner/repo.", nameof(ownerRepo)); + } + + var trimmed = ownerRepo.Trim(); + + // Accept a full GitHub URL as well as the short owner/repo form. + if (Validation.IsValidHttpUri(trimmed, out var uri) && uri != null) + { + return (Validation.ParseOwnerFromGitHubURL(uri), Validation.ParseRepositoryFromGitHubURL(uri)); + } + + var parts = trimmed.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length != 2) + { + throw new ArgumentException("Repository must be in the form owner/repo.", nameof(ownerRepo)); + } + + return (parts[0], parts[1]); + } +} diff --git a/GitHubExtension/DataManager/IGitHubCreateManager.cs b/GitHubExtension/DataManager/IGitHubCreateManager.cs new file mode 100644 index 0000000..e18be96 --- /dev/null +++ b/GitHubExtension/DataManager/IGitHubCreateManager.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using GitHubExtension.Controls; + +namespace GitHubExtension.DataManager; + +// Performs create/write operations that produce new GitHub resources (issues, +// pull requests, branches) plus comments on existing items. Kept separate from +// the read-only cache path and from the state-mutation manager. Introduced in +// M4 (create flows) and reused by later milestones. +public interface IGitHubCreateManager +{ + Task CreateIssueAsync(string ownerRepo, string title, string body); + + Task CreatePullRequestAsync(string ownerRepo, string title, string headBranch, string baseBranch, string body); + + Task CreateBranchAsync(string ownerRepo, string newBranch, string sourceBranch); + + Task AddCommentAsync(IIssue issue, string comment); +} diff --git a/GitHubExtension/GitHubExtensionCommandsProvider.cs b/GitHubExtension/GitHubExtensionCommandsProvider.cs index 8b0deb8..0d64c7e 100644 --- a/GitHubExtension/GitHubExtensionCommandsProvider.cs +++ b/GitHubExtension/GitHubExtensionCommandsProvider.cs @@ -4,7 +4,9 @@ using System.Diagnostics; using GitHubExtension.Controls; +using GitHubExtension.Controls.Forms; using GitHubExtension.Controls.Pages; +using GitHubExtension.DataManager; using GitHubExtension.DeveloperIds; using GitHubExtension.Helpers; using Microsoft.CommandPalette.Extensions; @@ -25,6 +27,7 @@ public partial class GitHubExtensionCommandsProvider : CommandProvider, IDisposa private readonly SavedSearchesMediator _savedSearchesMediator; private readonly AuthenticationMediator _authenticationMediator; private readonly NotificationsMediator _notificationsMediator; + private readonly IGitHubCreateManager _createManager; public GitHubExtensionCommandsProvider( SavedSearchesPage savedSearchesPage, @@ -37,7 +40,8 @@ public GitHubExtensionCommandsProvider( ISearchPageFactory searchPageFactory, SavedSearchesMediator savedSearchesMediator, AuthenticationMediator authenticationMediator, - NotificationsMediator notificationsMediator) + NotificationsMediator notificationsMediator, + IGitHubCreateManager createManager) { _savedSearchesPage = savedSearchesPage; _signOutPage = signOutPage; @@ -50,6 +54,7 @@ public GitHubExtensionCommandsProvider( _savedSearchesMediator = savedSearchesMediator; _authenticationMediator = authenticationMediator; _notificationsMediator = notificationsMediator; + _createManager = createManager; DisplayName = _resources.GetResource("ExtensionTitle"); @@ -106,6 +111,9 @@ public override ICommandItem[] TopLevelCommands() var defaultCommands = new List { BuildNotificationsCommandItem(), + BuildCreateIssueCommandItem(), + BuildCreatePullRequestCommandItem(), + BuildCreateBranchCommandItem(), new(_savedSearchesPage), new(_signOutPage), }; @@ -114,6 +122,57 @@ public override ICommandItem[] TopLevelCommands() return commands.ToArray(); } + private CommandItem BuildCreateIssueCommandItem() + { + var page = new GitHubFormPage( + new CreateIssueForm(_createManager, _resources), + _resources, + "Forms_CreateIssue_Title", + "\uE710", + "Message_CreateIssue_Success", + "Message_CreateIssue_Error"); + + return new CommandItem(page) + { + Title = _resources.GetResource("CommandsProvider_CreateIssueCommandName"), + Subtitle = _resources.GetResource("CommandsProvider_CreateIssueSubtitle"), + }; + } + + private CommandItem BuildCreatePullRequestCommandItem() + { + var page = new GitHubFormPage( + new CreatePullRequestForm(_createManager, _resources), + _resources, + "Forms_CreatePullRequest_Title", + "\uE710", + "Message_CreatePullRequest_Success", + "Message_CreatePullRequest_Error"); + + return new CommandItem(page) + { + Title = _resources.GetResource("CommandsProvider_CreatePullRequestCommandName"), + Subtitle = _resources.GetResource("CommandsProvider_CreatePullRequestSubtitle"), + }; + } + + private CommandItem BuildCreateBranchCommandItem() + { + var page = new GitHubFormPage( + new CreateBranchForm(_createManager, _resources), + _resources, + "Forms_CreateBranch_Title", + "\uE710", + "Message_CreateBranch_Success", + "Message_CreateBranch_Error"); + + return new CommandItem(page) + { + Title = _resources.GetResource("CommandsProvider_CreateBranchCommandName"), + Subtitle = _resources.GetResource("CommandsProvider_CreateBranchSubtitle"), + }; + } + private CommandItem BuildNotificationsCommandItem() { var subtitle = _notificationsPage.UnreadCount > 0 diff --git a/GitHubExtension/Helpers/TemplateHelper.cs b/GitHubExtension/Helpers/TemplateHelper.cs index 9d412eb..59011e3 100644 --- a/GitHubExtension/Helpers/TemplateHelper.cs +++ b/GitHubExtension/Helpers/TemplateHelper.cs @@ -14,6 +14,10 @@ public static string GetTemplatePath(string page) { "AuthTemplate" => "Controls\\Templates\\AuthTemplate.json", "SaveSearch" => "Controls\\Templates\\SaveSearchTemplate.json", + "CreateIssue" => "Controls\\Templates\\CreateIssueTemplate.json", + "CreatePullRequest" => "Controls\\Templates\\CreatePullRequestTemplate.json", + "CreateBranch" => "Controls\\Templates\\CreateBranchTemplate.json", + "AddComment" => "Controls\\Templates\\AddCommentTemplate.json", _ => throw new NotImplementedException($"Template for page '{page}' is not implemented."), }; } diff --git a/GitHubExtension/Program.cs b/GitHubExtension/Program.cs index 75d8d52..1da3c10 100644 --- a/GitHubExtension/Program.cs +++ b/GitHubExtension/Program.cs @@ -138,7 +138,8 @@ private static async Task HandleCOMServerActivationAsync() var notificationsPage = new NotificationsPage(notificationsDataManager, notificationsMediator, resources); var mutationManager = new GitHubMutationManager(gitHubClientProvider); - var mutationCommandsFactory = new MutationCommandsFactory(mutationManager, mutationMediator, resources); + var createManager = new GitHubCreateManager(gitHubClientProvider); + var mutationCommandsFactory = new MutationCommandsFactory(mutationManager, createManager, mutationMediator, resources); var searchPageFactory = new SearchPageFactory(cacheDataManager, searchRepository, resources, savedSearchesMediator, mutationCommandsFactory, mutationMediator); @@ -154,7 +155,7 @@ private static async Task HandleCOMServerActivationAsync() using var signInForm = new SignInForm(authenticationMediator, resources, developerIdProvider, signInCommand); using var signInPage = new SignInPage(signInForm, resources, signInCommand, authenticationMediator); - using var commandProvider = new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, notificationsPage, developerIdProvider, searchRepository, resources, searchPageFactory, savedSearchesMediator, authenticationMediator, notificationsMediator); + using var commandProvider = new GitHubExtensionCommandsProvider(savedSearchesPage, signOutPage, signInPage, notificationsPage, developerIdProvider, searchRepository, resources, searchPageFactory, savedSearchesMediator, authenticationMediator, notificationsMediator, createManager); var extensionInstance = new GitHubExtension(extensionDisposedEvent, commandProvider); // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. diff --git a/GitHubExtension/Strings/en-US/Resources.resw b/GitHubExtension/Strings/en-US/Resources.resw index 4863dde..dd505f9 100644 --- a/GitHubExtension/Strings/en-US/Resources.resw +++ b/GitHubExtension/Strings/en-US/Resources.resw @@ -445,6 +445,186 @@ This will merge the pull request into its base branch. Confirmation description before merging a pull request + + Create Issue + Top-level command name for creating an issue + + + Open a new issue on GitHub + Subtitle for the Create Issue top-level command + + + Create Pull Request + Top-level command name for creating a pull request + + + Open a new pull request on GitHub + Subtitle for the Create Pull Request top-level command + + + Create Branch + Top-level command name for creating a branch + + + Create a new branch on GitHub + Subtitle for the Create Branch top-level command + + + Repository + Label for the repository input on create forms + + + owner/repo + Placeholder for the repository input on create forms + + + Enter a repository as owner/repo + Validation error for the repository input on create forms + + + Title + Label for the title input on create forms + + + Enter a title + Placeholder for the title input on create forms + + + A title is required + Validation error for the title input on create forms + + + Description + Label for the body input on create forms + + + Enter a description (optional) + Placeholder for the body input on create forms + + + Create Issue + Title for the create issue form + + + Create issue + Submit action title for the create issue form + + + Create Pull Request + Title for the create pull request form + + + Create pull request + Submit action title for the create pull request form + + + Head branch + Label for the head branch input on the create pull request form + + + feature-branch + Placeholder for the head branch input on the create pull request form + + + A head branch is required + Validation error for the head branch input on the create pull request form + + + Base branch + Label for the base branch input on the create pull request form + + + main + Placeholder for the base branch input on the create pull request form + + + A base branch is required + Validation error for the base branch input on the create pull request form + + + Create Branch + Title for the create branch form + + + Create branch + Submit action title for the create branch form + + + New branch name + Label for the new branch input on the create branch form + + + feature-branch + Placeholder for the new branch input on the create branch form + + + A branch name is required + Validation error for the new branch input on the create branch form + + + Source branch + Label for the source branch input on the create branch form + + + main + Placeholder for the source branch input on the create branch form + + + A source branch is required + Validation error for the source branch input on the create branch form + + + Add Comment + Title for the add comment form + + + Add comment + Submit action title for the add comment form + + + Comment + Label for the comment input on the add comment form + + + Write a comment + Placeholder for the comment input on the add comment form + + + A comment is required + Validation error for the comment input on the add comment form + + + Issue created + Toast shown after an issue is created + + + Failed to create issue + Toast shown when creating an issue fails + + + Pull request created + Toast shown after a pull request is created + + + Failed to create pull request + Toast shown when creating a pull request fails + + + Branch created + Toast shown after a branch is created + + + Failed to create branch + Toast shown when creating a branch fails + + + Comment added + Toast shown after a comment is added + + + Failed to add comment + Toast shown when adding a comment fails + GitHub Extension (Preview) Title for the extension