зеркало из https://github.com/github/VisualStudio.git
Merge pull request #1787 from github/refactor/clone
Refactored Clone Dialog
This commit is contained in:
Коммит
0e64838b84
|
@ -33,9 +33,14 @@ namespace GitHub.Api
|
|||
public static string ClientSecret { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scopes required by the application.
|
||||
/// Gets the minimum scopes required by the application.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> RequiredScopes { get; } = new[] { "user", "repo", "gist", "write:public_key" };
|
||||
public static IReadOnlyList<string> MinimumScopes { get; } = new[] { "user", "repo", "gist", "write:public_key" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ideal scopes requested by the application.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> RequestedScopes { get; } = new[] { "user", "repo", "gist", "write:public_key", "read:org" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets a note that will be stored with the OAUTH token.
|
||||
|
|
|
@ -21,11 +21,11 @@ namespace GitHub.Api
|
|||
/// <param name="client">An octokit client configured to access the server.</param>
|
||||
/// <param name="userName">The username.</param>
|
||||
/// <param name="password">The password.</param>
|
||||
/// <returns>The logged in user.</returns>
|
||||
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
|
||||
/// <exception cref="AuthorizationException">
|
||||
/// The login authorization failed.
|
||||
/// </exception>
|
||||
Task<User> Login(HostAddress hostAddress, IGitHubClient client, string userName, string password);
|
||||
Task<LoginResult> Login(HostAddress hostAddress, IGitHubClient client, string userName, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to log into a GitHub server via OAuth in the browser.
|
||||
|
@ -35,11 +35,11 @@ namespace GitHub.Api
|
|||
/// <param name="oauthClient">An octokit OAuth client configured to access the server.</param>
|
||||
/// <param name="openBrowser">A callback that should open a browser at the requested URL.</param>
|
||||
/// <param name="cancel">A cancellation token used to cancel the operation.</param>
|
||||
/// <returns>The logged in user.</returns>
|
||||
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
|
||||
/// <exception cref="AuthorizationException">
|
||||
/// The login authorization failed.
|
||||
/// </exception>
|
||||
Task<User> LoginViaOAuth(
|
||||
Task<LoginResult> LoginViaOAuth(
|
||||
HostAddress hostAddress,
|
||||
IGitHubClient client,
|
||||
IOauthClient oauthClient,
|
||||
|
@ -52,7 +52,8 @@ namespace GitHub.Api
|
|||
/// <param name="hostAddress">The address of the server.</param>
|
||||
/// <param name="client">An octokit client configured to access the server.</param>
|
||||
/// <param name="token">The token.</param>
|
||||
Task<User> LoginWithToken(
|
||||
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
|
||||
Task<LoginResult> LoginWithToken(
|
||||
HostAddress hostAddress,
|
||||
IGitHubClient client,
|
||||
string token);
|
||||
|
@ -62,11 +63,11 @@ namespace GitHub.Api
|
|||
/// </summary>
|
||||
/// <param name="hostAddress">The address of the server.</param>
|
||||
/// <param name="client">An octokit client configured to access the server.</param>
|
||||
/// <returns>The logged in user.</returns>
|
||||
/// <returns>A <see cref="LoginResult"/> with the details of the successful login.</returns>
|
||||
/// <exception cref="AuthorizationException">
|
||||
/// The login authorization failed.
|
||||
/// </exception>
|
||||
Task<User> LoginFromCache(HostAddress hostAddress, IGitHubClient client);
|
||||
Task<LoginResult> LoginFromCache(HostAddress hostAddress, IGitHubClient client);
|
||||
|
||||
/// <summary>
|
||||
/// Logs out of GitHub server.
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Logging;
|
||||
using GitHub.Models;
|
||||
using GitHub.Primitives;
|
||||
using Octokit;
|
||||
using Serilog;
|
||||
|
@ -24,7 +25,8 @@ namespace GitHub.Api
|
|||
readonly Lazy<ITwoFactorChallengeHandler> twoFactorChallengeHandler;
|
||||
readonly string clientId;
|
||||
readonly string clientSecret;
|
||||
readonly IReadOnlyList<string> scopes;
|
||||
readonly IReadOnlyList<string> minimumScopes;
|
||||
readonly IReadOnlyList<string> requestedScopes;
|
||||
readonly string authorizationNote;
|
||||
readonly string fingerprint;
|
||||
IOAuthCallbackListener oauthListener;
|
||||
|
@ -37,7 +39,8 @@ namespace GitHub.Api
|
|||
/// <param name="oauthListener">The callback listener to signal successful login.</param>
|
||||
/// <param name="clientId">The application's client API ID.</param>
|
||||
/// <param name="clientSecret">The application's client API secret.</param>
|
||||
/// <param name="scopes">List of scopes to authenticate for</param>
|
||||
/// <param name="minimumScopes">The minimum acceptable scopes.</param>
|
||||
/// <param name="requestedScopes">The scopes to request when logging in.</param>
|
||||
/// <param name="authorizationNote">An note to store with the authorization.</param>
|
||||
/// <param name="fingerprint">The machine fingerprint.</param>
|
||||
public LoginManager(
|
||||
|
@ -46,7 +49,8 @@ namespace GitHub.Api
|
|||
IOAuthCallbackListener oauthListener,
|
||||
string clientId,
|
||||
string clientSecret,
|
||||
IReadOnlyList<string> scopes,
|
||||
IReadOnlyList<string> minimumScopes,
|
||||
IReadOnlyList<string> requestedScopes,
|
||||
string authorizationNote = null,
|
||||
string fingerprint = null)
|
||||
{
|
||||
|
@ -60,13 +64,14 @@ namespace GitHub.Api
|
|||
this.oauthListener = oauthListener;
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.scopes = scopes;
|
||||
this.minimumScopes = minimumScopes;
|
||||
this.requestedScopes = requestedScopes;
|
||||
this.authorizationNote = authorizationNote;
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<User> Login(
|
||||
public async Task<LoginResult> Login(
|
||||
HostAddress hostAddress,
|
||||
IGitHubClient client,
|
||||
string userName,
|
||||
|
@ -83,7 +88,7 @@ namespace GitHub.Api
|
|||
|
||||
var newAuth = new NewAuthorization
|
||||
{
|
||||
Scopes = scopes,
|
||||
Scopes = requestedScopes,
|
||||
Note = authorizationNote,
|
||||
Fingerprint = fingerprint,
|
||||
};
|
||||
|
@ -121,11 +126,11 @@ namespace GitHub.Api
|
|||
} while (auth == null);
|
||||
|
||||
await keychain.Save(userName, auth.Token, hostAddress).ConfigureAwait(false);
|
||||
return await ReadUserWithRetry(client);
|
||||
return await ReadUserWithRetry(client).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<User> LoginViaOAuth(
|
||||
public async Task<LoginResult> LoginViaOAuth(
|
||||
HostAddress hostAddress,
|
||||
IGitHubClient client,
|
||||
IOauthClient oauthClient,
|
||||
|
@ -143,18 +148,18 @@ namespace GitHub.Api
|
|||
|
||||
openBrowser(loginUrl);
|
||||
|
||||
var code = await listen;
|
||||
var code = await listen.ConfigureAwait(false);
|
||||
var request = new OauthTokenRequest(clientId, clientSecret, code);
|
||||
var token = await oauthClient.CreateAccessToken(request);
|
||||
var token = await oauthClient.CreateAccessToken(request).ConfigureAwait(false);
|
||||
|
||||
await keychain.Save("[oauth]", token.AccessToken, hostAddress).ConfigureAwait(false);
|
||||
var user = await ReadUserWithRetry(client);
|
||||
await keychain.Save(user.Login, token.AccessToken, hostAddress).ConfigureAwait(false);
|
||||
return user;
|
||||
var result = await ReadUserWithRetry(client).ConfigureAwait(false);
|
||||
await keychain.Save(result.User.Login, token.AccessToken, hostAddress).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<User> LoginWithToken(
|
||||
public async Task<LoginResult> LoginWithToken(
|
||||
HostAddress hostAddress,
|
||||
IGitHubClient client,
|
||||
string token)
|
||||
|
@ -167,19 +172,19 @@ namespace GitHub.Api
|
|||
|
||||
try
|
||||
{
|
||||
var user = await ReadUserWithRetry(client);
|
||||
await keychain.Save(user.Login, token, hostAddress).ConfigureAwait(false);
|
||||
return user;
|
||||
var result = await ReadUserWithRetry(client).ConfigureAwait(false);
|
||||
await keychain.Save(result.User.Login, token, hostAddress).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await keychain.Delete(hostAddress);
|
||||
await keychain.Delete(hostAddress).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<User> LoginFromCache(HostAddress hostAddress, IGitHubClient client)
|
||||
public Task<LoginResult> LoginFromCache(HostAddress hostAddress, IGitHubClient client)
|
||||
{
|
||||
Guard.ArgumentNotNull(hostAddress, nameof(hostAddress));
|
||||
Guard.ArgumentNotNull(client, nameof(client));
|
||||
|
@ -193,41 +198,7 @@ namespace GitHub.Api
|
|||
Guard.ArgumentNotNull(hostAddress, nameof(hostAddress));
|
||||
Guard.ArgumentNotNull(client, nameof(client));
|
||||
|
||||
await keychain.Delete(hostAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests if received API scopes match the required API scopes.
|
||||
/// </summary>
|
||||
/// <param name="required">The required API scopes.</param>
|
||||
/// <param name="received">The received API scopes.</param>
|
||||
/// <returns>True if all required scopes are present, otherwise false.</returns>
|
||||
public static bool ScopesMatch(IReadOnlyList<string> required, IReadOnlyList<string> received)
|
||||
{
|
||||
foreach (var scope in required)
|
||||
{
|
||||
var found = received.Contains(scope);
|
||||
|
||||
if (!found &&
|
||||
(scope.StartsWith("read:", StringComparison.Ordinal) ||
|
||||
scope.StartsWith("write:", StringComparison.Ordinal)))
|
||||
{
|
||||
// NOTE: Scopes are actually more complex than this, for example
|
||||
// `user` encompasses `read:user` and `user:email` but just use
|
||||
// this simple rule for now as it works for the scopes we require.
|
||||
var adminScope = scope
|
||||
.Replace("read:", "admin:")
|
||||
.Replace("write:", "admin:");
|
||||
found = received.Contains(adminScope);
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
await keychain.Delete(hostAddress).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
async Task<ApplicationAuthorization> CreateAndDeleteExistingApplicationAuthorization(
|
||||
|
@ -256,18 +227,18 @@ namespace GitHub.Api
|
|||
twoFactorAuthenticationCode).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (result.Token == string.Empty)
|
||||
if (string.IsNullOrEmpty(result.Token))
|
||||
{
|
||||
if (twoFactorAuthenticationCode == null)
|
||||
{
|
||||
await client.Authorization.Delete(result.Id);
|
||||
await client.Authorization.Delete(result.Id).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.Authorization.Delete(result.Id, twoFactorAuthenticationCode);
|
||||
await client.Authorization.Delete(result.Id, twoFactorAuthenticationCode).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
} while (result.Token == string.Empty && retry++ == 0);
|
||||
} while (string.IsNullOrEmpty(result.Token) && retry++ == 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -280,7 +251,7 @@ namespace GitHub.Api
|
|||
{
|
||||
for (;;)
|
||||
{
|
||||
var challengeResult = await twoFactorChallengeHandler.Value.HandleTwoFactorException(exception);
|
||||
var challengeResult = await twoFactorChallengeHandler.Value.HandleTwoFactorException(exception).ConfigureAwait(false);
|
||||
|
||||
if (challengeResult == null)
|
||||
{
|
||||
|
@ -304,7 +275,7 @@ namespace GitHub.Api
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await twoFactorChallengeHandler.Value.ChallengeFailed(e);
|
||||
await twoFactorChallengeHandler.Value.ChallengeFailed(e).ConfigureAwait(false);
|
||||
await keychain.Delete(hostAddress).ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
@ -345,7 +316,7 @@ namespace GitHub.Api
|
|||
apiException?.StatusCode == (HttpStatusCode)422);
|
||||
}
|
||||
|
||||
async Task<User> ReadUserWithRetry(IGitHubClient client)
|
||||
async Task<LoginResult> ReadUserWithRetry(IGitHubClient client)
|
||||
{
|
||||
var retry = 0;
|
||||
|
||||
|
@ -362,29 +333,29 @@ namespace GitHub.Api
|
|||
|
||||
// It seems that attempting to use a token immediately sometimes fails, retry a few
|
||||
// times with a delay of of 1s to allow the token to propagate.
|
||||
await Task.Delay(1000);
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
async Task<User> GetUserAndCheckScopes(IGitHubClient client)
|
||||
async Task<LoginResult> GetUserAndCheckScopes(IGitHubClient client)
|
||||
{
|
||||
var response = await client.Connection.Get<User>(
|
||||
UserEndpoint, null, null).ConfigureAwait(false);
|
||||
|
||||
if (response.HttpResponse.Headers.ContainsKey(ScopesHeader))
|
||||
{
|
||||
var returnedScopes = response.HttpResponse.Headers[ScopesHeader]
|
||||
var returnedScopes = new ScopesCollection(response.HttpResponse.Headers[ScopesHeader]
|
||||
.Split(',')
|
||||
.Select(x => x.Trim())
|
||||
.ToArray();
|
||||
.ToArray());
|
||||
|
||||
if (ScopesMatch(scopes, returnedScopes))
|
||||
if (returnedScopes.Matches(minimumScopes))
|
||||
{
|
||||
return response.Body;
|
||||
return new LoginResult(response.Body, returnedScopes);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Error("Incorrect API scopes: require {RequiredScopes} but got {Scopes}", scopes, returnedScopes);
|
||||
log.Error("Incorrect API scopes: require {RequiredScopes} but got {Scopes}", minimumScopes, returnedScopes);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -393,7 +364,7 @@ namespace GitHub.Api
|
|||
}
|
||||
|
||||
throw new IncorrectScopesException(
|
||||
"Incorrect API scopes. Required: " + string.Join(",", scopes));
|
||||
"Incorrect API scopes. Required: " + string.Join(",", minimumScopes));
|
||||
}
|
||||
|
||||
Uri GetLoginUrl(IOauthClient client, string state)
|
||||
|
@ -402,7 +373,7 @@ namespace GitHub.Api
|
|||
|
||||
request.State = state;
|
||||
|
||||
foreach (var scope in scopes)
|
||||
foreach (var scope in requestedScopes)
|
||||
{
|
||||
request.Scopes.Add(scope);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GitHub.Models;
|
||||
using Octokit;
|
||||
|
||||
namespace GitHub.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the result of a successful login by <see cref="ILoginManager"/>.
|
||||
/// </summary>
|
||||
public class LoginResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="user">The logged-in user.</param>
|
||||
/// <param name="scopes">The login scopes.</param>
|
||||
public LoginResult(User user, ScopesCollection scopes)
|
||||
{
|
||||
User = user;
|
||||
Scopes = scopes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the login scopes.
|
||||
/// </summary>
|
||||
public ScopesCollection Scopes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logged-in user.
|
||||
/// </summary>
|
||||
public User User { get; }
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
using System.Windows.Markup;
|
||||
|
||||
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData")]
|
||||
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.SampleData.Dialog.Clone")]
|
||||
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels")]
|
||||
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog")]
|
||||
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.Dialog.Clone")]
|
||||
[assembly: XmlnsDefinition("https://github.com/github/VisualStudio", "GitHub.ViewModels.GitHubPane")]
|
||||
|
|
|
@ -115,7 +115,7 @@ namespace GitHub.App {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Clone a {0} Repository.
|
||||
/// Looks up a localized string similar to Clone a Repository.
|
||||
/// </summary>
|
||||
public static string CloneTitle {
|
||||
get {
|
||||
|
@ -180,6 +180,15 @@ namespace GitHub.App {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The destination already exists..
|
||||
/// </summary>
|
||||
public static string DestinationAlreadyExists {
|
||||
get {
|
||||
return ResourceManager.GetString("DestinationAlreadyExists", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please enter an Enterprise URL.
|
||||
/// </summary>
|
||||
|
@ -288,6 +297,15 @@ namespace GitHub.App {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Logout Required.
|
||||
/// </summary>
|
||||
public static string LogoutRequired {
|
||||
get {
|
||||
return ResourceManager.GetString("LogoutRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You must pull before you can push.
|
||||
/// </summary>
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
<value>Select a containing folder for your new repository.</value>
|
||||
</data>
|
||||
<data name="CloneTitle" xml:space="preserve">
|
||||
<value>Clone a {0} Repository</value>
|
||||
<value>Clone a Repository</value>
|
||||
</data>
|
||||
<data name="CouldNotConnectToGitHub" xml:space="preserve">
|
||||
<value>Could not connect to github.com</value>
|
||||
|
@ -327,4 +327,10 @@ https://git-scm.com/download/win</value>
|
|||
<data name="CancelPendingReviewConfirmationCaption" xml:space="preserve">
|
||||
<value>Cancel Review</value>
|
||||
</data>
|
||||
<data name="DestinationAlreadyExists" xml:space="preserve">
|
||||
<value>The destination already exists.</value>
|
||||
</data>
|
||||
<data name="LogoutRequired" xml:space="preserve">
|
||||
<value>Logout Required</value>
|
||||
</data>
|
||||
</root>
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.SampleData.Dialog.Clone
|
||||
{
|
||||
public class RepositoryCloneViewModelDesigner : ViewModelBase, IRepositoryCloneViewModel
|
||||
{
|
||||
public RepositoryCloneViewModelDesigner()
|
||||
{
|
||||
GitHubTab = new SelectPageViewModelDesigner();
|
||||
EnterpriseTab = new SelectPageViewModelDesigner();
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
public string PathError { get; set; }
|
||||
public int SelectedTabIndex { get; set; }
|
||||
public string Title => null;
|
||||
public IObservable<object> Done => null;
|
||||
public IRepositorySelectViewModel GitHubTab { get; }
|
||||
public IRepositorySelectViewModel EnterpriseTab { get; }
|
||||
public IRepositoryUrlViewModel UrlTab { get; }
|
||||
public ReactiveCommand<object> Browse { get; }
|
||||
public ReactiveCommand<CloneDialogResult> Clone { get; }
|
||||
|
||||
public Task InitializeAsync(IConnection connection)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
using GitHub.Models;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
|
||||
namespace GitHub.SampleData.Dialog.Clone
|
||||
{
|
||||
public class SelectPageViewModelDesigner : ViewModelBase, IRepositorySelectViewModel
|
||||
{
|
||||
public SelectPageViewModelDesigner()
|
||||
{
|
||||
var items = new[]
|
||||
{
|
||||
new RepositoryListItemModel { Name = "encourage", Owner = "haacked" },
|
||||
new RepositoryListItemModel { Name = "haacked.com", Owner = "haacked", IsFork = true },
|
||||
new RepositoryListItemModel { Name = "octokit.net", Owner = "octokit" },
|
||||
new RepositoryListItemModel { Name = "octokit.rb", Owner = "octokit" },
|
||||
new RepositoryListItemModel { Name = "octokit.objc", Owner = "octokit" },
|
||||
new RepositoryListItemModel { Name = "windows", Owner = "github" },
|
||||
new RepositoryListItemModel { Name = "mac", Owner = "github", IsPrivate = true },
|
||||
new RepositoryListItemModel { Name = "github", Owner = "github", IsPrivate = true }
|
||||
};
|
||||
|
||||
Items = items.Select(x => new RepositoryItemViewModel(x, x.Owner)).ToList();
|
||||
ItemsView = CollectionViewSource.GetDefaultView(Items);
|
||||
ItemsView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(RepositoryItemViewModel.Group)));
|
||||
}
|
||||
|
||||
public Exception Error { get; set; }
|
||||
public string Filter { get; set; }
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public bool IsLoading { get; set; }
|
||||
public IReadOnlyList<IRepositoryItemViewModel> Items { get; }
|
||||
public ICollectionView ItemsView { get; }
|
||||
public IRepositoryItemViewModel SelectedItem { get; set; }
|
||||
public IRepositoryModel Repository { get; }
|
||||
|
||||
public void Initialize(IConnection connection)
|
||||
{
|
||||
}
|
||||
|
||||
public Task Activate()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ using GitHub.UI;
|
|||
using GitHub.Validation;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
using GitHub.ViewModels.TeamExplorer;
|
||||
using GitHub.VisualStudio.TeamExplorer.Connect;
|
||||
using GitHub.VisualStudio.TeamExplorer.Home;
|
||||
|
@ -198,9 +199,8 @@ namespace GitHub.SampleData
|
|||
public HostAddress HostAddress { get; set; }
|
||||
|
||||
public string Username { get; set; }
|
||||
public ObservableCollection<ILocalRepositoryModel> Repositories { get; set; }
|
||||
|
||||
public Octokit.User User => null;
|
||||
public ScopesCollection Scopes => null;
|
||||
public bool IsLoggedIn => true;
|
||||
public bool IsLoggingIn => false;
|
||||
|
||||
|
@ -261,195 +261,4 @@ namespace GitHub.SampleData
|
|||
return new RemoteRepositoryModel(0, name, new UriString("http://github.com/" + name + "/" + owner), false, false, new AccountDesigner() { Login = owner }, null);
|
||||
}
|
||||
}
|
||||
|
||||
public class RepositoryCloneViewModelDesigner : ViewModelBase, IRepositoryCloneViewModel
|
||||
{
|
||||
public RepositoryCloneViewModelDesigner()
|
||||
{
|
||||
Repositories = new ObservableCollection<IRemoteRepositoryModel>
|
||||
{
|
||||
RepositoryModelDesigner.Create("encourage", "haacked"),
|
||||
RepositoryModelDesigner.Create("haacked.com", "haacked"),
|
||||
RepositoryModelDesigner.Create("octokit.net", "octokit"),
|
||||
RepositoryModelDesigner.Create("octokit.rb", "octokit"),
|
||||
RepositoryModelDesigner.Create("octokit.objc", "octokit"),
|
||||
RepositoryModelDesigner.Create("windows", "github"),
|
||||
RepositoryModelDesigner.Create("mac", "github"),
|
||||
RepositoryModelDesigner.Create("github", "github")
|
||||
};
|
||||
|
||||
BrowseForDirectory = ReactiveCommand.Create();
|
||||
|
||||
BaseRepositoryPathValidator = ReactivePropertyValidator.ForObservable(this.WhenAny(x => x.BaseRepositoryPath, x => x.Value))
|
||||
.IfNullOrEmpty("Please enter a repository path")
|
||||
.IfTrue(x => x.Length > 200, "Path too long")
|
||||
.IfContainsInvalidPathChars("Path contains invalid characters")
|
||||
.IfPathNotRooted("Please enter a valid path");
|
||||
}
|
||||
|
||||
public IReactiveCommand<object> CloneCommand
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public IRepositoryModel SelectedRepository { get; set; }
|
||||
|
||||
public ObservableCollection<IRemoteRepositoryModel> Repositories
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool FilterTextIsEnabled
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string FilterText { get; set; }
|
||||
|
||||
public string Title { get { return "Clone a GitHub Repository"; } }
|
||||
|
||||
public IReactiveCommand<IReadOnlyList<IRemoteRepositoryModel>> LoadRepositoriesCommand
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool LoadingFailed
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public bool NoRepositoriesFound
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public ICommand BrowseForDirectory
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string BaseRepositoryPath
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool CanClone
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<string> BaseRepositoryPathValidator
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public IObservable<object> Done { get; }
|
||||
|
||||
public Task InitializeAsync(IConnection connection) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
public class GitHubHomeSectionDesigner : IGitHubHomeSection
|
||||
{
|
||||
public GitHubHomeSectionDesigner()
|
||||
{
|
||||
Icon = Octicon.repo;
|
||||
RepoName = "octokit";
|
||||
RepoUrl = "https://github.com/octokit/something-really-long-here-to-check-for-trimming";
|
||||
IsLoggedIn = false;
|
||||
}
|
||||
|
||||
public Octicon Icon
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool IsLoggedIn
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string RepoName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string RepoUrl
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void Login()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ICommand OpenOnGitHub { get; }
|
||||
}
|
||||
|
||||
public class GitHubConnectSectionDesigner : IGitHubConnectSection
|
||||
{
|
||||
public GitHubConnectSectionDesigner()
|
||||
{
|
||||
Repositories = new ObservableCollection<ILocalRepositoryModel>();
|
||||
Repositories.Add(new LocalRepositoryModel("octokit", new UriString("https://github.com/octokit/octokit.net"), @"C:\Users\user\Source\Repos\octokit.net", new GitServiceDesigner()));
|
||||
Repositories.Add(new LocalRepositoryModel("cefsharp", new UriString("https://github.com/cefsharp/cefsharp"), @"C:\Users\user\Source\Repos\cefsharp", new GitServiceDesigner()));
|
||||
Repositories.Add(new LocalRepositoryModel("git-lfs", new UriString("https://github.com/github/git-lfs"), @"C:\Users\user\Source\Repos\git-lfs", new GitServiceDesigner()));
|
||||
Repositories.Add(new LocalRepositoryModel("another octokit", new UriString("https://github.com/octokit/octokit.net"), @"C:\Users\user\Source\Repos\another-octokit.net", new GitServiceDesigner()));
|
||||
Repositories.Add(new LocalRepositoryModel("some cefsharp", new UriString("https://github.com/cefsharp/cefsharp"), @"C:\Users\user\Source\Repos\something-else", new GitServiceDesigner()));
|
||||
Repositories.Add(new LocalRepositoryModel("even more git-lfs", new UriString("https://github.com/github/git-lfs"), @"C:\Users\user\Source\Repos\A different path", new GitServiceDesigner()));
|
||||
}
|
||||
|
||||
public ObservableCollection<ILocalRepositoryModel> Repositories
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public void DoCreate()
|
||||
{
|
||||
}
|
||||
|
||||
public void SignOut()
|
||||
{
|
||||
}
|
||||
|
||||
public void Login()
|
||||
{
|
||||
}
|
||||
|
||||
public void Retry()
|
||||
{
|
||||
}
|
||||
|
||||
public bool OpenRepository()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public string ErrorMessage { get; set; }
|
||||
public IConnection SectionConnection { get; }
|
||||
public bool IsLoggingIn { get; set; }
|
||||
public bool ShowLogin { get; set; }
|
||||
public bool ShowLogout { get; set; }
|
||||
public bool ShowRetry { get; set; }
|
||||
public ICommand Clone { get; }
|
||||
}
|
||||
|
||||
public class InfoPanelDesigner
|
||||
{
|
||||
public string Message => "This is an informational message for the [info panel](link) to test things in design mode.";
|
||||
public MessageType MessageType => MessageType.Information;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Api;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Factories;
|
||||
using GitHub.Models;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
|
||||
namespace GitHub.Services
|
||||
{
|
||||
|
@ -29,17 +31,15 @@ namespace GitHub.Services
|
|||
|
||||
public async Task<CloneDialogResult> ShowCloneDialog(IConnection connection)
|
||||
{
|
||||
Guard.ArgumentNotNull(connection, nameof(connection));
|
||||
|
||||
var viewModel = factory.CreateViewModel<IRepositoryCloneViewModel>();
|
||||
|
||||
if (connection != null)
|
||||
{
|
||||
await viewModel.InitializeAsync(connection);
|
||||
return (CloneDialogResult)await showDialog.Show(viewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (CloneDialogResult)await showDialog.ShowWithFirstConnection(viewModel);
|
||||
}
|
||||
return (CloneDialogResult)await showDialog.Show(
|
||||
viewModel,
|
||||
connection,
|
||||
ApiClientConfiguration.RequestedScopes)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<string> ShowReCloneDialog(IRepositoryModel repository)
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
using System.Reactive;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Api;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Logging;
|
||||
using Microsoft.VisualStudio.Shell;
|
||||
using Serilog;
|
||||
using Rothko;
|
||||
using GitHub.Helpers;
|
||||
using GitHub.Logging;
|
||||
using GitHub.Models;
|
||||
using GitHub.Primitives;
|
||||
using Microsoft.VisualStudio.Shell;
|
||||
using Octokit.GraphQL;
|
||||
using Octokit.GraphQL.Model;
|
||||
using Rothko;
|
||||
using Serilog;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace GitHub.Services
|
||||
|
@ -27,21 +34,74 @@ namespace GitHub.Services
|
|||
readonly IOperatingSystem operatingSystem;
|
||||
readonly string defaultClonePath;
|
||||
readonly IVSGitServices vsGitServices;
|
||||
readonly IGraphQLClientFactory graphqlFactory;
|
||||
readonly IUsageTracker usageTracker;
|
||||
ICompiledQuery<ViewerRepositoriesModel> readViewerRepositories;
|
||||
|
||||
[ImportingConstructor]
|
||||
public RepositoryCloneService(
|
||||
IOperatingSystem operatingSystem,
|
||||
IVSGitServices vsGitServices,
|
||||
IGraphQLClientFactory graphqlFactory,
|
||||
IUsageTracker usageTracker)
|
||||
{
|
||||
this.operatingSystem = operatingSystem;
|
||||
this.vsGitServices = vsGitServices;
|
||||
this.graphqlFactory = graphqlFactory;
|
||||
this.usageTracker = usageTracker;
|
||||
|
||||
defaultClonePath = GetLocalClonePathFromGitProvider(operatingSystem.Environment.GetUserRepositoriesPath());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address)
|
||||
{
|
||||
if (readViewerRepositories == null)
|
||||
{
|
||||
var order = new RepositoryOrder
|
||||
{
|
||||
Field = RepositoryOrderField.Name,
|
||||
Direction = OrderDirection.Asc
|
||||
};
|
||||
|
||||
var affiliation = new RepositoryAffiliation?[]
|
||||
{
|
||||
RepositoryAffiliation.Owner
|
||||
};
|
||||
|
||||
var repositorySelection = new Fragment<Repository, RepositoryListItemModel>(
|
||||
"repository",
|
||||
repo => new RepositoryListItemModel
|
||||
{
|
||||
IsFork = repo.IsFork,
|
||||
IsPrivate = repo.IsPrivate,
|
||||
Name = repo.Name,
|
||||
Owner = repo.Owner.Login,
|
||||
Url = new Uri(repo.Url),
|
||||
});
|
||||
|
||||
readViewerRepositories = new Query()
|
||||
.Viewer
|
||||
.Select(viewer => new ViewerRepositoriesModel
|
||||
{
|
||||
Repositories = viewer.Repositories(null, null, null, null, null, order, affiliation, null, null)
|
||||
.AllPages()
|
||||
.Select(repositorySelection).ToList(),
|
||||
OrganizationRepositories = viewer.Organizations(null, null, null, null).AllPages().Select(org => new
|
||||
{
|
||||
org.Login,
|
||||
Repositories = org.Repositories(null, null, null, null, null, order, null, null, null)
|
||||
.AllPages()
|
||||
.Select(repositorySelection).ToList()
|
||||
}).ToDictionary(x => x.Login, x => (IReadOnlyList<RepositoryListItemModel>)x.Repositories),
|
||||
}).Compile();
|
||||
}
|
||||
|
||||
var graphql = await graphqlFactory.CreateConnection(address).ConfigureAwait(false);
|
||||
var result = await graphql.Run(readViewerRepositories).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task CloneRepository(
|
||||
string cloneUrl,
|
||||
|
@ -73,6 +133,9 @@ namespace GitHub.Services
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool DestinationExists(string path) => Directory.Exists(path) || File.Exists(path);
|
||||
|
||||
string GetLocalClonePathFromGitProvider(string fallbackPath)
|
||||
{
|
||||
var ret = vsGitServices.GetLocalClonePathFromGitProvider();
|
||||
|
@ -82,5 +145,10 @@ namespace GitHub.Services
|
|||
}
|
||||
|
||||
public string DefaultClonePath { get { return defaultClonePath; } }
|
||||
|
||||
class OrganizationAdapter
|
||||
{
|
||||
public IReadOnlyList<RepositoryListItemModel> Repositories { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.App;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Logging;
|
||||
using GitHub.Models;
|
||||
using GitHub.Services;
|
||||
using ReactiveUI;
|
||||
using Rothko;
|
||||
using Serilog;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
[Export(typeof(IRepositoryCloneViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public class RepositoryCloneViewModel : ViewModelBase, IRepositoryCloneViewModel
|
||||
{
|
||||
static readonly ILogger log = LogManager.ForContext<RepositoryCloneViewModel>();
|
||||
readonly IOperatingSystem os;
|
||||
readonly IConnectionManager connectionManager;
|
||||
readonly IRepositoryCloneService service;
|
||||
readonly IReadOnlyList<IRepositoryCloneTabViewModel> tabs;
|
||||
string path;
|
||||
ObservableAsPropertyHelper<string> pathError;
|
||||
int selectedTabIndex;
|
||||
|
||||
[ImportingConstructor]
|
||||
public RepositoryCloneViewModel(
|
||||
IOperatingSystem os,
|
||||
IConnectionManager connectionManager,
|
||||
IRepositoryCloneService service,
|
||||
IRepositorySelectViewModel gitHubTab,
|
||||
IRepositorySelectViewModel enterpriseTab,
|
||||
IRepositoryUrlViewModel urlTab)
|
||||
{
|
||||
this.os = os;
|
||||
this.connectionManager = connectionManager;
|
||||
this.service = service;
|
||||
|
||||
GitHubTab = gitHubTab;
|
||||
EnterpriseTab = enterpriseTab;
|
||||
UrlTab = urlTab;
|
||||
tabs = new IRepositoryCloneTabViewModel[] { GitHubTab, EnterpriseTab, UrlTab };
|
||||
|
||||
var repository = this.WhenAnyValue(x => x.SelectedTabIndex)
|
||||
.SelectMany(x => tabs[x].WhenAnyValue(tab => tab.Repository));
|
||||
|
||||
Path = service.DefaultClonePath;
|
||||
repository.Subscribe(x => UpdatePath(x));
|
||||
|
||||
pathError = Observable.CombineLatest(
|
||||
repository,
|
||||
this.WhenAnyValue(x => x.Path),
|
||||
ValidatePath)
|
||||
.ToProperty(this, x => x.PathError);
|
||||
|
||||
var canClone = Observable.CombineLatest(
|
||||
repository,
|
||||
this.WhenAnyValue(x => x.PathError),
|
||||
(repo, error) => (repo, error))
|
||||
.Select(x => x.repo != null && x.error == null);
|
||||
|
||||
Browse = ReactiveCommand.Create().OnExecuteCompleted(_ => BrowseForDirectory());
|
||||
Clone = ReactiveCommand.CreateAsyncObservable(
|
||||
canClone,
|
||||
_ => repository.Select(x => new CloneDialogResult(Path, x)));
|
||||
}
|
||||
|
||||
public IRepositorySelectViewModel GitHubTab { get; }
|
||||
public IRepositorySelectViewModel EnterpriseTab { get; }
|
||||
public IRepositoryUrlViewModel UrlTab { get; }
|
||||
|
||||
public string Path
|
||||
{
|
||||
get => path;
|
||||
set => this.RaiseAndSetIfChanged(ref path, value);
|
||||
}
|
||||
|
||||
public string PathError => pathError.Value;
|
||||
|
||||
public int SelectedTabIndex
|
||||
{
|
||||
get => selectedTabIndex;
|
||||
set => this.RaiseAndSetIfChanged(ref selectedTabIndex, value);
|
||||
}
|
||||
|
||||
public string Title => Resources.CloneTitle;
|
||||
|
||||
public IObservable<object> Done => Clone;
|
||||
|
||||
public ReactiveCommand<object> Browse { get; }
|
||||
|
||||
public ReactiveCommand<CloneDialogResult> Clone { get; }
|
||||
|
||||
public async Task InitializeAsync(IConnection connection)
|
||||
{
|
||||
var connections = await connectionManager.GetLoadedConnections().ConfigureAwait(false);
|
||||
var gitHubConnection = connections.FirstOrDefault(x => x.HostAddress.IsGitHubDotCom());
|
||||
var enterpriseConnection = connections.FirstOrDefault(x => !x.HostAddress.IsGitHubDotCom());
|
||||
|
||||
if (gitHubConnection?.IsLoggedIn == true)
|
||||
{
|
||||
GitHubTab.Initialize(gitHubConnection);
|
||||
}
|
||||
|
||||
if (enterpriseConnection?.IsLoggedIn == true)
|
||||
{
|
||||
EnterpriseTab.Initialize(enterpriseConnection);
|
||||
}
|
||||
|
||||
if (connection == enterpriseConnection)
|
||||
{
|
||||
SelectedTabIndex = 1;
|
||||
}
|
||||
|
||||
this.WhenAnyValue(x => x.SelectedTabIndex).Subscribe(x => tabs[x].Activate().Forget());
|
||||
}
|
||||
|
||||
void BrowseForDirectory()
|
||||
{
|
||||
var result = os.Dialog.BrowseForDirectory(Path, Resources.BrowseForDirectory);
|
||||
|
||||
if (result != BrowseDirectoryResult.Failed)
|
||||
{
|
||||
var path = result.DirectoryPath;
|
||||
var selected = tabs[SelectedTabIndex].Repository;
|
||||
|
||||
if (selected != null)
|
||||
{
|
||||
path = System.IO.Path.Combine(path, selected.Name);
|
||||
}
|
||||
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdatePath(IRepositoryModel x)
|
||||
{
|
||||
if (x != null)
|
||||
{
|
||||
if (Path == service.DefaultClonePath)
|
||||
{
|
||||
Path = System.IO.Path.Combine(Path, x.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
var basePath = System.IO.Path.GetDirectoryName(Path);
|
||||
Path = System.IO.Path.Combine(basePath, x.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string ValidatePath(IRepositoryModel repository, string path)
|
||||
{
|
||||
if (repository != null)
|
||||
{
|
||||
return service.DestinationExists(path) ?
|
||||
Resources.DestinationAlreadyExists :
|
||||
null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using GitHub.Models;
|
||||
using GitHub.UI;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
public class RepositoryItemViewModel : ViewModelBase, IRepositoryItemViewModel
|
||||
{
|
||||
public RepositoryItemViewModel(RepositoryListItemModel model, string group)
|
||||
{
|
||||
Name = model.Name;
|
||||
Owner = model.Owner;
|
||||
Icon = model.IsPrivate
|
||||
? Octicon.@lock
|
||||
: model.IsFork
|
||||
? Octicon.repo_forked
|
||||
: Octicon.repo;
|
||||
Url = model.Url;
|
||||
Group = group;
|
||||
}
|
||||
|
||||
public string Caption => Owner + '/' + Name;
|
||||
public string Name { get; }
|
||||
public string Owner { get; }
|
||||
public string Group { get; }
|
||||
public Octicon Icon { get; }
|
||||
public Uri Url { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Data;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Logging;
|
||||
using GitHub.Models;
|
||||
using GitHub.Primitives;
|
||||
using GitHub.Services;
|
||||
using ReactiveUI;
|
||||
using Serilog;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
[Export(typeof(IRepositorySelectViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public class RepositorySelectViewModel : ViewModelBase, IRepositorySelectViewModel
|
||||
{
|
||||
static readonly ILogger log = LogManager.ForContext<RepositorySelectViewModel>();
|
||||
readonly IRepositoryCloneService service;
|
||||
IConnection connection;
|
||||
Exception error;
|
||||
string filter;
|
||||
bool isEnabled;
|
||||
bool isLoading;
|
||||
bool loadingStarted;
|
||||
IReadOnlyList<IRepositoryItemViewModel> items;
|
||||
ICollectionView itemsView;
|
||||
ObservableAsPropertyHelper<IRepositoryModel> repository;
|
||||
IRepositoryItemViewModel selectedItem;
|
||||
|
||||
[ImportingConstructor]
|
||||
public RepositorySelectViewModel(IRepositoryCloneService service)
|
||||
{
|
||||
Guard.ArgumentNotNull(service, nameof(service));
|
||||
|
||||
this.service = service;
|
||||
|
||||
repository = this.WhenAnyValue(x => x.SelectedItem)
|
||||
.Select(CreateRepository)
|
||||
.ToProperty(this, x => x.Repository);
|
||||
this.WhenAnyValue(x => x.Filter).Subscribe(_ => ItemsView?.Refresh());
|
||||
}
|
||||
|
||||
public Exception Error
|
||||
{
|
||||
get => error;
|
||||
private set => this.RaiseAndSetIfChanged(ref error, value);
|
||||
}
|
||||
|
||||
public string Filter
|
||||
{
|
||||
get => filter;
|
||||
set => this.RaiseAndSetIfChanged(ref filter, value);
|
||||
}
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => isEnabled;
|
||||
private set => this.RaiseAndSetIfChanged(ref isEnabled, value);
|
||||
}
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => isLoading;
|
||||
private set => this.RaiseAndSetIfChanged(ref isLoading, value);
|
||||
}
|
||||
|
||||
public IReadOnlyList<IRepositoryItemViewModel> Items
|
||||
{
|
||||
get => items;
|
||||
private set => this.RaiseAndSetIfChanged(ref items, value);
|
||||
}
|
||||
|
||||
public ICollectionView ItemsView
|
||||
{
|
||||
get => itemsView;
|
||||
private set => this.RaiseAndSetIfChanged(ref itemsView, value);
|
||||
}
|
||||
|
||||
public IRepositoryItemViewModel SelectedItem
|
||||
{
|
||||
get => selectedItem;
|
||||
set => this.RaiseAndSetIfChanged(ref selectedItem, value);
|
||||
}
|
||||
|
||||
public IRepositoryModel Repository => repository.Value;
|
||||
|
||||
public void Initialize(IConnection connection)
|
||||
{
|
||||
Guard.ArgumentNotNull(connection, nameof(connection));
|
||||
|
||||
this.connection = connection;
|
||||
IsEnabled = true;
|
||||
}
|
||||
|
||||
public async Task Activate()
|
||||
{
|
||||
if (connection == null || loadingStarted) return;
|
||||
|
||||
Error = null;
|
||||
IsLoading = true;
|
||||
loadingStarted = true;
|
||||
|
||||
try
|
||||
{
|
||||
var results = await service.ReadViewerRepositories(connection.HostAddress).ConfigureAwait(true);
|
||||
|
||||
var yourRepositories = results.Repositories
|
||||
.Select(x => new RepositoryItemViewModel(x, "Your repositories"));
|
||||
var orgRepositories = results.OrganizationRepositories
|
||||
.OrderBy(x => x.Key)
|
||||
.SelectMany(x => x.Value.Select(y => new RepositoryItemViewModel(y, x.Key)));
|
||||
Items = yourRepositories.Concat(orgRepositories).ToList();
|
||||
ItemsView = CollectionViewSource.GetDefaultView(Items);
|
||||
ItemsView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(RepositoryItemViewModel.Group)));
|
||||
ItemsView.Filter = FilterItem;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error(ex, "Error reading repository list from {Address}", connection.HostAddress);
|
||||
|
||||
if (ex is AggregateException aggregate)
|
||||
{
|
||||
ex = aggregate.InnerExceptions[0];
|
||||
}
|
||||
|
||||
Error = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool FilterItem(object obj)
|
||||
{
|
||||
if (obj is IRepositoryItemViewModel item && !string.IsNullOrWhiteSpace(Filter))
|
||||
{
|
||||
return item.Caption.Contains(Filter, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
IRepositoryModel CreateRepository(IRepositoryItemViewModel item)
|
||||
{
|
||||
return item != null ?
|
||||
new RepositoryModel(item.Name, UriString.ToUriString(item.Url)) :
|
||||
null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Models;
|
||||
using GitHub.Primitives;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.App.ViewModels.Dialog.Clone
|
||||
{
|
||||
[Export(typeof(IRepositoryUrlViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public class RepositoryUrlViewModel : ViewModelBase, IRepositoryUrlViewModel
|
||||
{
|
||||
ObservableAsPropertyHelper<IRepositoryModel> repository;
|
||||
string url;
|
||||
|
||||
public RepositoryUrlViewModel()
|
||||
{
|
||||
repository = this.WhenAnyValue(x => x.Url, ParseUrl).ToProperty(this, x => x.Repository);
|
||||
}
|
||||
|
||||
public string Url
|
||||
{
|
||||
get => url;
|
||||
set => this.RaiseAndSetIfChanged(ref url, value);
|
||||
}
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
public IRepositoryModel Repository => repository.Value;
|
||||
|
||||
public Task Activate() => Task.CompletedTask;
|
||||
|
||||
IRepositoryModel ParseUrl(string s)
|
||||
{
|
||||
if (s != null)
|
||||
{
|
||||
var uri = new UriString(s);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(uri.Owner) || !string.IsNullOrWhiteSpace(uri.RepositoryName))
|
||||
{
|
||||
var parts = s.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
uri = UriString.ToUriString(
|
||||
HostAddress.GitHubDotComHostAddress.WebUri
|
||||
.Append(parts[0])
|
||||
.Append(parts[1]));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(uri.Owner) && !string.IsNullOrWhiteSpace(uri.RepositoryName))
|
||||
{
|
||||
return new RepositoryModel(uri.RepositoryName, uri);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -62,40 +62,52 @@ namespace GitHub.ViewModels.Dialog
|
|||
public async Task StartWithConnection<T>(T viewModel)
|
||||
where T : IDialogContentViewModel, IConnectionInitializedViewModel
|
||||
{
|
||||
var connections = await connectionManager.Value.GetLoadedConnections();
|
||||
var connections = await connectionManager.Value.GetLoadedConnections().ConfigureAwait(true);
|
||||
var connection = connections.FirstOrDefault(x => x.IsLoggedIn);
|
||||
|
||||
if (connection == null)
|
||||
{
|
||||
var login = CreateLoginViewModel();
|
||||
connection = await ShowLogin().ConfigureAwait(true);
|
||||
}
|
||||
|
||||
subscription = login.Done.Take(1).Subscribe(async x =>
|
||||
{
|
||||
var newConnection = (IConnection)x;
|
||||
|
||||
if (newConnection != null)
|
||||
{
|
||||
await viewModel.InitializeAsync(newConnection);
|
||||
Start(viewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
done.OnNext(null);
|
||||
}
|
||||
});
|
||||
|
||||
Content = login;
|
||||
if (connection != null)
|
||||
{
|
||||
await viewModel.InitializeAsync(connection).ConfigureAwait(true);
|
||||
Start(viewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
await viewModel.InitializeAsync(connection);
|
||||
Start(viewModel);
|
||||
done.OnNext(null);
|
||||
}
|
||||
}
|
||||
|
||||
ILoginViewModel CreateLoginViewModel()
|
||||
public async Task StartWithLogout<T>(T viewModel, IConnection connection)
|
||||
where T : IDialogContentViewModel, IConnectionInitializedViewModel
|
||||
{
|
||||
return factory.CreateViewModel<ILoginViewModel>();
|
||||
var logout = factory.CreateViewModel<ILogOutRequiredViewModel>();
|
||||
|
||||
subscription?.Dispose();
|
||||
subscription = logout.Done.Take(1).Subscribe(async _ =>
|
||||
{
|
||||
await connectionManager.Value.LogOut(connection.HostAddress).ConfigureAwait(true);
|
||||
|
||||
connection = await ShowLogin().ConfigureAwait(true);
|
||||
|
||||
if (connection != null)
|
||||
{
|
||||
await viewModel.InitializeAsync(connection).ConfigureAwait(true);
|
||||
Start(viewModel);
|
||||
}
|
||||
});
|
||||
|
||||
Content = logout;
|
||||
}
|
||||
|
||||
async Task<IConnection> ShowLogin()
|
||||
{
|
||||
var login = factory.CreateViewModel<ILoginViewModel>();
|
||||
Content = login;
|
||||
return (IConnection)await login.Done.Take(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.App.ViewModels.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// The "Logout required" dialog page.
|
||||
/// </summary>
|
||||
[Export(typeof(ILogOutRequiredViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public class LogOutRequiredViewModel : ViewModelBase, ILogOutRequiredViewModel
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ReactiveCommand<object> LogOut { get; } = ReactiveCommand.Create();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Title => Resources.LogoutRequired;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IObservable<object> Done => LogOut;
|
||||
}
|
||||
}
|
|
@ -1,280 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using GitHub.App;
|
||||
using GitHub.Collections;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Factories;
|
||||
using GitHub.Logging;
|
||||
using GitHub.Models;
|
||||
using GitHub.Services;
|
||||
using GitHub.Validation;
|
||||
using ReactiveUI;
|
||||
using Rothko;
|
||||
using Serilog;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog
|
||||
{
|
||||
[Export(typeof(IRepositoryCloneViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public class RepositoryCloneViewModel : ViewModelBase, IRepositoryCloneViewModel
|
||||
{
|
||||
static readonly ILogger log = LogManager.ForContext<RepositoryCloneViewModel>();
|
||||
|
||||
readonly IModelServiceFactory modelServiceFactory;
|
||||
readonly IOperatingSystem operatingSystem;
|
||||
readonly ReactiveCommand<object> browseForDirectoryCommand = ReactiveCommand.Create();
|
||||
bool noRepositoriesFound;
|
||||
readonly ObservableAsPropertyHelper<bool> canClone;
|
||||
string baseRepositoryPath;
|
||||
bool loadingFailed;
|
||||
|
||||
[ImportingConstructor]
|
||||
public RepositoryCloneViewModel(
|
||||
IModelServiceFactory modelServiceFactory,
|
||||
IRepositoryCloneService cloneService,
|
||||
IOperatingSystem operatingSystem)
|
||||
{
|
||||
Guard.ArgumentNotNull(modelServiceFactory, nameof(modelServiceFactory));
|
||||
Guard.ArgumentNotNull(cloneService, nameof(cloneService));
|
||||
Guard.ArgumentNotNull(operatingSystem, nameof(operatingSystem));
|
||||
|
||||
this.modelServiceFactory = modelServiceFactory;
|
||||
this.operatingSystem = operatingSystem;
|
||||
|
||||
Repositories = new TrackingCollection<IRemoteRepositoryModel>();
|
||||
repositories.ProcessingDelay = TimeSpan.Zero;
|
||||
repositories.Comparer = OrderedComparer<IRemoteRepositoryModel>.OrderBy(x => x.Owner).ThenBy(x => x.Name).Compare;
|
||||
repositories.Filter = FilterRepository;
|
||||
repositories.NewerComparer = OrderedComparer<IRemoteRepositoryModel>.OrderByDescending(x => x.UpdatedAt).Compare;
|
||||
|
||||
filterTextIsEnabled = this.WhenAny(x => x.IsBusy,
|
||||
loading => loading.Value || repositories.UnfilteredCount > 0 && !LoadingFailed)
|
||||
.ToProperty(this, x => x.FilterTextIsEnabled);
|
||||
|
||||
this.WhenAny(
|
||||
x => x.repositories.UnfilteredCount,
|
||||
x => x.IsBusy,
|
||||
x => x.LoadingFailed,
|
||||
(unfilteredCount, loading, failed) =>
|
||||
{
|
||||
if (loading.Value)
|
||||
return false;
|
||||
|
||||
if (failed.Value)
|
||||
return false;
|
||||
|
||||
return unfilteredCount.Value == 0;
|
||||
})
|
||||
.Subscribe(x =>
|
||||
{
|
||||
NoRepositoriesFound = x;
|
||||
});
|
||||
|
||||
this.WhenAny(x => x.FilterText, x => x.Value)
|
||||
.DistinctUntilChanged(StringComparer.OrdinalIgnoreCase)
|
||||
.Throttle(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler)
|
||||
.Subscribe(_ => repositories.Filter = FilterRepository);
|
||||
|
||||
var baseRepositoryPath = this.WhenAny(
|
||||
x => x.BaseRepositoryPath,
|
||||
x => x.SelectedRepository,
|
||||
(x, y) => x.Value);
|
||||
|
||||
BaseRepositoryPathValidator = ReactivePropertyValidator.ForObservable(baseRepositoryPath)
|
||||
.IfNullOrEmpty(Resources.RepositoryCreationClonePathEmpty)
|
||||
.IfTrue(x => x.Length > 200, Resources.RepositoryCreationClonePathTooLong)
|
||||
.IfContainsInvalidPathChars(Resources.RepositoryCreationClonePathInvalidCharacters)
|
||||
.IfPathNotRooted(Resources.RepositoryCreationClonePathInvalid)
|
||||
.IfTrue(IsAlreadyRepoAtPath, Resources.RepositoryNameValidatorAlreadyExists);
|
||||
|
||||
var canCloneObservable = this.WhenAny(
|
||||
x => x.SelectedRepository,
|
||||
x => x.BaseRepositoryPathValidator.ValidationResult.IsValid,
|
||||
(x, y) => x.Value != null && y.Value);
|
||||
canClone = canCloneObservable.ToProperty(this, x => x.CanClone);
|
||||
CloneCommand = ReactiveCommand.Create(canCloneObservable);
|
||||
Done = CloneCommand.Select(_ => new CloneDialogResult(BaseRepositoryPath, SelectedRepository));
|
||||
|
||||
browseForDirectoryCommand.Subscribe(_ => ShowBrowseForDirectoryDialog());
|
||||
this.WhenAny(x => x.BaseRepositoryPathValidator.ValidationResult, x => x.Value)
|
||||
.Subscribe();
|
||||
BaseRepositoryPath = cloneService.DefaultClonePath;
|
||||
NoRepositoriesFound = true;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(IConnection connection)
|
||||
{
|
||||
var modelService = await modelServiceFactory.CreateAsync(connection);
|
||||
|
||||
Title = string.Format(CultureInfo.CurrentCulture, Resources.CloneTitle, connection.HostAddress.Title);
|
||||
|
||||
IsBusy = true;
|
||||
modelService.GetRepositories(repositories);
|
||||
repositories.OriginalCompleted
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(
|
||||
_ => { }
|
||||
, ex =>
|
||||
{
|
||||
LoadingFailed = true;
|
||||
IsBusy = false;
|
||||
log.Error(ex, "Error while loading repositories");
|
||||
},
|
||||
() => IsBusy = false
|
||||
);
|
||||
repositories.Subscribe();
|
||||
}
|
||||
|
||||
bool FilterRepository(IRemoteRepositoryModel repo, int position, IList<IRemoteRepositoryModel> list)
|
||||
{
|
||||
Guard.ArgumentNotNull(repo, nameof(repo));
|
||||
Guard.ArgumentNotNull(list, nameof(list));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(FilterText))
|
||||
return true;
|
||||
|
||||
// Not matching on NameWithOwner here since that's already been filtered on by the selected account
|
||||
return repo.Name.IndexOf(FilterText ?? "", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
bool IsAlreadyRepoAtPath(string path)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(path, nameof(path));
|
||||
|
||||
bool isAlreadyRepoAtPath = false;
|
||||
|
||||
if (SelectedRepository != null)
|
||||
{
|
||||
string potentialPath = Path.Combine(path, SelectedRepository.Name);
|
||||
isAlreadyRepoAtPath = operatingSystem.Directory.Exists(potentialPath);
|
||||
}
|
||||
|
||||
return isAlreadyRepoAtPath;
|
||||
}
|
||||
|
||||
IObservable<Unit> ShowBrowseForDirectoryDialog()
|
||||
{
|
||||
return Observable.Start(() =>
|
||||
{
|
||||
// We store this in a local variable to prevent it changing underneath us while the
|
||||
// folder dialog is open.
|
||||
var localBaseRepositoryPath = BaseRepositoryPath;
|
||||
var browseResult = operatingSystem.Dialog.BrowseForDirectory(localBaseRepositoryPath, Resources.BrowseForDirectory);
|
||||
|
||||
if (!browseResult.Success)
|
||||
return;
|
||||
|
||||
var directory = browseResult.DirectoryPath ?? localBaseRepositoryPath;
|
||||
|
||||
try
|
||||
{
|
||||
BaseRepositoryPath = directory;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// TODO: We really should limit this to exceptions we know how to handle.
|
||||
log.Error(e, "Failed to set base repository path. {@Repository}",
|
||||
new { localBaseRepositoryPath, BaseRepositoryPath, directory });
|
||||
}
|
||||
}, RxApp.MainThreadScheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the title for the dialog.
|
||||
/// </summary>
|
||||
public string Title { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to clone repositories into
|
||||
/// </summary>
|
||||
public string BaseRepositoryPath
|
||||
{
|
||||
get { return baseRepositoryPath; }
|
||||
set { this.RaiseAndSetIfChanged(ref baseRepositoryPath, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals that the user clicked the clone button.
|
||||
/// </summary>
|
||||
public IReactiveCommand<object> CloneCommand { get; private set; }
|
||||
|
||||
bool isBusy;
|
||||
public bool IsBusy
|
||||
{
|
||||
get { return isBusy; }
|
||||
private set { this.RaiseAndSetIfChanged(ref isBusy, value); }
|
||||
}
|
||||
|
||||
TrackingCollection<IRemoteRepositoryModel> repositories;
|
||||
public ObservableCollection<IRemoteRepositoryModel> Repositories
|
||||
{
|
||||
get { return repositories; }
|
||||
private set { this.RaiseAndSetIfChanged(ref repositories, (TrackingCollection<IRemoteRepositoryModel>)value); }
|
||||
}
|
||||
|
||||
IRepositoryModel selectedRepository;
|
||||
/// <summary>
|
||||
/// Selected repository to clone
|
||||
/// </summary>
|
||||
public IRepositoryModel SelectedRepository
|
||||
{
|
||||
get { return selectedRepository; }
|
||||
set { this.RaiseAndSetIfChanged(ref selectedRepository, value); }
|
||||
}
|
||||
|
||||
readonly ObservableAsPropertyHelper<bool> filterTextIsEnabled;
|
||||
/// <summary>
|
||||
/// True if there are repositories (otherwise no point in filtering)
|
||||
/// </summary>
|
||||
public bool FilterTextIsEnabled { get { return filterTextIsEnabled.Value; } }
|
||||
|
||||
string filterText;
|
||||
/// <summary>
|
||||
/// User text to filter the repositories list
|
||||
/// </summary>
|
||||
public string FilterText
|
||||
{
|
||||
get { return filterText; }
|
||||
set { this.RaiseAndSetIfChanged(ref filterText, value); }
|
||||
}
|
||||
|
||||
public bool LoadingFailed
|
||||
{
|
||||
get { return loadingFailed; }
|
||||
private set { this.RaiseAndSetIfChanged(ref loadingFailed, value); }
|
||||
}
|
||||
|
||||
public bool NoRepositoriesFound
|
||||
{
|
||||
get { return noRepositoriesFound; }
|
||||
private set { this.RaiseAndSetIfChanged(ref noRepositoriesFound, value); }
|
||||
}
|
||||
|
||||
public ICommand BrowseForDirectory
|
||||
{
|
||||
get { return browseForDirectoryCommand; }
|
||||
}
|
||||
|
||||
public bool CanClone
|
||||
{
|
||||
get { return canClone.Value; }
|
||||
}
|
||||
|
||||
public ReactivePropertyValidator<string> BaseRepositoryPathValidator
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public IObservable<object> Done { get; }
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Reactive;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
using GitHub.Primitives;
|
||||
|
||||
namespace GitHub.Services
|
||||
{
|
||||
|
@ -32,5 +34,16 @@ namespace GitHub.Services
|
|||
string repositoryName,
|
||||
string repositoryPath,
|
||||
object progress = null);
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified destination path already exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The destination path.</param>
|
||||
/// <returns>
|
||||
/// true if a file or directory is already present at <paramref name="path"/>; otherwise false.
|
||||
/// </returns>
|
||||
bool DestinationExists(string path);
|
||||
|
||||
Task<ViewerRepositoriesModel> ReadViewerRepositories(HostAddress address);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Api;
|
||||
using GitHub.Models;
|
||||
using GitHub.Primitives;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
|
@ -27,6 +30,22 @@ namespace GitHub.Services
|
|||
/// </returns>
|
||||
Task<object> Show(IDialogContentViewModel viewModel);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a view model that requires a connection with specifiec scopes in the dialog.
|
||||
/// </summary>
|
||||
/// <param name="viewModel">The view model to show.</param>
|
||||
/// <param name="connection">The connection.</param>
|
||||
/// <param name="scopes">The required scopes.</param>
|
||||
/// <returns>
|
||||
/// If the connection does not have the requested scopes, the user will be invited to log
|
||||
/// out and back in.
|
||||
/// </returns>
|
||||
Task<object> Show<TViewModel>(
|
||||
TViewModel viewModel,
|
||||
IConnection connection,
|
||||
IEnumerable<string> scopes)
|
||||
where TViewModel : IDialogContentViewModel, IConnectionInitializedViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Shows a view model that requires a connection in the dialog.
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a tab in the repository clone dialog.
|
||||
/// </summary>
|
||||
public interface IRepositoryCloneTabViewModel : IViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the tab is enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A disabled tab will be hidden.
|
||||
/// </remarks>
|
||||
bool IsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected repository, or null if no repository has been selected.
|
||||
/// </summary>
|
||||
IRepositoryModel Repository { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Activates the tab.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will be called each time the tab is selected.
|
||||
/// </remarks>
|
||||
Task Activate();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using GitHub.Models;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the the Clone Repository dialog
|
||||
/// </summary>
|
||||
public interface IRepositoryCloneViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the view model for the GitHub.com tab.
|
||||
/// </summary>
|
||||
IRepositorySelectViewModel GitHubTab { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the view model for the enterprise tab.
|
||||
/// </summary>
|
||||
IRepositorySelectViewModel EnterpriseTab { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the view model for the URL tab.
|
||||
/// </summary>
|
||||
IRepositoryUrlViewModel UrlTab { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to clone the repository to.
|
||||
/// </summary>
|
||||
string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an error message that explains why <see cref="Path"/> is not valid.
|
||||
/// </summary>
|
||||
string PathError { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the selected tab.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The tabs are: GitHubPage, EnterprisePage, UrlPage.
|
||||
/// </remarks>
|
||||
int SelectedTabIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command executed when the user clicks "Browse".
|
||||
/// </summary>
|
||||
ReactiveCommand<object> Browse { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command executed when the user clicks "Clone".
|
||||
/// </summary>
|
||||
ReactiveCommand<CloneDialogResult> Clone { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using System;
|
||||
using GitHub.UI;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
public interface IRepositoryItemViewModel
|
||||
{
|
||||
string Caption { get; }
|
||||
string Name { get; }
|
||||
string Owner { get; }
|
||||
Octicon Icon { get; }
|
||||
Uri Url { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
public interface IRepositorySelectViewModel : IRepositoryCloneTabViewModel
|
||||
{
|
||||
Exception Error { get; }
|
||||
string Filter { get; set; }
|
||||
bool IsLoading { get; }
|
||||
IReadOnlyList<IRepositoryItemViewModel> Items { get; }
|
||||
ICollectionView ItemsView { get; }
|
||||
IRepositoryItemViewModel SelectedItem { get; set; }
|
||||
|
||||
void Initialize(IConnection connection);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog.Clone
|
||||
{
|
||||
public interface IRepositoryUrlViewModel : IRepositoryCloneTabViewModel
|
||||
{
|
||||
string Url { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog
|
||||
{
|
||||
|
@ -33,5 +34,13 @@ namespace GitHub.ViewModels.Dialog
|
|||
/// <param name="viewModel">The view model to display.</param>
|
||||
Task StartWithConnection<T>(T viewModel)
|
||||
where T : IDialogContentViewModel, IConnectionInitializedViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Starts displaying a view model that requires a connection that needs to be logged out
|
||||
/// and back in.
|
||||
/// </summary>
|
||||
/// <param name="viewModel">The view model to display.</param>
|
||||
Task StartWithLogout<T>(T viewModel, IConnection connection)
|
||||
where T : IDialogContentViewModel, IConnectionInitializedViewModel;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the "Logout required" dialog page.
|
||||
/// </summary>
|
||||
public interface ILogOutRequiredViewModel : IDialogContentViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a command that will log out the user.
|
||||
/// </summary>
|
||||
ReactiveCommand<object> LogOut { get; }
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
using System;
|
||||
using GitHub.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace GitHub.ViewModels.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel for the the Clone Repository dialog
|
||||
/// </summary>
|
||||
public interface IRepositoryCloneViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of repositories the current user may clone from the specified host.
|
||||
/// </summary>
|
||||
ObservableCollection<IRemoteRepositoryModel> Repositories { get; }
|
||||
|
||||
bool FilterTextIsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, then we failed to load the repositories.
|
||||
/// </summary>
|
||||
bool LoadingFailed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if no repositories were found.
|
||||
/// </summary>
|
||||
bool NoRepositoriesFound { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if a repository is selected.
|
||||
/// </summary>
|
||||
bool CanClone { get; }
|
||||
|
||||
string FilterText { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using GitHub.Services;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
|
@ -11,18 +10,18 @@ namespace GitHub.Models
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CloneDialogResult"/> class.
|
||||
/// </summary>
|
||||
/// <param name="basePath">The selected base path for the clone.</param>
|
||||
/// <param name="path">The path to clone the repository to.</param>
|
||||
/// <param name="repository">The selected repository.</param>
|
||||
public CloneDialogResult(string basePath, IRepositoryModel repository)
|
||||
public CloneDialogResult(string path, IRepositoryModel repository)
|
||||
{
|
||||
BasePath = basePath;
|
||||
Path = path;
|
||||
Repository = repository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filesystem path to which the user wants to clone.
|
||||
/// Gets the path to clone the repository to.
|
||||
/// </summary>
|
||||
public string BasePath { get; }
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository selected by the user.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Primitives;
|
||||
using Octokit;
|
||||
|
||||
|
@ -29,6 +28,11 @@ namespace GitHub.Models
|
|||
/// </remarks>
|
||||
User User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the login scopes.
|
||||
/// </summary>
|
||||
ScopesCollection Scopes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the login of the account succeeded.
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public class OrganizationRepositoriesModel
|
||||
{
|
||||
public ActorModel Organization { get; set; }
|
||||
public IReadOnlyList<RepositoryListItemModel> Repositories { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public class RepositoryListItemModel
|
||||
{
|
||||
public bool IsFork { get; set; }
|
||||
public bool IsPrivate { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Owner { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of OAuth scopes.
|
||||
/// </summary>
|
||||
public class ScopesCollection : IReadOnlyList<string>
|
||||
{
|
||||
readonly IReadOnlyList<string> inner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Scopes"/> class.
|
||||
/// </summary>
|
||||
/// <param name="scopes">The scopes.</param>
|
||||
public ScopesCollection(IReadOnlyList<string> scopes)
|
||||
{
|
||||
inner = scopes;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string this[int index] => inner[index];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count => inner.Count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<string> GetEnumerator() => inner.GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => inner.GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Tests if received API scopes match the required API scopes.
|
||||
/// </summary>
|
||||
/// <param name="required">The required API scopes.</param>
|
||||
/// <returns>True if all required scopes are present, otherwise false.</returns>
|
||||
public bool Matches(IEnumerable<string> required)
|
||||
{
|
||||
foreach (var scope in required)
|
||||
{
|
||||
var found = inner.Contains(scope);
|
||||
|
||||
if (!found &&
|
||||
(scope.StartsWith("read:", StringComparison.Ordinal) ||
|
||||
scope.StartsWith("write:", StringComparison.Ordinal)))
|
||||
{
|
||||
// NOTE: Scopes are actually more complex than this, for example
|
||||
// `user` encompasses `read:user` and `user:email` but just use
|
||||
// this simple rule for now as it works for the scopes we require.
|
||||
var adminScope = scope
|
||||
.Replace("read:", "admin:")
|
||||
.Replace("write:", "admin:");
|
||||
found = inner.Contains(adminScope);
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => string.Join(",", inner);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GitHub.Models
|
||||
{
|
||||
public class ViewerRepositoriesModel
|
||||
{
|
||||
public IReadOnlyList<RepositoryListItemModel> Repositories { get; set; }
|
||||
public IDictionary<string, IReadOnlyList<RepositoryListItemModel>> OrganizationRepositories { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GitHub.Models;
|
||||
|
@ -13,12 +14,16 @@ namespace GitHub.Services
|
|||
{
|
||||
string username;
|
||||
Octokit.User user;
|
||||
ScopesCollection scopes;
|
||||
bool isLoggedIn;
|
||||
bool isLoggingIn;
|
||||
Exception connectionError;
|
||||
|
||||
public Connection(HostAddress hostAddress)
|
||||
public Connection(
|
||||
HostAddress hostAddress,
|
||||
string username)
|
||||
{
|
||||
this.username = username;
|
||||
HostAddress = hostAddress;
|
||||
isLoggedIn = false;
|
||||
isLoggingIn = true;
|
||||
|
@ -26,12 +31,12 @@ namespace GitHub.Services
|
|||
|
||||
public Connection(
|
||||
HostAddress hostAddress,
|
||||
string username,
|
||||
Octokit.User user)
|
||||
Octokit.User user,
|
||||
ScopesCollection scopes)
|
||||
{
|
||||
HostAddress = hostAddress;
|
||||
this.username = username;
|
||||
this.user = user;
|
||||
this.scopes = scopes;
|
||||
isLoggedIn = true;
|
||||
}
|
||||
|
||||
|
@ -44,7 +49,6 @@ namespace GitHub.Services
|
|||
get => username;
|
||||
private set => RaiseAndSetIfChanged(ref username, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Octokit.User User
|
||||
{
|
||||
|
@ -52,6 +56,13 @@ namespace GitHub.Services
|
|||
private set => RaiseAndSetIfChanged(ref user, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ScopesCollection Scopes
|
||||
{
|
||||
get => scopes;
|
||||
private set => RaiseAndSetIfChanged(ref scopes, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsLoggedIn
|
||||
{
|
||||
|
@ -82,7 +93,7 @@ namespace GitHub.Services
|
|||
IsLoggedIn = false;
|
||||
IsLoggingIn = true;
|
||||
User = null;
|
||||
Username = null;
|
||||
Scopes = null;
|
||||
}
|
||||
|
||||
public void SetError(Exception e)
|
||||
|
@ -90,12 +101,13 @@ namespace GitHub.Services
|
|||
ConnectionError = e;
|
||||
IsLoggingIn = false;
|
||||
IsLoggedIn = false;
|
||||
Scopes = null;
|
||||
}
|
||||
|
||||
public void SetSuccess(Octokit.User user)
|
||||
public void SetSuccess(Octokit.User user, ScopesCollection scopes)
|
||||
{
|
||||
User = user;
|
||||
Username = user.Login;
|
||||
Scopes = scopes;
|
||||
IsLoggingIn = false;
|
||||
IsLoggedIn = true;
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
namespace GitHub.ViewModels
|
||||
{
|
||||
public interface IInfoPanel
|
||||
{
|
||||
string Message { get; set; }
|
||||
MessageType MessageType { get; set; }
|
||||
}
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
Information,
|
||||
Warning
|
||||
}
|
||||
}
|
|
@ -370,14 +370,6 @@
|
|||
<HintPath>..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Octokit.GraphQL, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Octokit.GraphQL.Core, Version=0.1.0.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Octokit.GraphQL.0.1.0-beta\lib\netstandard1.1\Octokit.GraphQL.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Octokit.GraphQL, Version=0.1.1.0, Culture=neutral, PublicKeyToken=0be8860aee462442, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Octokit.GraphQL.0.1.1-beta\lib\netstandard1.1\Octokit.GraphQL.dll</HintPath>
|
||||
</Reference>
|
||||
|
|
|
@ -70,11 +70,10 @@ namespace GitHub.StartPage
|
|||
if (request == null)
|
||||
return null;
|
||||
|
||||
var path = Path.Combine(request.BasePath, request.Repository.Name);
|
||||
var uri = request.Repository.CloneUrl.ToRepositoryUrl();
|
||||
return new CodeContainer(
|
||||
localProperties: new CodeContainerLocalProperties(path, CodeContainerType.Folder,
|
||||
new CodeContainerSourceControlProperties(request.Repository.Name, path, new Guid(Guids.GitSccProviderId))),
|
||||
localProperties: new CodeContainerLocalProperties(request.Path, CodeContainerType.Folder,
|
||||
new CodeContainerSourceControlProperties(request.Repository.Name, request.Path, new Guid(Guids.GitSccProviderId))),
|
||||
remote: new RemoteCodeContainer(request.Repository.Name,
|
||||
new Guid(Guids.CodeContainerProviderId),
|
||||
uri,
|
||||
|
@ -144,7 +143,7 @@ namespace GitHub.StartPage
|
|||
await cloneService.CloneRepository(
|
||||
result.Repository.CloneUrl,
|
||||
result.Repository.Name,
|
||||
result.BasePath,
|
||||
result.Path,
|
||||
progress);
|
||||
|
||||
usageTracker.IncrementCounter(x => x.NumberOfStartPageClones).Forget();
|
||||
|
|
|
@ -154,7 +154,7 @@ namespace GitHub.VisualStudio.TeamExplorer.Connect
|
|||
await cloneService.CloneRepository(
|
||||
result.Repository.CloneUrl,
|
||||
result.Repository.Name,
|
||||
result.BasePath);
|
||||
result.Path);
|
||||
|
||||
usageTracker.IncrementCounter(x => x.NumberOfGitHubConnectSectionClones).Forget();
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource GHLinkButtonTextBrush}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,0"/>
|
||||
<Setter Property="Padding" Value="0,0,12,0"/>
|
||||
<Setter Property="Padding" Value="0,2,12,2"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
|
@ -63,7 +63,7 @@
|
|||
<ControlTemplate TargetType="{x:Type TabItem}">
|
||||
<Grid
|
||||
x:Name="templateRoot"
|
||||
Margin="0,2,0,2"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="true"
|
||||
Background="{TemplateBinding Background}">
|
||||
<Grid.RowDefinitions>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
|
||||
namespace GitHub.UI
|
||||
{
|
||||
[Localizability(LocalizationCategory.NeverLocalize)]
|
||||
public sealed class NullOrWhitespaceToVisibilityConverter : ValueConverterMarkupExtension<NullOrWhitespaceToVisibilityConverter>
|
||||
{
|
||||
public override object Convert(object value,
|
||||
Type targetType,
|
||||
object parameter,
|
||||
CultureInfo culture)
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(s) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public override object ConvertBack(object value,
|
||||
Type targetType,
|
||||
object parameter,
|
||||
CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
<cache:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/Styles/GitHubActionLink.xaml"/>
|
||||
<cache:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/Styles/ItemsControls.xaml"/>
|
||||
<cache:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/Styles/SectionControl.xaml"/>
|
||||
<cache:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/UI/Controls/InfoPanel.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<DrawingBrush x:Key="ConnectArrowBrush">
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/GitHub.VisualStudio.UI;component/UI/Controls/InfoPanel.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
|
@ -0,0 +1,107 @@
|
|||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using GitHub.Services;
|
||||
using GitHub.UI;
|
||||
|
||||
namespace GitHub.VisualStudio.UI.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays informational or error message markdown in a banner.
|
||||
/// </summary>
|
||||
public class InfoPanel : Control
|
||||
{
|
||||
public static readonly DependencyProperty MessageProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Message),
|
||||
typeof(string),
|
||||
typeof(InfoPanel));
|
||||
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Icon),
|
||||
typeof(Octicon),
|
||||
typeof(InfoPanel),
|
||||
new FrameworkPropertyMetadata(Octicon.info));
|
||||
|
||||
public static readonly DependencyProperty ShowCloseButtonProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ShowCloseButton),
|
||||
typeof(bool),
|
||||
typeof(InfoPanel));
|
||||
|
||||
static IVisualStudioBrowser browser;
|
||||
Button closeButton;
|
||||
|
||||
static InfoPanel()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(
|
||||
typeof(InfoPanel),
|
||||
new FrameworkPropertyMetadata(typeof(InfoPanel)));
|
||||
DockPanel.DockProperty.OverrideMetadata(
|
||||
typeof(InfoPanel),
|
||||
new FrameworkPropertyMetadata(Dock.Top));
|
||||
}
|
||||
|
||||
public InfoPanel()
|
||||
{
|
||||
var commandBinding = new CommandBinding(Markdig.Wpf.Commands.Hyperlink);
|
||||
commandBinding.Executed += OpenHyperlink;
|
||||
CommandBindings.Add(commandBinding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the message in markdown.
|
||||
/// </summary>
|
||||
public string Message
|
||||
{
|
||||
get => (string)GetValue(MessageProperty);
|
||||
set => SetValue(MessageProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the icon to display.
|
||||
/// </summary>
|
||||
public Octicon Icon
|
||||
{
|
||||
get => (Octicon)GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public bool ShowCloseButton
|
||||
{
|
||||
get => (bool)GetValue(ShowCloseButtonProperty);
|
||||
set => SetValue(ShowCloseButtonProperty, value);
|
||||
}
|
||||
|
||||
static IVisualStudioBrowser Browser
|
||||
{
|
||||
get
|
||||
{
|
||||
if (browser == null)
|
||||
browser = Services.GitHubServiceProvider.TryGetService<IVisualStudioBrowser>();
|
||||
return browser;
|
||||
}
|
||||
}
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
closeButton = (Button)Template.FindName("PART_CloseButton", this);
|
||||
closeButton.Click += CloseButtonClicked;
|
||||
}
|
||||
|
||||
void CloseButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Message = null;
|
||||
}
|
||||
|
||||
void OpenHyperlink(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var url = e.Parameter.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
Browser.OpenUrl(new Uri(url));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +1,65 @@
|
|||
<UserControl x:Class="GitHub.VisualStudio.UI.Controls.InfoPanel"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ghfvs="https://github.com/github/VisualStudio"
|
||||
xmlns:local="clr-namespace:GitHub.VisualStudio.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf"
|
||||
Background="{DynamicResource VsBrush.InfoBackground}"
|
||||
d:DataContext="{d:DesignInstance Type=ghfvs:InfoPanelDesigner, IsDesignTimeCreatable=True}"
|
||||
d:DesignHeight="300"
|
||||
d:DesignWidth="300"
|
||||
mc:Ignorable="d"
|
||||
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.GitHubInfoPanel}">
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ghfvs="https://github.com/github/VisualStudio"
|
||||
xmlns:local="clr-namespace:GitHub.VisualStudio.UI.Controls"
|
||||
xmlns:markdig="clr-namespace:Markdig.Wpf;assembly=Markdig.Wpf">
|
||||
<Style TargetType="local:InfoPanel">
|
||||
<Setter Property="Background" Value="{DynamicResource VsBrush.InfoBackground}"/>
|
||||
<Setter Property="Padding" Value="8"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:InfoPanel">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Visibility="{TemplateBinding Message, Converter={ghfvs:NullOrWhitespaceToVisibilityConverter}}">
|
||||
<DockPanel>
|
||||
<DockPanel.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/SharedDictionary.xaml"/>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/Assets/Markdown.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<Style x:Key="DocumentStyle" TargetType="FlowDocument">
|
||||
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
<Setter Property="PagePadding" Value="0"/>
|
||||
</Style>
|
||||
<Style x:Key="LinkStyle" TargetType="Hyperlink">
|
||||
<Setter Property="Foreground" Value="{DynamicResource GitHubActionLinkItemBrush}"/>
|
||||
<Setter Property="TextDecorations" Value="Underline"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</DockPanel.Resources>
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/SharedDictionary.xaml"/>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/Assets/Markdown.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<Style x:Key="DocumentStyle" TargetType="FlowDocument">
|
||||
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="TextAlignment" Value="Left"/>
|
||||
<Setter Property="PagePadding" Value="0"/>
|
||||
</Style>
|
||||
<Style x:Key="LinkStyle" TargetType="Hyperlink">
|
||||
<Setter Property="Foreground" Value="{DynamicResource GitHubActionLinkItemBrush}"/>
|
||||
<Setter Property="TextDecorations" Value="Underline"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<FrameworkElement.CommandBindings>
|
||||
<CommandBinding Command="{x:Static markdig:Commands.Hyperlink}" Executed="OpenHyperlink" />
|
||||
</FrameworkElement.CommandBindings>
|
||||
|
||||
<DockPanel Margin="8" LastChildFill="True">
|
||||
<ghfvs:OcticonImage Width="16"
|
||||
Height="16"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Top"
|
||||
DockPanel.Dock="Left"
|
||||
Foreground="{Binding IconColor}"
|
||||
Icon="{Binding Icon}"/>
|
||||
<ghfvs:OcticonButton Width="16"
|
||||
Height="16"
|
||||
Margin="0"
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent"
|
||||
Click="Dismiss_Click"
|
||||
DockPanel.Dock="Right"
|
||||
Foreground="Black"
|
||||
Icon="x"/>
|
||||
|
||||
<markdig:MarkdownViewer x:Name="document"
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Top"
|
||||
DockPanel.Dock="Top"
|
||||
Foreground="Black"
|
||||
Markdown="{Binding Message}"
|
||||
ScrollViewer.CanContentScroll="False"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"/>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
<ghfvs:OcticonImage Width="16"
|
||||
Height="16"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Top"
|
||||
DockPanel.Dock="Left"
|
||||
Icon="{TemplateBinding Icon}"/>
|
||||
<ghfvs:OcticonButton x:Name="PART_CloseButton"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="0"
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent"
|
||||
DockPanel.Dock="Right"
|
||||
Foreground="Black"
|
||||
Icon="x"
|
||||
Visibility="{TemplateBinding ShowCloseButton, Converter={ghfvs:BooleanToVisibilityConverter}}"/>
|
||||
<markdig:MarkdownViewer Margin="8,0"
|
||||
VerticalAlignment="Top"
|
||||
DockPanel.Dock="Top"
|
||||
Foreground="Black"
|
||||
Markdown="{TemplateBinding Message}"
|
||||
ScrollViewer.CanContentScroll="False"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Hidden"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
|
@ -1,114 +0,0 @@
|
|||
using GitHub.UI;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using GitHub.ViewModels;
|
||||
using System.ComponentModel;
|
||||
using GitHub.Services;
|
||||
using GitHub.Extensions;
|
||||
using System.Windows.Input;
|
||||
using GitHub.Primitives;
|
||||
using GitHub.VisualStudio.Helpers;
|
||||
using Colors = System.Windows.Media.Colors;
|
||||
|
||||
namespace GitHub.VisualStudio.UI.Controls
|
||||
{
|
||||
public partial class InfoPanel : UserControl, IInfoPanel, INotifyPropertyChanged, INotifyPropertySource
|
||||
{
|
||||
static SolidColorBrush WarningColorBrush = new SolidColorBrush(Colors.DarkRed);
|
||||
static SolidColorBrush InfoColorBrush = new SolidColorBrush(Colors.Black);
|
||||
|
||||
static readonly DependencyProperty MessageProperty =
|
||||
DependencyProperty.Register(nameof(Message), typeof(string), typeof(InfoPanel), new PropertyMetadata(String.Empty, UpdateMessage));
|
||||
|
||||
static readonly DependencyProperty MessageTypeProperty =
|
||||
DependencyProperty.Register(nameof(MessageType), typeof(MessageType), typeof(InfoPanel), new PropertyMetadata(MessageType.Information, UpdateIcon));
|
||||
|
||||
public string Message
|
||||
{
|
||||
get { return (string)GetValue(MessageProperty); }
|
||||
set { SetValue(MessageProperty, value); }
|
||||
}
|
||||
|
||||
public MessageType MessageType
|
||||
{
|
||||
get { return (MessageType)GetValue(MessageTypeProperty); }
|
||||
set { SetValue(MessageTypeProperty, value); }
|
||||
}
|
||||
|
||||
Octicon icon;
|
||||
public Octicon Icon
|
||||
{
|
||||
get { return icon; }
|
||||
private set { icon = value; RaisePropertyChanged(nameof(Icon)); }
|
||||
}
|
||||
|
||||
Brush iconColor;
|
||||
public Brush IconColor
|
||||
{
|
||||
get { return iconColor; }
|
||||
private set { iconColor = value; RaisePropertyChanged(nameof(IconColor)); }
|
||||
}
|
||||
|
||||
static InfoPanel()
|
||||
{
|
||||
WarningColorBrush.Freeze();
|
||||
InfoColorBrush.Freeze();
|
||||
}
|
||||
|
||||
static IVisualStudioBrowser browser;
|
||||
static IVisualStudioBrowser Browser
|
||||
{
|
||||
get
|
||||
{
|
||||
if (browser == null)
|
||||
browser = Services.GitHubServiceProvider.TryGetService<IVisualStudioBrowser>();
|
||||
return browser;
|
||||
}
|
||||
}
|
||||
|
||||
public InfoPanel()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = this;
|
||||
Icon = Octicon.info;
|
||||
IconColor = InfoColorBrush;
|
||||
}
|
||||
|
||||
void OpenHyperlink(object sender, ExecutedRoutedEventArgs e)
|
||||
{
|
||||
var url = e.Parameter.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
Browser.OpenUrl(new Uri(url));
|
||||
}
|
||||
|
||||
static void UpdateMessage(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (InfoPanel)d;
|
||||
var msg = e.NewValue as string;
|
||||
control.Visibility = String.IsNullOrEmpty(msg) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
static void UpdateIcon(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (InfoPanel)d;
|
||||
control.Icon = (MessageType)e.NewValue == MessageType.Warning ? Octicon.alert : Octicon.info;
|
||||
control.IconColor = control.Icon == Octicon.alert ? WarningColorBrush : InfoColorBrush;
|
||||
}
|
||||
|
||||
void Dismiss_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SetCurrentValue(MessageProperty, String.Empty);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public void RaisePropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -384,6 +384,12 @@
|
|||
</Compile>
|
||||
<Compile Include="Views\ActorAvatarView.cs" />
|
||||
<Compile Include="Views\ContentView.cs" />
|
||||
<Compile Include="Views\Dialog\Clone\RepositoryCloneView.xaml.cs">
|
||||
<DependentUpon>RepositoryCloneView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Dialog\Clone\SelectPageView.xaml.cs">
|
||||
<DependentUpon>SelectPageView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Dialog\ForkRepositoryExecuteView.xaml.cs">
|
||||
<DependentUpon>ForkRepositoryExecuteView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
@ -402,8 +408,8 @@
|
|||
<Compile Include="Views\Dialog\Login2FaView.xaml.cs">
|
||||
<DependentUpon>Login2FaView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Dialog\RepositoryCloneView.xaml.cs">
|
||||
<DependentUpon>RepositoryCloneView.xaml</DependentUpon>
|
||||
<Compile Include="Views\Dialog\LogOutRequiredView.xaml.cs">
|
||||
<DependentUpon>LogOutRequiredView.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Dialog\RepositoryCreationView.xaml.cs">
|
||||
<DependentUpon>RepositoryCreationView.xaml</DependentUpon>
|
||||
|
@ -562,6 +568,14 @@
|
|||
<Generator>MSBuild:Compile</Generator>
|
||||
<CustomToolNamespace>GitHub.VisualStudio.UI</CustomToolNamespace>
|
||||
</Page>
|
||||
<Page Include="Views\Dialog\Clone\RepositoryCloneView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Views\Dialog\Clone\SelectPageView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Dialog\ForkRepositoryExecuteView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
@ -586,9 +600,8 @@
|
|||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Views\Dialog\RepositoryCloneView.xaml">
|
||||
<Page Include="Views\Dialog\LogOutRequiredView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Include="Views\Dialog\RepositoryCreationView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
|
|
@ -256,7 +256,8 @@ namespace GitHub.VisualStudio
|
|||
oauthListener,
|
||||
ApiClientConfiguration.ClientId,
|
||||
ApiClientConfiguration.ClientSecret,
|
||||
ApiClientConfiguration.RequiredScopes,
|
||||
ApiClientConfiguration.MinimumScopes,
|
||||
ApiClientConfiguration.RequestedScopes,
|
||||
ApiClientConfiguration.AuthorizationNote,
|
||||
ApiClientConfiguration.MachineFingerprint);
|
||||
}
|
||||
|
|
|
@ -79,8 +79,8 @@ namespace GitHub.VisualStudio
|
|||
}
|
||||
|
||||
var client = CreateClient(address);
|
||||
var user = await loginManager.Login(address, client, userName, password);
|
||||
var connection = new Connection(address, userName, user);
|
||||
var login = await loginManager.Login(address, client, userName, password);
|
||||
var connection = new Connection(address, login.User, login.Scopes);
|
||||
|
||||
conns.Add(connection);
|
||||
await SaveConnections();
|
||||
|
@ -100,8 +100,8 @@ namespace GitHub.VisualStudio
|
|||
|
||||
var client = CreateClient(address);
|
||||
var oauthClient = new OauthClient(client.Connection);
|
||||
var user = await loginManager.LoginViaOAuth(address, client, oauthClient, OpenBrowser, cancel);
|
||||
var connection = new Connection(address, user.Login, user);
|
||||
var login = await loginManager.LoginViaOAuth(address, client, oauthClient, OpenBrowser, cancel);
|
||||
var connection = new Connection(address, login.User, login.Scopes);
|
||||
|
||||
conns.Add(connection);
|
||||
await SaveConnections();
|
||||
|
@ -121,8 +121,8 @@ namespace GitHub.VisualStudio
|
|||
}
|
||||
|
||||
var client = CreateClient(address);
|
||||
var user = await loginManager.LoginWithToken(address, client, token);
|
||||
var connection = new Connection(address, user.Login, user);
|
||||
var login = await loginManager.LoginWithToken(address, client, token);
|
||||
var connection = new Connection(address, login.User, login.Scopes);
|
||||
|
||||
conns.Add(connection);
|
||||
await SaveConnections();
|
||||
|
@ -156,8 +156,8 @@ namespace GitHub.VisualStudio
|
|||
try
|
||||
{
|
||||
var client = CreateClient(c.HostAddress);
|
||||
var user = await loginManager.LoginFromCache(connection.HostAddress, client);
|
||||
c.SetSuccess(user);
|
||||
var login = await loginManager.LoginFromCache(connection.HostAddress, client);
|
||||
c.SetSuccess(login.User, login.Scopes);
|
||||
await usageTracker.IncrementCounter(x => x.NumberOfLogins);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
@ -194,19 +194,19 @@ namespace GitHub.VisualStudio
|
|||
{
|
||||
foreach (var c in await cache.Load())
|
||||
{
|
||||
var connection = new Connection(c.HostAddress);
|
||||
var connection = new Connection(c.HostAddress, c.UserName);
|
||||
result.Add(connection);
|
||||
}
|
||||
|
||||
foreach (Connection connection in result)
|
||||
{
|
||||
var client = CreateClient(connection.HostAddress);
|
||||
User user = null;
|
||||
LoginResult login = null;
|
||||
|
||||
try
|
||||
{
|
||||
user = await loginManager.LoginFromCache(connection.HostAddress, client);
|
||||
connection.SetSuccess(user);
|
||||
login = await loginManager.LoginFromCache(connection.HostAddress, client);
|
||||
connection.SetSuccess(login.User, login.Scopes);
|
||||
await usageTracker.IncrementCounter(x => x.NumberOfLogins);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
using GitHub.Services;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
|
@ -37,6 +39,34 @@ namespace GitHub.VisualStudio.UI.Services
|
|||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public async Task<object> Show<TViewModel>(
|
||||
TViewModel viewModel,
|
||||
IConnection connection,
|
||||
IEnumerable<string> scopes)
|
||||
where TViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
|
||||
{
|
||||
var result = default(object);
|
||||
|
||||
using (var dialogViewModel = CreateViewModel())
|
||||
using (dialogViewModel.Done.Take(1).Subscribe(x => result = x))
|
||||
{
|
||||
if (!connection.Scopes.Matches(scopes))
|
||||
{
|
||||
await dialogViewModel.StartWithLogout(viewModel, connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
await viewModel.InitializeAsync(connection);
|
||||
dialogViewModel.Start(viewModel);
|
||||
}
|
||||
|
||||
var window = new GitHubDialogWindow(dialogViewModel);
|
||||
window.ShowModal();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<object> ShowWithFirstConnection<TViewModel>(TViewModel viewModel)
|
||||
where TViewModel : IDialogContentViewModel, IConnectionInitializedViewModel
|
||||
{
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Windows.Data;
|
|||
using GitHub.Exports;
|
||||
using GitHub.ViewModels;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
|
||||
namespace GitHub.VisualStudio.Views
|
||||
{
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<UserControl x:Class="GitHub.VisualStudio.Views.Dialog.Clone.RepositoryCloneView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ghfvs="https://github.com/github/VisualStudio"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:GitHub.VisualStudio.Views.Dialog.Clone"
|
||||
mc:Ignorable="d" d:DesignHeight="414" d:DesignWidth="440">
|
||||
<d:DesignData.DataContext>
|
||||
<ghfvs:RepositoryCloneViewModelDesigner/>
|
||||
</d:DesignData.DataContext>
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/SharedDictionary.xaml" />
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI.Reactive;component/SharedDictionary.xaml" />
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/SharedDictionary.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<DockPanel>
|
||||
<ghfvs:InfoPanel Message="{Binding PathError}"/>
|
||||
|
||||
<ghfvs:OcticonCircleButton DockPanel.Dock="Bottom"
|
||||
Margin="12"
|
||||
HorizontalAlignment="Center"
|
||||
Icon="check"
|
||||
IsDefault="True"
|
||||
Command="{Binding Clone}"
|
||||
Content="Clone"
|
||||
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.CloneRepositoryButton}"/>
|
||||
|
||||
<DockPanel DockPanel.Dock="Bottom"
|
||||
Margin="16">
|
||||
<Label DockPanel.Dock="Left">Local Path</Label>
|
||||
<Button DockPanel.Dock="Right"
|
||||
Command="{Binding Browse}"
|
||||
Style="{DynamicResource GitHubBlueLinkButton}"
|
||||
VerticalContentAlignment="Center">
|
||||
Browse
|
||||
</Button>
|
||||
<TextBox Text="{Binding Path, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</DockPanel>
|
||||
|
||||
<TabControl SelectedIndex="{Binding SelectedTabIndex}"
|
||||
Style="{StaticResource LightModalViewTabControl}">
|
||||
<TabItem Header="GitHub.com"
|
||||
Style="{DynamicResource LightModalViewTabItem}"
|
||||
Visibility="{Binding GitHubTab.IsEnabled, Converter={ghfvs:BooleanToVisibilityConverter}}">
|
||||
<local:SelectPageView DataContext="{Binding GitHubTab}"/>
|
||||
</TabItem>
|
||||
<TabItem Header="Enterprise"
|
||||
Style="{DynamicResource LightModalViewTabItem}"
|
||||
Visibility="{Binding EnterpriseTab.IsEnabled, Converter={ghfvs:BooleanToVisibilityConverter}}">
|
||||
<local:SelectPageView DataContext="{Binding EnterpriseTab}"/>
|
||||
</TabItem>
|
||||
<TabItem Header="URL" Style="{DynamicResource LightModalViewTabItem}">
|
||||
<DockPanel DataContext="{Binding UrlTab}"
|
||||
LastChildFill="False"
|
||||
Margin="8">
|
||||
<TextBlock DockPanel.Dock="Top">
|
||||
Repository URL or GitHub username and repository
|
||||
<LineBreak/>
|
||||
(<Run Background="#66d3d3d3" FontFamily="Consolas">hubot/cool-repo</Run>)
|
||||
</TextBlock>
|
||||
<ghfvs:PromptTextBox DockPanel.Dock="Top"
|
||||
Margin="0 4"
|
||||
PromptText="URL or username/repository"
|
||||
Text="{Binding Url, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</DockPanel>
|
||||
</UserControl>
|
|
@ -0,0 +1,17 @@
|
|||
using System.ComponentModel.Composition;
|
||||
using System.Windows.Controls;
|
||||
using GitHub.Exports;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
|
||||
namespace GitHub.VisualStudio.Views.Dialog.Clone
|
||||
{
|
||||
[ExportViewFor(typeof(IRepositoryCloneViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public partial class RepositoryCloneView : UserControl
|
||||
{
|
||||
public RepositoryCloneView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<UserControl x:Class="GitHub.VisualStudio.Views.Dialog.Clone.SelectPageView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:GitHub.VisualStudio.Views.Dialog.Clone"
|
||||
xmlns:ghfvs="https://github.com/github/VisualStudio"
|
||||
mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="400">
|
||||
<d:DesignData.DataContext>
|
||||
<ghfvs:SelectPageViewModelDesigner/>
|
||||
</d:DesignData.DataContext>
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/SharedDictionary.xaml" />
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI.Reactive;component/SharedDictionary.xaml" />
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/SharedDictionary.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<DockPanel>
|
||||
<ghfvs:PromptTextBox DockPanel.Dock="Top"
|
||||
Margin="0,4"
|
||||
PromptText="Filter"
|
||||
Text="{Binding Filter, UpdateSourceTrigger=PropertyChanged, Delay=300}"/>
|
||||
|
||||
<Grid>
|
||||
<ListView BorderThickness="0"
|
||||
ItemsSource="{Binding ItemsView}"
|
||||
SelectedItem="{Binding SelectedItem}"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
VirtualizingPanel.IsVirtualizingWhenGrouping="True">
|
||||
<ListView.GroupStyle>
|
||||
<GroupStyle>
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock FontWeight="Bold"
|
||||
Margin="0 4"
|
||||
Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ListView.GroupStyle>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ghfvs:OcticonImage Icon="{Binding Icon}"/>
|
||||
<TextBlock Margin="4" Text="{Binding Caption}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<ghfvs:Spinner Visibility="{Binding IsLoading, Converter={ghfvs:BooleanToVisibilityConverter}}"
|
||||
Width="32"
|
||||
Height="32"/>
|
||||
<TextBlock Text="{Binding Error.Message}"
|
||||
Margin="4"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</UserControl>
|
|
@ -0,0 +1,23 @@
|
|||
using System.ComponentModel.Composition;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using GitHub.Exports;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
|
||||
namespace GitHub.VisualStudio.Views.Dialog.Clone
|
||||
{
|
||||
[ExportViewFor(typeof(IRepositorySelectViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public partial class SelectPageView : UserControl
|
||||
{
|
||||
public SelectPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
base.OnPreviewMouseDown(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<UserControl x:Class="GitHub.VisualStudio.Views.Dialog.LogOutRequiredView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ghfvs="https://github.com/github/VisualStudio"
|
||||
xmlns:prop="clr-namespace:GitHub.VisualStudio.UI;assembly=GitHub.VisualStudio.UI"
|
||||
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
|
||||
|
||||
<Control.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.VisualStudio.UI;component/SharedDictionary.xaml" />
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/SharedDictionary.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Control.Resources>
|
||||
|
||||
<DockPanel Margin="10">
|
||||
<ghfvs:OcticonImage DockPanel.Dock="Top"
|
||||
Icon="mark_github"
|
||||
Foreground="{DynamicResource GitHubVsWindowText}"
|
||||
Margin="0,5"
|
||||
Width="48"
|
||||
Height="48" />
|
||||
|
||||
<Label DockPanel.Dock="Top"
|
||||
Foreground="{DynamicResource GitHubVsWindowText}"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="16"
|
||||
Content="You need to sign out and back in." />
|
||||
|
||||
<ghfvs:OcticonCircleButton DockPanel.Dock="Bottom"
|
||||
Command="{Binding LogOut}"
|
||||
Content="Sign out"
|
||||
Icon="check"
|
||||
IsDefault="True"
|
||||
Margin="0 16"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
TextAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Text="We're sorry, but the operation you requested requires more permissions than we currently have. Please sign out and back in."/>
|
||||
</DockPanel>
|
||||
</UserControl>
|
|
@ -0,0 +1,17 @@
|
|||
using System.ComponentModel.Composition;
|
||||
using System.Windows.Controls;
|
||||
using GitHub.Exports;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
|
||||
namespace GitHub.VisualStudio.Views.Dialog
|
||||
{
|
||||
[ExportViewFor(typeof(ILogOutRequiredViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public partial class LogOutRequiredView : UserControl
|
||||
{
|
||||
public LogOutRequiredView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -129,7 +129,7 @@
|
|||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<TabItem x:Name="enterpriseTab" Header="GitHub Enterprise" Margin="10,0,-10,0" AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.SignInEnterpriseHostTabItem}">
|
||||
<TabItem x:Name="enterpriseTab" Header="GitHub Enterprise" AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.SignInEnterpriseHostTabItem}">
|
||||
<DockPanel Style="{StaticResource TabDockPanel}">
|
||||
<StackPanel DockPanel.Dock="Bottom">
|
||||
<TextBlock TextWrapping="Wrap" HorizontalAlignment="Center" Margin="0" Text="{x:Static prop:Resources.dontHaveGitHubEnterpriseText}" AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.DontHaveEnterpriseTextBlock}" >
|
||||
|
|
|
@ -1,327 +0,0 @@
|
|||
<local:GenericRepositoryCloneView x:Class="GitHub.VisualStudio.Views.Dialog.RepositoryCloneView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ghfvs="https://github.com/github/VisualStudio"
|
||||
xmlns:local="clr-namespace:GitHub.VisualStudio.Views.Dialog"
|
||||
xmlns:prop="clr-namespace:GitHub.VisualStudio.UI;assembly=GitHub.VisualStudio.UI"
|
||||
d:DesignHeight="440"
|
||||
d:DesignWidth="414"
|
||||
Background="Transparent"
|
||||
mc:Ignorable="d"
|
||||
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.RepositoryCloneControlCustom}" >
|
||||
<d:DesignProperties.DataContext>
|
||||
<Binding>
|
||||
<Binding.Source>
|
||||
<ghfvs:RepositoryCloneViewModelDesigner />
|
||||
</Binding.Source>
|
||||
</Binding>
|
||||
</d:DesignProperties.DataContext>
|
||||
|
||||
<Control.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI;component/SharedDictionary.xaml" />
|
||||
<ghfvs:SharedDictionaryManager Source="pack://application:,,,/GitHub.UI.Reactive;component/SharedDictionary.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Control.Resources>
|
||||
|
||||
<DockPanel LastChildFill="True">
|
||||
<DockPanel.Resources>
|
||||
<Style x:Key="repositoryBorderStyle" TargetType="Border">
|
||||
<Setter Property="BorderBrush" Value="#EAEAEA" />
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="expanderDownHeaderStyle" TargetType="{x:Type ToggleButton}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type ToggleButton}">
|
||||
<Border Padding="{TemplateBinding Padding}" Style="{StaticResource repositoryBorderStyle}">
|
||||
<StackPanel Background="#F8F8F8" Orientation="Horizontal">
|
||||
<ghfvs:OcticonImage x:Name="arrow"
|
||||
Height="10"
|
||||
Margin="5,0,0,0"
|
||||
Foreground="Black"
|
||||
Icon="triangle_right" />
|
||||
<ContentPresenter Margin="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
RecognizesAccessKey="True"
|
||||
SnapsToDevicePixels="True" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="arrow" Property="Icon" Value="triangle_down" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="expanderHeaderFocusVisual">
|
||||
<Setter Property="Control.Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate>
|
||||
<Border>
|
||||
<Rectangle Margin="0"
|
||||
SnapsToDevicePixels="true"
|
||||
Stroke="Black"
|
||||
StrokeDashArray="1 2"
|
||||
StrokeThickness="1" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="cloneGroupExpander" TargetType="{x:Type Expander}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type Expander}">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="3"
|
||||
SnapsToDevicePixels="true">
|
||||
<DockPanel>
|
||||
<ToggleButton x:Name="HeaderSite"
|
||||
MinWidth="0"
|
||||
MinHeight="0"
|
||||
Margin="0"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Header}"
|
||||
ContentTemplate="{TemplateBinding HeaderTemplate}"
|
||||
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
|
||||
DockPanel.Dock="Top"
|
||||
FocusVisualStyle="{StaticResource expanderHeaderFocusVisual}"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontStretch="{TemplateBinding FontStretch}"
|
||||
FontStyle="{TemplateBinding FontStyle}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Style="{StaticResource expanderDownHeaderStyle}" />
|
||||
<ContentPresenter x:Name="ExpandSite"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
DockPanel.Dock="Bottom"
|
||||
Focusable="false"
|
||||
Visibility="Collapsed" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsExpanded" Value="true">
|
||||
<Setter TargetName="ExpandSite" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</DockPanel.Resources>
|
||||
|
||||
<ghfvs:FilterTextBox x:Name="filterText"
|
||||
Margin="10"
|
||||
DockPanel.Dock="Top"
|
||||
PromptText="{x:Static prop:Resources.filterTextPromptText}"
|
||||
Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding FilterTextIsEnabled, Mode=OneWay}"
|
||||
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.SearchRepositoryTextBox}" />
|
||||
|
||||
<ghfvs:GitHubProgressBar DockPanel.Dock="Top"
|
||||
Foreground="{DynamicResource GitHubAccentBrush}"
|
||||
IsIndeterminate="True"
|
||||
Style="{DynamicResource GitHubProgressBar}"
|
||||
Visibility="{Binding IsBusy, Converter={ghfvs:BooleanToHiddenVisibilityConverter}}"/>
|
||||
|
||||
<StackPanel x:Name="loadingFailedPanel"
|
||||
Margin="0,4,0,4"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
DockPanel.Dock="Top">
|
||||
<ghfvs:ErrorMessageDisplay x:Name="loadingFailedMessage"
|
||||
Margin="8,0"
|
||||
Icon="stop"
|
||||
Message="{x:Static prop:Resources.loadingFailedMessageMessage}"
|
||||
Visibility="{Binding LoadingFailed, Mode=OneWay, Converter={ghfvs:BooleanToVisibilityConverter}}">
|
||||
<TextBlock Text="{x:Static prop:Resources.loadingFailedMessageContent}" TextWrapping="Wrap" />
|
||||
</ghfvs:ErrorMessageDisplay>
|
||||
</StackPanel>
|
||||
|
||||
<ghfvs:OcticonCircleButton x:Name="cloneButton"
|
||||
Margin="12"
|
||||
HorizontalAlignment="Center"
|
||||
DockPanel.Dock="Bottom"
|
||||
Icon="check"
|
||||
IsDefault="True"
|
||||
Command="{Binding CloneCommand}"
|
||||
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.CloneRepositoryButton}" >
|
||||
<TextBlock Text="{x:Static prop:Resources.CloneLink}" />
|
||||
</ghfvs:OcticonCircleButton>
|
||||
|
||||
<Border DockPanel.Dock="Bottom" Style="{StaticResource repositoryBorderStyle}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ghfvs:PromptTextBox x:Name="clonePath"
|
||||
Grid.Column="1"
|
||||
Margin="0,2"
|
||||
Text="{Binding BaseRepositoryPath, UpdateSourceTrigger=PropertyChanged}"
|
||||
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.CreateRepositoryLocalPathTextBox}"
|
||||
/>
|
||||
<Button x:Name="browsePathButton"
|
||||
Grid.Column="2"
|
||||
Margin="4,0,4,0"
|
||||
VerticalContentAlignment="Center"
|
||||
Content="{x:Static prop:Resources.browsePathButtonContent}"
|
||||
Padding="0"
|
||||
Style="{StaticResource GitHubBlueLinkButton}"
|
||||
Command="{Binding BrowseForDirectory}"
|
||||
AutomationProperties.AutomationId="{x:Static ghfvs:AutomationIDs.CloneRepositoryLocalPathBrowsePathButton}"
|
||||
/>
|
||||
<Label Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Margin="4,0"
|
||||
Content="{x:Static prop:Resources.pathText}"
|
||||
Target="{Binding ElementName=clonePath}" />
|
||||
<ghfvs:ValidationMessage x:Name="pathValidationMessage"
|
||||
Grid.Row="4"
|
||||
Grid.Column="1"
|
||||
ValidatesControl="{Binding ElementName=clonePath}"
|
||||
ReactiveValidator="{Binding BaseRepositoryPathValidator, Mode=OneWay}"
|
||||
/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="repositoriesListPane"
|
||||
Margin="0"
|
||||
BorderBrush="#EAEAEA"
|
||||
BorderThickness="0,1"
|
||||
FocusManager.IsFocusScope="True"
|
||||
FocusVisualStyle="{x:Null}">
|
||||
<Border.Resources>
|
||||
<Style BasedOn="{StaticResource GitHubTextBlock}" TargetType="{x:Type ghfvs:TrimmedTextBlock}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsTextTrimmed" Value="True">
|
||||
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Text}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style x:Key="cloneRepoHeaderStyle" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource GHTextBrush}" />
|
||||
<Setter Property="Margin" Value="0,6,12,6" />
|
||||
</Style>
|
||||
</Border.Resources>
|
||||
<Grid Margin="0">
|
||||
<ListBox x:Name="repositoryList"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ItemsSource="{Binding Path=RepositoriesView, RelativeSource={RelativeSource AncestorType={x:Type local:RepositoryCloneView}}}"
|
||||
SelectedItem="{Binding SelectedRepository}"
|
||||
Style="{DynamicResource LightListBox}">
|
||||
<ListBox.GroupStyle>
|
||||
<GroupStyle HidesIfEmpty="true">
|
||||
<GroupStyle.ContainerStyle>
|
||||
<Style TargetType="{x:Type GroupItem}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type GroupItem}">
|
||||
<Expander IsExpanded="{Binding Name.IsExpanded}" Style="{StaticResource cloneGroupExpander}">
|
||||
<Expander.Header>
|
||||
<Border Style="{StaticResource repositoryBorderStyle}">
|
||||
<StackPanel Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Image x:Name="avatar"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="0,0,5,0"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Source="{Binding Items[0].Owner.Avatar}" />
|
||||
<TextBlock Style="{StaticResource cloneRepoHeaderStyle}" Text="{Binding Path=Name.Header}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Expander.Header>
|
||||
<ItemsPresenter Margin="0" />
|
||||
</Expander>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</GroupStyle.ContainerStyle>
|
||||
</GroupStyle>
|
||||
</ListBox.GroupStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Style="{StaticResource repositoryBorderStyle}">
|
||||
<StackPanel Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<ghfvs:OcticonImage x:Name="iconPath"
|
||||
Width="16"
|
||||
Height="16"
|
||||
Margin="32,0,6,0"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#D0D0D0"
|
||||
Icon="{Binding Icon}" />
|
||||
|
||||
<ghfvs:TrimmedTextBlock x:Name="label"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#666"
|
||||
Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<DataTemplate.Triggers>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="True" />
|
||||
<Condition Binding="{Binding Path=(Selector.IsSelectionActive), RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<MultiDataTrigger.Setters>
|
||||
<Setter TargetName="iconPath" Property="Foreground" Value="White" />
|
||||
<Setter TargetName="label" Property="Foreground" Value="White" />
|
||||
</MultiDataTrigger.Setters>
|
||||
</MultiDataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<StackPanel x:Name="noRepositoriesMessage"
|
||||
Margin="0,-70,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding NoRepositoriesFound, Mode=OneWay, Converter={ghfvs:BooleanToVisibilityConverter}}">
|
||||
<TextBlock HorizontalAlignment="Center"
|
||||
Style="{DynamicResource GitHubH2TextBlock}"
|
||||
Text="{x:Static prop:Resources.noRepositoriesMessageText}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</local:GenericRepositoryCloneView>
|
|
@ -1,126 +0,0 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using GitHub.Exports;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Models;
|
||||
using GitHub.UI;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace GitHub.VisualStudio.Views.Dialog
|
||||
{
|
||||
public class GenericRepositoryCloneView : ViewBase<IRepositoryCloneViewModel, RepositoryCloneView>
|
||||
{}
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for CloneRepoControl.xaml
|
||||
/// </summary>
|
||||
[ExportViewFor(typeof(IRepositoryCloneViewModel))]
|
||||
[PartCreationPolicy(CreationPolicy.NonShared)]
|
||||
public partial class RepositoryCloneView : GenericRepositoryCloneView
|
||||
{
|
||||
readonly Dictionary<string, RepositoryGroup> groups = new Dictionary<string, RepositoryGroup>();
|
||||
|
||||
static readonly DependencyPropertyKey RepositoriesViewPropertyKey =
|
||||
DependencyProperty.RegisterReadOnly(
|
||||
nameof(RepositoriesView),
|
||||
typeof(ICollectionView),
|
||||
typeof(RepositoryCloneView),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public static readonly DependencyProperty RepositoriesViewProperty = RepositoriesViewPropertyKey.DependencyProperty;
|
||||
|
||||
public RepositoryCloneView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
//d(repositoryList.Events().MouseDoubleClick.InvokeCommand(this, x => x.ViewModel.CloneCommand));
|
||||
});
|
||||
|
||||
IsVisibleChanged += (s, e) =>
|
||||
{
|
||||
if (IsVisible)
|
||||
this.TryMoveFocus(FocusNavigationDirection.First).Subscribe();
|
||||
};
|
||||
|
||||
this.WhenAnyValue(x => x.ViewModel.Repositories, CreateRepositoryListCollectionView).Subscribe(x => RepositoriesView = x);
|
||||
}
|
||||
|
||||
public ICollectionView RepositoriesView
|
||||
{
|
||||
get { return (ICollectionView)GetValue(RepositoriesViewProperty); }
|
||||
private set { SetValue(RepositoriesViewPropertyKey, value); }
|
||||
}
|
||||
|
||||
ListCollectionView CreateRepositoryListCollectionView(IEnumerable<IRemoteRepositoryModel> repositories)
|
||||
{
|
||||
if (repositories == null)
|
||||
return null;
|
||||
|
||||
var view = new ListCollectionView((IList)repositories);
|
||||
view.GroupDescriptions.Add(new RepositoryGroupDescription(this));
|
||||
return view;
|
||||
}
|
||||
|
||||
class RepositoryGroupDescription : GroupDescription
|
||||
{
|
||||
readonly RepositoryCloneView owner;
|
||||
|
||||
public RepositoryGroupDescription(RepositoryCloneView owner)
|
||||
{
|
||||
Guard.ArgumentNotNull(owner, nameof(owner));
|
||||
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public override object GroupNameFromItem(object item, int level, System.Globalization.CultureInfo culture)
|
||||
{
|
||||
var repo = item as IRemoteRepositoryModel;
|
||||
var name = repo?.Owner ?? string.Empty;
|
||||
RepositoryGroup group;
|
||||
|
||||
if (!owner.groups.TryGetValue(name, out group))
|
||||
{
|
||||
group = new RepositoryGroup(name, owner.groups.Count == 0);
|
||||
|
||||
if (owner.groups.Count == 1)
|
||||
owner.groups.Values.First().IsExpanded = false;
|
||||
owner.groups.Add(name, group);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
||||
class RepositoryGroup : ReactiveObject
|
||||
{
|
||||
bool isExpanded;
|
||||
|
||||
public RepositoryGroup(string header, bool isExpanded)
|
||||
{
|
||||
Guard.ArgumentNotEmptyString(header, nameof(header));
|
||||
|
||||
Header = header;
|
||||
this.isExpanded = isExpanded;
|
||||
}
|
||||
|
||||
public string Header { get; }
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get { return isExpanded; }
|
||||
set { this.RaiseAndSetIfChanged(ref isExpanded, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,7 +41,6 @@
|
|||
<DockPanel>
|
||||
<ghfvs:InfoPanel Name="infoPanel"
|
||||
DockPanel.Dock="Top"
|
||||
MessageType="{Binding MessageType}"
|
||||
Message="{Binding Message}"
|
||||
VerticalAlignment="Top"/>
|
||||
<StackPanel DockPanel.Dock="Top"
|
||||
|
|
|
@ -33,9 +33,9 @@ namespace GitHub.VisualStudio.Views.GitHubPane
|
|||
.Subscribe(n =>
|
||||
{
|
||||
if (n.Type == Notification.NotificationType.Error || n.Type == Notification.NotificationType.Warning)
|
||||
infoPanel.MessageType = MessageType.Warning;
|
||||
infoPanel.Icon = Octicon.alert;
|
||||
else
|
||||
infoPanel.MessageType = MessageType.Information;
|
||||
infoPanel.Icon = Octicon.info;
|
||||
infoPanel.Message = n.Message;
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ public class LoginManagerTests
|
|||
var tfa = new Lazy<ITwoFactorChallengeHandler>(() => Substitute.For<ITwoFactorChallengeHandler>());
|
||||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
await target.Login(host, client, "foo", "bar");
|
||||
|
||||
await keychain.Received().Save("foo", "123abc", host);
|
||||
|
@ -45,10 +45,10 @@ public class LoginManagerTests
|
|||
var tfa = new Lazy<ITwoFactorChallengeHandler>(() => Substitute.For<ITwoFactorChallengeHandler>());
|
||||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
var result = await target.Login(host, client, "foo", "bar");
|
||||
|
||||
Assert.That(user, Is.SameAs(result));
|
||||
Assert.That(user, Is.SameAs(result.User));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -69,7 +69,7 @@ public class LoginManagerTests
|
|||
var tfa = new Lazy<ITwoFactorChallengeHandler>(() => Substitute.For<ITwoFactorChallengeHandler>());
|
||||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
var result = await target.Login(host, client, "foo", "bar");
|
||||
|
||||
await client.Authorization.Received(2).GetOrCreateApplicationAuthentication("id", "secret", Arg.Any<NewAuthorization>());
|
||||
|
@ -93,7 +93,7 @@ public class LoginManagerTests
|
|||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
tfa.Value.HandleTwoFactorException(exception).Returns(new TwoFactorChallengeResult("123456"));
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
await target.Login(host, client, "foo", "bar");
|
||||
|
||||
await client.Authorization.Received().GetOrCreateApplicationAuthentication(
|
||||
|
@ -123,7 +123,7 @@ public class LoginManagerTests
|
|||
new TwoFactorChallengeResult("111111"),
|
||||
new TwoFactorChallengeResult("123456"));
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
await target.Login(host, client, "foo", "bar");
|
||||
|
||||
await client.Authorization.Received(1).GetOrCreateApplicationAuthentication(
|
||||
|
@ -159,7 +159,7 @@ public class LoginManagerTests
|
|||
new TwoFactorChallengeResult("111111"),
|
||||
new TwoFactorChallengeResult("123456"));
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
Assert.ThrowsAsync<LoginAttemptsExceededException>(async () => await target.Login(host, client, "foo", "bar"));
|
||||
|
||||
await client.Authorization.Received(1).GetOrCreateApplicationAuthentication(
|
||||
|
@ -190,7 +190,7 @@ public class LoginManagerTests
|
|||
TwoFactorChallengeResult.RequestResendCode,
|
||||
new TwoFactorChallengeResult("123456"));
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
await target.Login(host, client, "foo", "bar");
|
||||
|
||||
await client.Authorization.Received(2).GetOrCreateApplicationAuthentication("id", "secret", Arg.Any<NewAuthorization>());
|
||||
|
@ -213,7 +213,7 @@ public class LoginManagerTests
|
|||
var tfa = new Lazy<ITwoFactorChallengeHandler>(() => Substitute.For<ITwoFactorChallengeHandler>());
|
||||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
await target.Login(enterprise, client, "foo", "bar");
|
||||
|
||||
await keychain.Received().Save("foo", "bar", enterprise);
|
||||
|
@ -232,7 +232,7 @@ public class LoginManagerTests
|
|||
var tfa = new Lazy<ITwoFactorChallengeHandler>(() => Substitute.For<ITwoFactorChallengeHandler>());
|
||||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
Assert.ThrowsAsync<AuthorizationException>(async () => await target.Login(enterprise, client, "foo", "bar"));
|
||||
|
||||
await keychain.Received().Delete(enterprise);
|
||||
|
@ -251,7 +251,7 @@ public class LoginManagerTests
|
|||
var tfa = new Lazy<ITwoFactorChallengeHandler>(() => Substitute.For<ITwoFactorChallengeHandler>());
|
||||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () => await target.Login(host, client, "foo", "bar"));
|
||||
|
||||
|
||||
|
@ -276,7 +276,7 @@ public class LoginManagerTests
|
|||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
tfa.Value.HandleTwoFactorException(exception).Returns(new TwoFactorChallengeResult("123456"));
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () => await target.Login(host, client, "foo", "bar"));
|
||||
|
||||
await keychain.Received().Delete(host);
|
||||
|
@ -293,7 +293,7 @@ public class LoginManagerTests
|
|||
var tfa = new Lazy<ITwoFactorChallengeHandler>(() => Substitute.For<ITwoFactorChallengeHandler>());
|
||||
var oauthListener = Substitute.For<IOAuthCallbackListener>();
|
||||
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes);
|
||||
var target = new LoginManager(keychain, tfa, oauthListener, "id", "secret", scopes, scopes);
|
||||
|
||||
Assert.ThrowsAsync<IncorrectScopesException>(() => target.Login(host, client, "foo", "bar"));
|
||||
}
|
||||
|
@ -311,39 +311,4 @@ public class LoginManagerTests
|
|||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public class TheScopesMatchMethod
|
||||
{
|
||||
[Test]
|
||||
public void ReturnsFalseWhenMissingScopes()
|
||||
{
|
||||
var received = new[] { "user", "repo", "write:public_key" };
|
||||
|
||||
Assert.False(LoginManager.ScopesMatch(scopes, received));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReturnsTrueWhenScopesEqual()
|
||||
{
|
||||
var received = new[] { "user", "repo", "gist", "write:public_key" };
|
||||
|
||||
Assert.True(LoginManager.ScopesMatch(scopes, received));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReturnsTrueWhenExtraScopesReturned()
|
||||
{
|
||||
var received = new[] { "user", "repo", "gist", "foo", "write:public_key" };
|
||||
|
||||
Assert.True(LoginManager.ScopesMatch(scopes, received));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReturnsTrueWhenAdminScopeReturnedInsteadOfWrite()
|
||||
{
|
||||
var received = new[] { "user", "repo", "gist", "foo", "admin:public_key" };
|
||||
|
||||
Assert.True(LoginManager.ScopesMatch(scopes, received));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ using GitHub.Services;
|
|||
using System.Linq.Expressions;
|
||||
using System;
|
||||
using GitHub.Models;
|
||||
using GitHub.Api;
|
||||
|
||||
public class RepositoryCloneServiceTests
|
||||
{
|
||||
|
@ -32,8 +33,9 @@ public class RepositoryCloneServiceTests
|
|||
var serviceProvider = Substitutes.ServiceProvider;
|
||||
var operatingSystem = serviceProvider.GetOperatingSystem();
|
||||
var vsGitServices = serviceProvider.GetVSGitServices();
|
||||
var graphqlFactory = Substitute.For<IGraphQLClientFactory>();
|
||||
var usageTracker = Substitute.For<IUsageTracker>();
|
||||
var cloneService = new RepositoryCloneService(operatingSystem, vsGitServices, usageTracker);
|
||||
var cloneService = new RepositoryCloneService(operatingSystem, vsGitServices, graphqlFactory, usageTracker);
|
||||
|
||||
await cloneService.CloneRepository("https://github.com/foo/bar", "bar", @"c:\dev");
|
||||
var model = UsageModel.Create(Guid.NewGuid());
|
||||
|
|
|
@ -9,6 +9,7 @@ using System;
|
|||
using System.ComponentModel.Composition;
|
||||
using System.ComponentModel.Composition.Hosting;
|
||||
using GitHub.Factories;
|
||||
using GitHub.Api;
|
||||
|
||||
namespace UnitTests
|
||||
{
|
||||
|
@ -110,7 +111,7 @@ namespace UnitTests
|
|||
|
||||
var os = OperatingSystem;
|
||||
var vsgit = IVSGitServices;
|
||||
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<IUsageTracker>());
|
||||
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<IGraphQLClientFactory>(), Substitute.For<IUsageTracker>());
|
||||
var create = creationService ?? new RepositoryCreationService(clone);
|
||||
avatarProvider = avatarProvider ?? Substitute.For<IAvatarProvider>();
|
||||
//ret.GetService(typeof(IGitRepositoriesExt)).Returns(IGitRepositoriesExt);
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Extensions;
|
||||
using GitHub.Models;
|
||||
using GitHub.Primitives;
|
||||
using GitHub.Services;
|
||||
using GitHub.ViewModels.Dialog.Clone;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Rothko;
|
||||
|
||||
namespace GitHub.App.UnitTests.ViewModels.Dialog.Clone
|
||||
{
|
||||
public class RepositoryCloneViewModelTests
|
||||
{
|
||||
[Test]
|
||||
public async Task GitHubPage_Is_Initialized()
|
||||
{
|
||||
var cm = CreateConnectionManager("https://github.com");
|
||||
var target = CreateTarget(connectionManager: cm);
|
||||
|
||||
await target.InitializeAsync(null);
|
||||
|
||||
target.GitHubTab.Received(1).Initialize(cm.Connections[0]);
|
||||
target.EnterpriseTab.DidNotReceiveWithAnyArgs().Initialize(null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task EnterprisePage_Is_Initialized()
|
||||
{
|
||||
var cm = CreateConnectionManager("https://enterprise.com");
|
||||
var target = CreateTarget(connectionManager: cm);
|
||||
|
||||
await target.InitializeAsync(null);
|
||||
|
||||
target.GitHubTab.DidNotReceiveWithAnyArgs().Initialize(null);
|
||||
target.EnterpriseTab.Received(1).Initialize(cm.Connections[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GitHub_And_Enterprise_Pages_Are_Initialized()
|
||||
{
|
||||
var cm = CreateConnectionManager("https://github.com", "https://enterprise.com");
|
||||
var target = CreateTarget(connectionManager: cm);
|
||||
|
||||
await target.InitializeAsync(null);
|
||||
|
||||
target.GitHubTab.Received(1).Initialize(cm.Connections[0]);
|
||||
target.EnterpriseTab.Received(1).Initialize(cm.Connections[1]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GitHubPage_Is_Loaded()
|
||||
{
|
||||
var cm = CreateConnectionManager("https://github.com", "https://enterprise.com");
|
||||
var target = CreateTarget(connectionManager: cm);
|
||||
|
||||
await target.InitializeAsync(null);
|
||||
|
||||
await target.GitHubTab.Received(1).Activate();
|
||||
await target.EnterpriseTab.DidNotReceive().Activate();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Enterprise_Is_Loaded()
|
||||
{
|
||||
var cm = CreateConnectionManager("https://github.com", "https://enterprise.com");
|
||||
var target = CreateTarget(connectionManager: cm);
|
||||
|
||||
await target.InitializeAsync(cm.Connections[1]);
|
||||
|
||||
await target.GitHubTab.DidNotReceive().Activate();
|
||||
await target.EnterpriseTab.Received(1).Activate();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Switching_To_GitHubPage_Loads_It()
|
||||
{
|
||||
var cm = CreateConnectionManager("https://github.com", "https://enterprise.com");
|
||||
var target = CreateTarget(connectionManager: cm);
|
||||
|
||||
await target.InitializeAsync(cm.Connections[1]);
|
||||
await target.GitHubTab.DidNotReceive().Activate();
|
||||
|
||||
target.SelectedTabIndex = 0;
|
||||
|
||||
await target.GitHubTab.Received(1).Activate();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Switching_To_EnterprisePage_Loads_It()
|
||||
{
|
||||
var cm = CreateConnectionManager("https://github.com", "https://enterprise.com");
|
||||
var target = CreateTarget(connectionManager: cm);
|
||||
|
||||
await target.InitializeAsync(cm.Connections[0]);
|
||||
await target.EnterpriseTab.DidNotReceive().Activate();
|
||||
|
||||
target.SelectedTabIndex = 1;
|
||||
|
||||
await target.EnterpriseTab.Received(1).Activate();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Path_Is_Initialized()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
|
||||
Assert.That(target.Path, Is.EqualTo("d:\\efault\\path"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Repository_Name_Is_Appended_To_Base_Path()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var repository = Substitute.For<IRepositoryModel>();
|
||||
|
||||
repository.Name.Returns("repo");
|
||||
SetRepository(target.GitHubTab, repository);
|
||||
|
||||
Assert.That(target.Path, Is.EqualTo("d:\\efault\\path\\repo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PathError_Is_Not_Set_When_No_Repository_Selected()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
|
||||
target.Path = "d:\\exists";
|
||||
|
||||
Assert.That(target.PathError, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PathError_Is_Set_For_Existing_Destination()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var repository = Substitute.For<IRepositoryModel>();
|
||||
|
||||
repository.Name.Returns("repo");
|
||||
SetRepository(target.GitHubTab, repository);
|
||||
target.Path = "d:\\exists";
|
||||
|
||||
Assert.That(target.PathError, Is.EqualTo(Resources.DestinationAlreadyExists));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Repository_Name_Replaces_Last_Part_Of_Non_Base_Path()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
var repository = Substitute.For<IRepositoryModel>();
|
||||
|
||||
target.Path = "d:\\efault\\foo";
|
||||
repository.Name.Returns("repo");
|
||||
SetRepository(target.GitHubTab, repository);
|
||||
|
||||
Assert.That(target.Path, Is.EqualTo("d:\\efault\\repo"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Clone_Is_Initially_Disabled()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
|
||||
await target.InitializeAsync(null);
|
||||
|
||||
Assert.That(target.Clone.CanExecute(null), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Clone_Is_Enabled_When_Repository_Selected()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
|
||||
await target.InitializeAsync(null);
|
||||
|
||||
SetRepository(target.GitHubTab, Substitute.For<IRepositoryModel>());
|
||||
|
||||
Assert.That(target.Clone.CanExecute(null), Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Clone_Is_Disabled_When_Has_PathError()
|
||||
{
|
||||
var target = CreateTarget();
|
||||
|
||||
await target.InitializeAsync(null);
|
||||
|
||||
SetRepository(target.GitHubTab, Substitute.For<IRepositoryModel>());
|
||||
Assert.That(target.Clone.CanExecute(null), Is.True);
|
||||
|
||||
target.Path = "d:\\exists";
|
||||
|
||||
Assert.That(target.Clone.CanExecute(null), Is.False);
|
||||
}
|
||||
|
||||
static void SetRepository(IRepositoryCloneTabViewModel vm, IRepositoryModel repository)
|
||||
{
|
||||
vm.Repository.Returns(repository);
|
||||
vm.PropertyChanged += Raise.Event<PropertyChangedEventHandler>(
|
||||
vm,
|
||||
new PropertyChangedEventArgs(nameof(vm.Repository)));
|
||||
}
|
||||
|
||||
static IConnectionManager CreateConnectionManager(params string[] addresses)
|
||||
{
|
||||
var result = Substitute.For<IConnectionManager>();
|
||||
var connections = new ObservableCollectionEx<IConnection>();
|
||||
|
||||
result.Connections.Returns(connections);
|
||||
result.GetLoadedConnections().Returns(connections);
|
||||
result.GetConnection(null).ReturnsForAnyArgs(default(IConnection));
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
var connection = Substitute.For<IConnection>();
|
||||
var hostAddress = HostAddress.Create(address);
|
||||
connection.HostAddress.Returns(hostAddress);
|
||||
connection.IsLoggedIn.Returns(true);
|
||||
connections.Add(connection);
|
||||
result.GetConnection(hostAddress).Returns(connection);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static IRepositorySelectViewModel CreateSelectViewModel()
|
||||
{
|
||||
var result = Substitute.For<IRepositorySelectViewModel>();
|
||||
result.Repository.Returns((IRepositoryModel)null);
|
||||
result.WhenForAnyArgs(x => x.Initialize(null)).Do(_ => result.IsEnabled.Returns(true));
|
||||
return result;
|
||||
}
|
||||
|
||||
static IRepositoryCloneService CreateRepositoryCloneService()
|
||||
{
|
||||
var result = Substitute.For<IRepositoryCloneService>();
|
||||
result.DefaultClonePath.Returns("d:\\efault\\path");
|
||||
result.DestinationExists("d:\\exists").Returns(true);
|
||||
return result;
|
||||
}
|
||||
|
||||
static RepositoryCloneViewModel CreateTarget(
|
||||
IOperatingSystem os = null,
|
||||
IConnectionManager connectionManager = null,
|
||||
IRepositoryCloneService service = null,
|
||||
IRepositorySelectViewModel gitHubTab = null,
|
||||
IRepositorySelectViewModel enterpriseTab = null,
|
||||
IRepositoryUrlViewModel urlTab = null)
|
||||
{
|
||||
os = os ?? Substitute.For<IOperatingSystem>();
|
||||
connectionManager = connectionManager ?? CreateConnectionManager("https://github.com");
|
||||
service = service ?? CreateRepositoryCloneService();
|
||||
gitHubTab = gitHubTab ?? CreateSelectViewModel();
|
||||
enterpriseTab = enterpriseTab ?? CreateSelectViewModel();
|
||||
urlTab = urlTab ?? Substitute.For<IRepositoryUrlViewModel>();
|
||||
|
||||
return new RepositoryCloneViewModel(
|
||||
os,
|
||||
connectionManager,
|
||||
service,
|
||||
gitHubTab,
|
||||
enterpriseTab,
|
||||
urlTab);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ namespace UnitTests.GitHub.App.ViewModels.Dialog
|
|||
var target = CreateTarget();
|
||||
var content = Substitute.For<ITestViewModel>();
|
||||
|
||||
await target.StartWithConnection(content);
|
||||
target.StartWithConnection(content).Forget();
|
||||
|
||||
Assert.That(target.Content, Is.InstanceOf<ILoginViewModel>());
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ namespace UnitTests.GitHub.App.ViewModels.Dialog
|
|||
var target = CreateTarget(connectionManager);
|
||||
var content = Substitute.For<ITestViewModel>();
|
||||
|
||||
await target.StartWithConnection(content);
|
||||
target.StartWithConnection(content).Forget();
|
||||
|
||||
Assert.That(content, Is.SameAs(target.Content));
|
||||
await content.Received(1).InitializeAsync(connectionManager.Connections[0]);
|
||||
|
@ -75,13 +75,13 @@ namespace UnitTests.GitHub.App.ViewModels.Dialog
|
|||
{
|
||||
var target = CreateTarget();
|
||||
var content = Substitute.For<ITestViewModel>();
|
||||
|
||||
await target.StartWithConnection(content);
|
||||
var task = target.StartWithConnection(content);
|
||||
|
||||
var login = (ILoginViewModel)target.Content;
|
||||
var connection = Substitute.For<IConnection>();
|
||||
((ISubject<object>)login.Done).OnNext(connection);
|
||||
|
||||
await task;
|
||||
Assert.That(content, Is.SameAs(target.Content));
|
||||
await content.Received(1).InitializeAsync(connection);
|
||||
}
|
||||
|
@ -94,11 +94,12 @@ namespace UnitTests.GitHub.App.ViewModels.Dialog
|
|||
var closed = false;
|
||||
|
||||
target.Done.Subscribe(_ => closed = true);
|
||||
await target.StartWithConnection(content);
|
||||
|
||||
var task = target.StartWithConnection(content);
|
||||
var login = (ILoginViewModel)target.Content;
|
||||
((ISubject<object>)login.Done).OnNext(null);
|
||||
|
||||
await task;
|
||||
Assert.True(closed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,400 +0,0 @@
|
|||
using System;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Models;
|
||||
using GitHub.Services;
|
||||
using GitHub.Validation;
|
||||
using NSubstitute;
|
||||
using Rothko;
|
||||
using NUnit.Framework;
|
||||
using GitHub.Collections;
|
||||
using NSubstitute.Core;
|
||||
using GitHub.Factories;
|
||||
using GitHub.Primitives;
|
||||
using GitHub.ViewModels.Dialog;
|
||||
using System.Diagnostics;
|
||||
|
||||
public class RepositoryCloneViewModelTests
|
||||
{
|
||||
const int Timeout = 2000;
|
||||
|
||||
static RepositoryCloneViewModel GetVM(IModelService modelService, IRepositoryCloneService cloneService, IOperatingSystem os)
|
||||
{
|
||||
var connection = Substitute.For<IConnection>();
|
||||
connection.HostAddress.Returns(HostAddress.GitHubDotComHostAddress);
|
||||
var modelServiceFactory = Substitute.For<IModelServiceFactory>();
|
||||
modelServiceFactory.CreateAsync(connection).Returns(modelService);
|
||||
|
||||
var vm = new RepositoryCloneViewModel(
|
||||
modelServiceFactory,
|
||||
cloneService,
|
||||
os);
|
||||
vm.InitializeAsync(connection).Wait();
|
||||
return vm;
|
||||
}
|
||||
|
||||
static ITrackingCollection<IRemoteRepositoryModel> SetupRepositories(
|
||||
CallInfo callInfo,
|
||||
IObservable<IRemoteRepositoryModel> repositories)
|
||||
{
|
||||
var collection = callInfo.Arg<ITrackingCollection<IRemoteRepositoryModel>>();
|
||||
collection.Listen(repositories);
|
||||
return collection;
|
||||
}
|
||||
|
||||
static IRemoteRepositoryModel CreateMockRepositoryModel()
|
||||
{
|
||||
var result = Substitute.For<IRemoteRepositoryModel>();
|
||||
result.Equals(result).Returns(true);
|
||||
return result;
|
||||
}
|
||||
|
||||
public class TheLoadRepositoriesCommand : TestBaseClass
|
||||
{
|
||||
[Test]
|
||||
public async Task LoadsRepositoriesAsync()
|
||||
{
|
||||
var repos = new IRemoteRepositoryModel[]
|
||||
{
|
||||
CreateMockRepositoryModel(),
|
||||
CreateMockRepositoryModel(),
|
||||
CreateMockRepositoryModel(),
|
||||
};
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repos.ToObservable()));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
var col = (ITrackingCollection<IRemoteRepositoryModel>)vm.Repositories;
|
||||
await col.OriginalCompleted.Timeout(TimeSpan.FromMilliseconds(Timeout));
|
||||
Assert.That(3, Is.EqualTo(vm.Repositories.Count));
|
||||
}
|
||||
}
|
||||
|
||||
public class TheIsBusyProperty : TestBaseClass
|
||||
{
|
||||
[Test]
|
||||
public async Task StartsTrueBecomesFalseWhenCompletedAsync()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
var col = (ITrackingCollection<IRemoteRepositoryModel>)vm.Repositories;
|
||||
|
||||
Assert.True(vm.IsBusy);
|
||||
|
||||
var done = new ReplaySubject<Unit>();
|
||||
done.OnNext(Unit.Default);
|
||||
done.Subscribe();
|
||||
col.Subscribe(t => done?.OnCompleted(), () => { });
|
||||
|
||||
repoSubject.OnNext(Substitute.For<IRemoteRepositoryModel>());
|
||||
repoSubject.OnNext(Substitute.For<IRemoteRepositoryModel>());
|
||||
|
||||
await done.Timeout(TimeSpan.FromMilliseconds(Timeout));
|
||||
done = null;
|
||||
|
||||
Assert.True(vm.IsBusy);
|
||||
|
||||
repoSubject.OnCompleted();
|
||||
|
||||
await col.OriginalCompleted.Timeout(TimeSpan.FromMilliseconds(Timeout));
|
||||
|
||||
// we need to wait slightly because the subscription OnComplete in the model
|
||||
// runs right after the above await finishes, which means the assert
|
||||
// gets checked before the flag is set
|
||||
await Task.Delay(100);
|
||||
Assert.False(vm.IsBusy);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFalseWhenLoadingReposFailsImmediately()
|
||||
{
|
||||
var repoSubject = Observable.Throw<IRemoteRepositoryModel>(new InvalidOperationException("Doh!"));
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
Assert.True(vm.LoadingFailed);
|
||||
Assert.False(vm.IsBusy);
|
||||
}
|
||||
}
|
||||
|
||||
public class TheNoRepositoriesFoundProperty : TestBaseClass
|
||||
{
|
||||
[Test]
|
||||
public void IsTrueInitially()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
|
||||
var connection = Substitute.For<IConnection>();
|
||||
connection.HostAddress.Returns(HostAddress.GitHubDotComHostAddress);
|
||||
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
|
||||
var modelServiceFactory = Substitute.For<IModelServiceFactory>();
|
||||
modelServiceFactory.CreateAsync(connection).Returns(modelService);
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
|
||||
var vm = new RepositoryCloneViewModel(
|
||||
modelServiceFactory,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
Assert.False(vm.LoadingFailed);
|
||||
Assert.True(vm.NoRepositoriesFound);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsFalseWhenLoadingAndCompletedWithRepositoryAsync()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
repoSubject.OnNext(Substitute.For<IRemoteRepositoryModel>());
|
||||
|
||||
Assert.False(vm.NoRepositoriesFound);
|
||||
|
||||
repoSubject.OnCompleted();
|
||||
|
||||
var col = (ITrackingCollection<IRemoteRepositoryModel>)vm.Repositories;
|
||||
await col.OriginalCompleted.Timeout(TimeSpan.FromMilliseconds(Timeout));
|
||||
//Assert.Single(vm.Repositories);
|
||||
Assert.False(vm.NoRepositoriesFound);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFalseWhenFailed()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
repoSubject.OnError(new InvalidOperationException());
|
||||
|
||||
Assert.False(vm.NoRepositoriesFound);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsTrueWhenLoadingCompleteNotFailedAndNoRepositoriesAsync()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
repoSubject.OnCompleted();
|
||||
|
||||
// we need to delay slightly because the subscribers listening for OnComplete
|
||||
// need to run before the assert is checked
|
||||
await Task.Delay(100);
|
||||
Assert.True(vm.NoRepositoriesFound);
|
||||
}
|
||||
}
|
||||
|
||||
public class TheFilterTextEnabledProperty : TestBaseClass
|
||||
{
|
||||
[Test]
|
||||
public void IsTrueInitially()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
Assert.False(vm.LoadingFailed);
|
||||
Assert.True(vm.FilterTextIsEnabled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFalseIfLoadingReposFails()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
Assert.False(vm.LoadingFailed);
|
||||
|
||||
repoSubject.OnError(new InvalidOperationException("Doh!"));
|
||||
|
||||
Assert.True(vm.LoadingFailed);
|
||||
Assert.False(vm.FilterTextIsEnabled);
|
||||
repoSubject.OnCompleted();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsFalseWhenLoadingCompleteNotFailedAndNoRepositoriesAsync()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
repoSubject.OnCompleted();
|
||||
|
||||
// we need to delay slightly because the subscribers listening for OnComplete
|
||||
// need to run before the assert is checked
|
||||
await Task.Delay(100);
|
||||
Assert.False(vm.FilterTextIsEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
public class TheLoadingFailedProperty : TestBaseClass
|
||||
{
|
||||
[Test]
|
||||
public void IsTrueIfLoadingReposFails()
|
||||
{
|
||||
var repoSubject = new Subject<IRemoteRepositoryModel>();
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, repoSubject));
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
|
||||
Assert.False(vm.LoadingFailed);
|
||||
|
||||
repoSubject.OnError(new InvalidOperationException("Doh!"));
|
||||
|
||||
Assert.True(vm.LoadingFailed);
|
||||
Assert.False(vm.IsBusy);
|
||||
repoSubject.OnCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
public class TheBaseRepositoryPathValidator
|
||||
{
|
||||
[Test]
|
||||
public void IsInvalidWhenDestinationRepositoryExists()
|
||||
{
|
||||
var repo = Substitute.For<IRemoteRepositoryModel>();
|
||||
repo.Id.Returns(1);
|
||||
repo.Name.Returns("bar");
|
||||
var data = new[] { repo }.ToObservable();
|
||||
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, data));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var os = Substitute.For<IOperatingSystem>();
|
||||
var directories = Substitute.For<IDirectoryFacade>();
|
||||
os.Directory.Returns(directories);
|
||||
directories.Exists(@"c:\foo\bar").Returns(true);
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
os);
|
||||
|
||||
vm.BaseRepositoryPath = @"c:\foo";
|
||||
vm.SelectedRepository = repo;
|
||||
|
||||
Assert.That(ValidationStatus.Invalid, Is.EqualTo(vm.BaseRepositoryPathValidator.ValidationResult.Status));
|
||||
}
|
||||
}
|
||||
|
||||
public class TheCloneCommand : TestBaseClass
|
||||
{
|
||||
[Test]
|
||||
public void IsEnabledWhenRepositorySelectedAndPathValid()
|
||||
{
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, Observable.Empty<IRemoteRepositoryModel>()));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
Assert.False(vm.CloneCommand.CanExecute(null));
|
||||
|
||||
vm.BaseRepositoryPath = @"c:\fake\path";
|
||||
vm.SelectedRepository = Substitute.For<IRemoteRepositoryModel>();
|
||||
|
||||
Assert.True(vm.CloneCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsNotEnabledWhenPathIsNotValid()
|
||||
{
|
||||
var modelService = Substitute.For<IModelService>();
|
||||
modelService.GetRepositories(Arg.Any<ITrackingCollection<IRemoteRepositoryModel>>())
|
||||
.Returns(x => SetupRepositories(x, Observable.Empty<IRemoteRepositoryModel>()));
|
||||
|
||||
var cloneService = Substitute.For<IRepositoryCloneService>();
|
||||
var vm = GetVM(
|
||||
modelService,
|
||||
cloneService,
|
||||
Substitute.For<IOperatingSystem>());
|
||||
vm.BaseRepositoryPath = @"c:|fake\path";
|
||||
Assert.False(vm.CloneCommand.CanExecute(null));
|
||||
|
||||
vm.SelectedRepository = Substitute.For<IRemoteRepositoryModel>();
|
||||
|
||||
Assert.False(vm.CloneCommand.CanExecute(null));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using GitHub.Models;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GitHub.Exports.UnitTests
|
||||
{
|
||||
public class ScopesCollectionTests
|
||||
{
|
||||
[Test]
|
||||
public void Matches_Returns_False_When_Missing_Scopes()
|
||||
{
|
||||
var required = new[] { "user", "repo", "gist", "write:public_key" };
|
||||
var target = new ScopesCollection(new[] { "user", "repo", "write:public_key" });
|
||||
|
||||
Assert.False(target.Matches(required));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_True_When_Scopes_Equal()
|
||||
{
|
||||
var required = new[] { "user", "repo", "gist", "write:public_key" };
|
||||
var target = new ScopesCollection(new[] { "user", "repo", "gist", "write:public_key" });
|
||||
|
||||
Assert.True(target.Matches(required));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_True_When_Extra_Scopes_Returned()
|
||||
{
|
||||
var required = new[] { "user", "repo", "gist", "write:public_key" };
|
||||
var target = new ScopesCollection(new[] { "user", "repo", "gist", "foo", "write:public_key" });
|
||||
|
||||
Assert.True(target.Matches(required));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Returns_True_When_Admin_Scope_Returned_Instead_Of_Write()
|
||||
{
|
||||
var required = new[] { "user", "repo", "gist", "write:public_key" };
|
||||
var target = new ScopesCollection(new[] { "user", "repo", "gist", "foo", "admin:public_key" });
|
||||
|
||||
Assert.True(target.Matches(required));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -261,12 +261,14 @@ public class ConnectionManagerTests
|
|||
static ILoginManager CreateLoginManager()
|
||||
{
|
||||
var result = Substitute.For<ILoginManager>();
|
||||
result.Login(null, null, null, null)
|
||||
.ReturnsForAnyArgs(new LoginResult(new User(), new ScopesCollection(new[] { "scope1" })));
|
||||
result.Login(HostAddress.Create("invalid.com"), Arg.Any<IGitHubClient>(), Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns<User>(_ => { throw new AuthorizationException(); });
|
||||
.Returns<LoginResult>(_ => { throw new AuthorizationException(); });
|
||||
result.LoginFromCache(null, null)
|
||||
.ReturnsForAnyArgs(new User());
|
||||
.ReturnsForAnyArgs(new LoginResult(new User(), new ScopesCollection(new[] { "scope1" })));
|
||||
result.LoginFromCache(HostAddress.Create("invalid.com"), Arg.Any<IGitHubClient>())
|
||||
.Returns<User>(_ => { throw new AuthorizationException(); });
|
||||
.Returns<LoginResult>(_ => { throw new AuthorizationException(); });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ using System;
|
|||
using System.ComponentModel.Composition;
|
||||
using System.ComponentModel.Composition.Hosting;
|
||||
using GitHub.Factories;
|
||||
using GitHub.Api;
|
||||
|
||||
namespace UnitTests
|
||||
{
|
||||
|
@ -110,7 +111,7 @@ namespace UnitTests
|
|||
|
||||
var os = OperatingSystem;
|
||||
var vsgit = IVSGitServices;
|
||||
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<IUsageTracker>());
|
||||
var clone = cloneService ?? new RepositoryCloneService(os, vsgit, Substitute.For<IGraphQLClientFactory>(), Substitute.For<IUsageTracker>());
|
||||
var create = creationService ?? new RepositoryCreationService(clone);
|
||||
avatarProvider = avatarProvider ?? Substitute.For<IAvatarProvider>();
|
||||
//ret.GetService(typeof(IGitRepositoriesExt)).Returns(IGitRepositoriesExt);
|
||||
|
|
Загрузка…
Ссылка в новой задаче