diff --git a/TFSAzurePicker/LICENSE b/TFSAzurePicker/LICENSE new file mode 100644 index 0000000..b4571fe --- /dev/null +++ b/TFSAzurePicker/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Countersoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/TFSAzurePicker/Microsoft.TeamFoundation.Test.WebApi.dll b/TFSAzurePicker/Microsoft.TeamFoundation.Test.WebApi.dll new file mode 100644 index 0000000..e83f653 Binary files /dev/null and b/TFSAzurePicker/Microsoft.TeamFoundation.Test.WebApi.dll differ diff --git a/TFSAzurePicker/Microsoft.TeamFoundation.TestManagement.WebApi.dll b/TFSAzurePicker/Microsoft.TeamFoundation.TestManagement.WebApi.dll new file mode 100644 index 0000000..f996b86 Binary files /dev/null and b/TFSAzurePicker/Microsoft.TeamFoundation.TestManagement.WebApi.dll differ diff --git a/TFSAzurePicker/Microsoft.TeamFoundation.WorkItemTracking.WebApi.dll b/TFSAzurePicker/Microsoft.TeamFoundation.WorkItemTracking.WebApi.dll new file mode 100644 index 0000000..fdbc36a Binary files /dev/null and b/TFSAzurePicker/Microsoft.TeamFoundation.WorkItemTracking.WebApi.dll differ diff --git a/TFSAzurePicker/Microsoft.VisualStudio.Services.Common.dll b/TFSAzurePicker/Microsoft.VisualStudio.Services.Common.dll new file mode 100644 index 0000000..75037d8 Binary files /dev/null and b/TFSAzurePicker/Microsoft.VisualStudio.Services.Common.dll differ diff --git a/TFSAzurePicker/Microsoft.VisualStudio.Services.TestResults.WebApi.dll b/TFSAzurePicker/Microsoft.VisualStudio.Services.TestResults.WebApi.dll new file mode 100644 index 0000000..56d83bf Binary files /dev/null and b/TFSAzurePicker/Microsoft.VisualStudio.Services.TestResults.WebApi.dll differ diff --git a/TFSAzurePicker/Microsoft.VisualStudio.Services.WebApi.dll b/TFSAzurePicker/Microsoft.VisualStudio.Services.WebApi.dll new file mode 100644 index 0000000..9cb38ae Binary files /dev/null and b/TFSAzurePicker/Microsoft.VisualStudio.Services.WebApi.dll differ diff --git a/TFSAzurePicker/README.md b/TFSAzurePicker/README.md new file mode 100644 index 0000000..3521c1d --- /dev/null +++ b/TFSAzurePicker/README.md @@ -0,0 +1,6 @@ +App-TFSPicker +============= + +Associate TFS Work Items with items in Gemini. + +![Alt text](screenshot.png "") \ No newline at end of file diff --git a/TFSAzurePicker/TFSAzurePicker.csproj b/TFSAzurePicker/TFSAzurePicker.csproj new file mode 100644 index 0000000..ce1aae8 --- /dev/null +++ b/TFSAzurePicker/TFSAzurePicker.csproj @@ -0,0 +1,225 @@ + + + + + Debug + AnyCPU + {6C9851AB-A979-4F55-88EB-21BD9E51DC83} + Library + Properties + TFSAzurePicker + TFSAzurePicker + v4.7.2 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + bin\Build\ + TRACE + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + false + + + + ..\libs\Countersoft.Foundation.Commons.dll + + + ..\libs\Countersoft.Foundation.Contracts.dll + + + ..\libs\Countersoft.Gemini.dll + + + ..\libs\Countersoft.Gemini.Authentication.dll + + + ..\libs\Countersoft.Gemini.Commons.dll + + + ..\libs\Countersoft.Gemini.Contracts.dll + + + ..\libs\Countersoft.Gemini.Extensibility.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.Azure.DevOps.Comments.WebApi.dll + + + packages\Microsoft.IdentityModel.JsonWebTokens.5.6.0\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll + + + packages\Microsoft.IdentityModel.Logging.5.6.0\lib\net461\Microsoft.IdentityModel.Logging.dll + + + packages\Microsoft.IdentityModel.Tokens.5.6.0\lib\net461\Microsoft.IdentityModel.Tokens.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Build2.WebApi.dll + + + ..\packages\Microsoft.VisualStudio.Services.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Common.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Core.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Dashboards.WebApi.dll + + + ..\packages\Microsoft.TeamFoundation.DistributedTask.Common.Contracts.16.170.0\lib\net462\Microsoft.TeamFoundation.DistributedTask.Common.Contracts.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Policy.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.SourceControl.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Test.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.TestManagement.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Wiki.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.Work.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.WorkItemTracking.Process.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.TeamFoundation.WorkItemTracking.WebApi.dll + + + ..\packages\Microsoft.VisualStudio.Services.Client.16.170.0\lib\net462\Microsoft.VisualStudio.Services.Common.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.VisualStudio.Services.TestManagement.TestPlanning.WebApi.dll + + + ..\packages\Microsoft.TeamFoundationServer.Client.16.170.0\lib\net462\Microsoft.VisualStudio.Services.TestResults.WebApi.dll + + + ..\packages\Microsoft.VisualStudio.Services.Client.16.170.0\lib\net462\Microsoft.VisualStudio.Services.WebApi.dll + + + packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + + packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + + + + + + packages\System.IdentityModel.Tokens.Jwt.5.6.0\lib\net461\System.IdentityModel.Tokens.Jwt.dll + + + + packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll + + + + packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.Helpers.dll + + + packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll + + + packages\Microsoft.AspNet.WebApi.WebHost.5.2.7\lib\net45\System.Web.Http.WebHost.dll + + + packages\Microsoft.AspNet.Mvc.5.2.7\lib\net45\System.Web.Mvc.dll + + + packages\Microsoft.AspNet.Razor.3.2.7\lib\net45\System.Web.Razor.dll + + + + packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.dll + + + packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Deployment.dll + + + packages\Microsoft.AspNet.WebPages.3.2.7\lib\net45\System.Web.WebPages.Razor.dll + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + + + + + + + + del $(ProjectDir)$(TargetName).dll 2>null 1>null +copy $(TargetDir)$(TargetName).dll $(ProjectDir)$(TargetName).dll + +del $(ProjectDir)$(TargetName).pdb +copy $(TargetDir)$(TargetName).pdb $(ProjectDir)$(TargetName).pdb + +del $(ProjectDir)$(TargetName).zip +$(SolutionDir)libs\7za.exe a $(TargetDir)$(TargetName).zip $(ProjectDir)*.dll $(ProjectDir)*.pdb $(ProjectDir)*.manifest $(ProjectDir)views $(ProjectDir)*.js $(ProjectDir)*.css + +copy $(TargetDir)$(TargetName).zip C:\Projects\Gemini\Countersoft.Gemini\App_Data\apps\ + +del $(ProjectDir)$(TargetName).dll +del $(ProjectDir)$(TargetName).pdb + + + \ No newline at end of file diff --git a/TFSAzurePicker/TFSPicker.cs b/TFSAzurePicker/TFSPicker.cs new file mode 100644 index 0000000..6abc24e --- /dev/null +++ b/TFSAzurePicker/TFSPicker.cs @@ -0,0 +1,70 @@ +using System.Text; +using Countersoft.Gemini.Extensibility.Apps; +using Countersoft.Foundation.Commons.Extensions; +using System.Net; +using Countersoft.Gemini.Commons.Entity; +using Countersoft.Gemini.Commons.System; +using System.Web; + +namespace TFSPicker +{ + internal static class Constants + { + public static string AppId = "782D003D-D9F0-455F-AF09-74417D6DFD2B"; + public static string ControlId = "061F1F58-35BC-4509-83FC-CCC996ED4F36"; + } + + public class TFSPicker + { + private string Username { get; set; } + private string Password { get; set; } + private string RepositoryUrl { get; set; } + public static bool IsBasicAuth { get; set; } + + static TFSPicker() + { + try + { + IsBasicAuth = System.Configuration.ConfigurationManager.AppSettings["gemini.tfs.basicauth"].ToBool(); + } + catch + { + } + } + + public TFSPicker() + { + try + { + IsBasicAuth = System.Configuration.ConfigurationManager.AppSettings["gemini.tfs.basicauth"].ToBool(); + } + catch + { + } + } + + + public void setLoginDetails(string authUsername, string authPassword, string authRepositoryUrl) + { + Username = authUsername; + + Password = authPassword; + + RepositoryUrl = authRepositoryUrl; + } + + //public UserWidgetDataDetails getLoginDetails() + //{ + // UserWidgetDataDetails user = new UserWidgetDataDetails(); + + // user.Password = Password; + + // user.Username = Username; + + // user.RepositoryUrl = RepositoryUrl; + + // return user; + //} + + } +} diff --git a/TFSAzurePicker/TfsAzureConstants.cs b/TFSAzurePicker/TfsAzureConstants.cs new file mode 100644 index 0000000..a7c7e33 --- /dev/null +++ b/TFSAzurePicker/TfsAzureConstants.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TFSAzurePicker +{ + public static class TfsAzureConstants + { + public const string AppId = "3af0b23d-177f-43f4-b663-12368289f3c2"; + public const string ControlId = "8279b9d6-65fe-47df-bc54-4043ac9aed09"; + } +} diff --git a/TFSAzurePicker/TfsPickerController.cs b/TFSAzurePicker/TfsPickerController.cs new file mode 100644 index 0000000..b1101c5 --- /dev/null +++ b/TFSAzurePicker/TfsPickerController.cs @@ -0,0 +1,814 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Countersoft.Gemini.Extensibility.Apps; +using Countersoft.Foundation.Commons.Extensions; +using System.Net; +using System.Collections.ObjectModel; +using Countersoft.Gemini.Commons.Entity; +using System.Web.Mvc; +using Countersoft.Gemini.Infrastructure; +using Countersoft.Gemini.Infrastructure.Apps; +using Countersoft.Gemini.Commons.System; +using Countersoft.Gemini.Commons.Dto; +using Countersoft.Gemini.Contracts; +using System.Web.UI; +using System.Web.Routing; +using Countersoft.Gemini; +using Countersoft.Gemini.Authentication.OAuth; +using Microsoft.VisualStudio.Services.Common; +using Countersoft.Gemini.Commons.Entity.System; +using Microsoft.VisualStudio.Services.OAuth; +using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; +using Microsoft.TeamFoundation.WorkItemTracking.WebApi; + +namespace TFSAzurePicker +{ + + public class TFSAuthenticationModel + { + public int IssueId { get; set; } + public string Url { get; set; } + public bool NoOAuthClient { get; internal set; } + } + + + [AppType(AppTypeEnum.Widget), + AppGuid( TfsAzureConstants.AppId ), + AppControlGuid( TfsAzureConstants.ControlId ), + AppAuthor("Countersoft"), + AppKey("tfsazurepicker"), + AppName("tfsazurepicker"), + AppDescription("tfsazurepicker")] + [OutputCache(Duration = 0, NoStore = false, Location = OutputCacheLocation.None)] + public class tfsazurepickerController : BaseAppController + { + ContentResult dataView = null; + + bool successView = true; + + string messageView = string.Empty; + + private bool _validLicense; + + private string RepositoryUrl { get; set; } + + private string Token { get; set; } + + + public override WidgetResult Caption(IssueDto issue) + { + WidgetResult result = new WidgetResult(); + + result.Success = true; + + result.Markup.Html = "TFS Azure Picker"; + + return result; + } + + public override void RegisterRoutes(RouteCollection routes) + { + routes.MapRoute(null, "apps/tfsazurepicker/search", new { controller = "tfsazurepicker", action = "Search" }); + + routes.MapRoute(null, "apps/tfsazurepicker/add", new { controller = "tfsazurepicker", action = "Add" }); + + routes.MapRoute(null, "apps/tfsazurepicker/delete", new { controller = "tfsazurepicker", action = "Delete" }); + + routes.MapRoute(null, "apps/tfsazurepicker/authenticate/{issueid}", new { controller = "tfsazurepicker", action = "Authenticate" }); + + routes.MapRoute(null, "apps/tfsazurepicker/logout", new { controller = "tfsazurepicker", action = "Logout" }); + } + + public override WidgetResult Show(IssueDto issueItem) + { + WidgetResult result = new WidgetResult(); + + if (!_validLicense) + { + _validLicense = !GeminiApp.LicenseSummary.IsFree || GeminiApp.LicenseSummary.IsGeminiTrial(); + + if (!_validLicense) + { + result.Markup = new WidgetMarkup(UnlicensedMessage); + result.Success = true; + return result; + } + } + + List tfsDetails = new List(); + + + //Check if OAuth Configured + var oauthClient = GeminiContext.OAuthClients + .FindWhere( c => c.App == OAuthSupportedApps.TFS ) + .FirstOrDefault(); + TFSAuthenticationModel authModel = new TFSAuthenticationModel(); + authModel.IssueId = issueItem.Entity.Id; + ITokenProvider provider = null; + if ( oauthClient == null ) + { + authModel.NoOAuthClient = true; + authModel.Url = GeminiApp.UserContext().Url + "/configure"; + result.Markup = new WidgetMarkup( "views\\authenticationForm.cshtml", authModel ); + result.Success = true; + return result; + } + + OAuthTokenManager manager = new OAuthTokenManager( GeminiApp.UserContext().Url ); + provider = manager.Create( oauthClient, GeminiApp.UserContext().User.Entity.Id ); + + Token = provider.AquireTokenSilently(); + if(Token == null || string.IsNullOrEmpty( Token ) ) + { + result.Markup = new WidgetMarkup( "views\\authenticationForm.cshtml", authModel ); + authModel.Url = provider.GetAuthorizationUrl(); + result.Success = true; + return result; + } + var userDetails = GetUserDetails(); + if ( string.IsNullOrEmpty( userDetails.Organisation ) ) + { + authModel.Url = provider.GetAuthorizationUrl(); + result.Markup = new WidgetMarkup( "views\\authenticationForm.cshtml", authModel ); + result.Success = true; + return result; + } + + Dictionary details = new Dictionary(); + + IssueWidgetData> data = GeminiContext.IssueWidgetStore.Get>( issueItem.Entity.Id, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + if ( data != null && data.Value != null && data.Value.Count > 0 ) + { + tfsDetails = data.Value; + } + + foreach ( var tfs in tfsDetails ) + { + string url = string.Empty; + try + { + WorkItem item = GetItem( tfs ); + if ( item != null ) + { + if(item.Fields.ContainsKey("System.TeamProject")) + { + url = string.Format( "https://dev.azure.com/{0}/{1}/_workitems/edit/{2}", userDetails.Organisation, item.Fields["System.TeamProject"], item.Id ); + } + if ( !details.ContainsKey( url ) ) + { + details.Add( url, item ); + } + } + + } + catch (Exception ex) + { + var tempWorkItem = new WorkItem(); + var error = string.Format( "Error loading item {0} from Organisation {1}: {2} ", tfs, userDetails.Organisation, ex.Message ); + if(ex.InnerException != null ) + { + error += ex.InnerException.Message; + } + tempWorkItem.Fields.Add( "System.Description", error); + details.Add( "error" + tfs, tempWorkItem ); + } + } + + Dictionary tfspickerModel = ConvertWorkItemsToPickerItems( details ); + result.Markup = new WidgetMarkup( "views\\items.cshtml", tfspickerModel ); + + + //Dictionary details = new Dictionary(); + + //Pair authenticationModel = new Pair(issueItem.Entity.Id, string.Concat("apps/tfsazurepicker/authenticate/", issueItem.Entity.Id)); + //authModel.IssueId = issueItem.Entity.Id; + + + // IssueWidgetData> data = GeminiContext.IssueWidgetStore.Get>( issueItem.Entity.Id, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + //if ( data != null && data.Value != null && data.Value.Count > 0 ) + //{ + // tfsDetails = data.Value; + //} + + + //if ( tfsDetails.Count > 0) + //{ + // if (AuthenticateUser(issueItem, provider)) + // { + // //IsTfs2012(); + // foreach (var tfs in tfsDetails) + // { + // try + // { + // string url; + + // var item = GetItem(tfs, out url); + + // if (item != null) + // { + // if (url == null) url = string.Format("{0}/web/UI/Pages/WorkItems/WorkItemEdit.aspx?id={1}&pguid={2}", RepositoryUrl, item.Id, item.Project.Guid); + // /*if (isTfs2012) + // { + // url = string.Format("{0}/DefaultCollection/Countersoft/_workitems#_a=edit&id={1}", RepositoryUrl, item.Id); + // } + // else + // { + // url = string.Format("{0}/web/UI/Pages/WorkItems/WorkItemEdit.aspx?id={1}&pguid={2}", RepositoryUrl, item.Id, item.Project.Guid); + // }*/ + // if (!details.ContainsKey(url)) + // { + // details.Add(url, item); + // } + // } + + // Dictionary tfsazurepickerModel = ConvertWorkItemsTotfsazurepickerItems(details); + + // result.Markup = new WidgetMarkup("views\\items.cshtml", tfsazurepickerModel); + // } + // catch (Exception ex) + // { + // result.Markup = new WidgetMarkup("views\\authenticationForm.cshtml", authModel); + + // GeminiApp.LogException(new Exception(ex.Message) { Source = "TFS Picker" }, false); + // } + // } + // } + // else + // { + // result.Markup = new WidgetMarkup("views\\authenticationForm.cshtml", authModel); + // } + //} + //else + //{ + // try + // { + // if (AuthenticateUser(issueItem, provider)) + // { + // string url; + // WorkItem item = null; + // if ( authModel.IsOAuth ) + // { + + // } + // else + // { + // item = GetItem( "", out url ); + // } + + // Dictionary tfsazurepickerModel = ConvertWorkItemsTotfsazurepickerItems(details); + + // result.Markup = new WidgetMarkup("views\\items.cshtml", tfsazurepickerModel); + // } + // else + // { + // result.Markup = new WidgetMarkup("views\\authenticationForm.cshtml", authModel); + // } + // } + // catch (Exception ex) + // { + // result.Markup = new WidgetMarkup("views\\authenticationForm.cshtml", authModel); + + // GeminiApp.LogException(new Exception(ex.Message) { Source = "TFS Picker" }, false); + // } + + + //} + + result.Success = true; + + return result; + } + + private Dictionary ConvertWorkItemsToPickerItems(Dictionary details) + { + Dictionary tfsazurepickerModel = new Dictionary(); + + foreach (var workitem in details) + { + TfsPickerItem pickerItem = new TfsPickerItem(); + + pickerItem.Id = workitem.Value.Id.GetValueOrDefault(); + if( workitem.Value.Fields.ContainsKey("System.WorkItemType" )) { + pickerItem.TypeName = workitem.Value.Fields["System.WorkItemType"].ToString(); + } + if( workitem.Value.Fields.ContainsKey( "System.Title" )) + { + pickerItem.Title = workitem.Value.Fields["System.Title"].ToString(); + } + if ( workitem.Value.Fields.ContainsKey( "System.Description" ) ) + { + pickerItem.Description = workitem.Value.Fields["System.Description"].ToString(); + } + if ( workitem.Value.Fields.ContainsKey( "System.TeamProject" ) ) + { + pickerItem.ProjectName = workitem.Value.Fields["System.TeamProject"].ToString(); + } + tfsazurepickerModel.Add(workitem.Key, pickerItem); + } + + return tfsazurepickerModel; + } + + [AppUrl( "saveinstance" )] + public ActionResult SaveInstance() + { + var userData = base.GeminiContext.UserWidgetStore.Get( UserContext.User.Entity.Id, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + if ( userData == null ) + { + userData = new UserWidgetData(); + + userData.Value = new UserWidgetDataDetails(); + } + userData.Value.Organisation = Request["organisation"] ?? string.Empty; + userData.Value.Project = Request["project"] ?? string.Empty; + + GeminiContext.UserWidgetStore.Save( UserContext.User.Entity.Id, TfsAzureConstants.AppId, TfsAzureConstants.ControlId, userData.Value ); + + return JsonSuccess(); + } + + private UserWidgetDataDetails GetUserDetails() + { + var userData = base.GeminiContext.UserWidgetStore.Get( UserContext.User.Entity.Id, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + return userData == null ? new UserWidgetDataDetails() : userData.Value; + } + + [AppUrl("Search")] + public ActionResult Search( string id, string search ) + { + //ItemWidgetArguments args = new ItemWidgetArguments( UserContext, GeminiContext, Cache, System.Web.HttpContext.Current.Request, CurrentIssue ); + + OAuthTokenManager manager = new OAuthTokenManager( GeminiApp.UserContext().Url ); + var client = GeminiContext.OAuthClients + .FindWhere( c => c.App == OAuthSupportedApps.TFS ) + .FirstOrDefault(); + var provider = manager.Create( client, UserContext.User.Entity.Id ); + Token = provider.AquireTokenSilently(); + string query = "Select [Id], [Work Item Type], [Title], [State] From WorkItems Where [Title] Contains '" + search + "' Order By [Id] Asc"; + + if ( search.Trim().Length == 0 ) + { + query = "Select [Id], [Work Item Type], [Title], [Description] From WorkItems Order By [Id] Asc"; + } + + var wiql = new Wiql() + { + Query = query + }; + UserWidgetDataDetails loginDetails = GetUserDetails(); + Dictionary details = new Dictionary(); + try + { + using ( var httpClient = GetClient() ) + { + var result = httpClient.QueryByWiqlAsync( wiql ).Result; + var ids = result.WorkItems.Select( item => item.Id ).ToArray(); + if ( ids.Length == 0 ) + { + //handle empty list + } + else + { + var fields = new[] { "System.Id", "System.Title", "System.State", "System.Description", "System.WorkItemType", "System.TeamProject" }; + var items = httpClient.GetWorkItemsAsync( ids, fields, result.AsOf ).Result; + foreach ( var item in items ) + { + var url = string.Format( "https://dev.azure.com/{0}/{1}/_workitems/edit/{2}/", loginDetails.Organisation, item.Fields["System.TeamProject"], item.Id ); + details.Add( url, item ); + } + // return Content( items.ToJson() ); + } + } + } + catch ( Exception ex ) + { + TFSAuthenticationModel auth = new TFSAuthenticationModel() + { + IssueId = CurrentIssue.Entity.Id, + NoOAuthClient = false, + Url = provider.GetAuthorizationUrl() + }; + + dataView = Content( BaseController.RenderPartialViewToString( this, + AppManager.Instance.GetAppUrl( TfsAzureConstants.AppId, "views/authenticationForm.cshtml" ), auth ) ); + + successView = false; + + messageView = ex.Message; + + GeminiApp.LogException( new Exception( ex.Message ) { Source = "TFS Picker" }, false ); + } + + Dictionary tfsazurepickerModel = ConvertWorkItemsToPickerItems( details ); + + dataView = Content( BaseController.RenderPartialViewToString( this, AppManager.Instance.GetAppUrl( TfsAzureConstants.AppId, "views/search.cshtml" ), tfsazurepickerModel ) ); + + return JsonSuccess( new { success = successView, data = dataView, message = messageView } ); + + + } + + [AppUrl("Add")] + public ActionResult Add( int issueId, string tfsId ) + { + IssueWidgetData> data = + GeminiContext.IssueWidgetStore.Get>( issueId, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + if ( data == null || data.Value == null ) + { + data = new IssueWidgetData>(); + data.AppId = TfsAzureConstants.AppId; + data.ControlId = TfsAzureConstants.ControlId; + data.IssueId = issueId; + data.Value = new List(); + } + + data.Value.AddRange( tfsId.TrimEnd( ',' ).Split( ',' ) ); + + GeminiContext.IssueWidgetStore.Save( data ); + + var result = AppManager.Instance.ItemContentWidgetsOnShow( this, UserContext, GeminiContext, Cache, UserContext.Issue, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + return JsonSuccess(result) ; + } + + [AppUrl( "Delete" )] + public ActionResult Delete( int issueId, string tfsRow ) + { + IssueWidgetData> data = GeminiContext.IssueWidgetStore.Get>( issueId, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + + issueId = data.IssueId; + + if ( data == null || data.Value == null ) + { + return JsonError(); + } + + var index = data.Value.FindIndex( d => string.Compare( d, tfsRow, StringComparison.InvariantCultureIgnoreCase ) == 0 ); + + if ( index == -1 ) return JsonError(); + + data.Value.RemoveAt( index ); + + GeminiContext.IssueWidgetStore.Save( data ); + + return JsonSuccess( AppManager.Instance.ItemContentWidgetsOnShow( this, UserContext, GeminiContext, Cache, UserContext.Issue, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ) ); + + } + + + [AppUrl("Logout")] + public ActionResult Logout() + { + var data = GeminiContext.UserWidgetStore.Get( CurrentUser.Entity.Id, TfsAzureConstants.AppId, TfsAzureConstants.ControlId ); + + if ( data != null ) + { + GeminiContext.UserWidgetStore.Delete( data.Id ); + return JsonSuccess(); + } + + return JsonError(); + } + + + /* + public bool AuthenticateUser(IssueDto args, ITokenProvider provider ) + { + UserWidgetData userDataRaw = GeminiContext.UserWidgetStore.Get(CurrentUser.Entity.Id, Constants.AppId, Constants.ControlId); + + var token = provider.AquireTokenSilently(); + if(token != null ) + { + return true; + } + + if (userDataRaw == null && token == null) return false; + + Username = userDataRaw.Value.Username; + + Password = SecretsHelper.Decrypt(userDataRaw.Value.Password, SecretsHelper.EncryptionKey); + + RepositoryUrl = userDataRaw.Value.RepositoryUrl; + + + return true; + } + + public class WorkItem2 + { + public WorkItem Item { get; set; } + + public string BaseUrl { get; set; } + + public Uri FullUrl { get; set; } + } + + + + + + + [AppUrl(@"authenticate/{issueid}")] + public ActionResult Authenticate(int issueId) + { + //Authentication + string username = Request["username"] ?? string.Empty; + + string password = Request["password"] ?? string.Empty; + + string repositoryUrl = Request["repositoryurl"] ?? string.Empty; + + string message = string.Empty; + + bool success = true; + + string dataView = string.Empty; + + if (username.IsEmpty() || password.IsEmpty() || repositoryUrl.IsEmpty()) + { + message = "Please make sure Username, Password and Url are not empty"; + success = false; + } + + if (success) + { + UserWidgetDataDetails userData = new UserWidgetDataDetails(); + + userData.Username = username.Trim(); + + userData.Password = SecretsHelper.Encrypt(password.Trim(), SecretsHelper.EncryptionKey); + + userData.RepositoryUrl = repositoryUrl.Trim(); + + SaveLoginDetails(CurrentUser, userData, GeminiContext); + + tfsazurepicker tfsazurepicker = new TFSAzurePicker(); + + try + { + ItemWidgetArguments args = new ItemWidgetArguments(UserContext, GeminiContext, Cache, System.Web.HttpContext.Current.Request, CurrentIssue); + + tfsazurepicker.AuthenticateUser(args); + + UserWidgetDataDetails loginDetails = tfsazurepicker.getLoginDetails(); + + TFSAzurePicker.ConnectByImplementingCredentialsProvider connect = new TFSAzurePicker.ConnectByImplementingCredentialsProvider(); + + ICredentials iCred = new NetworkCredential(loginDetails.Username, loginDetails.Password); + + connect.setLoginDetails(loginDetails.Username, loginDetails.Password, loginDetails.RepositoryUrl); + + connect.GetCredentials(new Uri(loginDetails.RepositoryUrl), iCred); + + TfsConfigurationServer configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(new Uri(loginDetails.RepositoryUrl)); + + + configurationServer.Credentials = iCred; + + if (tfsazurepicker.IsBasicAuth) + { + configurationServer.ClientCredentials = new TfsClientCredentials(new BasicAuthCredential(iCred)); + } + else + { + configurationServer.ClientCredentials = new TfsClientCredentials(new WindowsCredential(iCred)); + } + + try + { + configurationServer.EnsureAuthenticated(); + } + catch + { + System.Threading.Thread.Sleep(1000); + configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(new Uri(loginDetails.RepositoryUrl)); + configurationServer.Credentials = iCred; + + if (tfsazurepicker.IsBasicAuth) + { + configurationServer.ClientCredentials = new TfsClientCredentials(new BasicAuthCredential(iCred)); + } + else + { + configurationServer.ClientCredentials = new TfsClientCredentials(new WindowsCredential(iCred)); + } + configurationServer.EnsureAuthenticated(); + } + } + catch (Exception ex) + { + var logindetails = GeminiContext.UserWidgetStore.Get(CurrentUser.Entity.Id, Constants.AppId, Constants.ControlId); + + if (logindetails != null) + { + GeminiContext.UserWidgetStore.Delete(logindetails.Id); + } + + success = false; + + message = ex.Message; + + GeminiApp.LogException(new Exception(ex.Message) { Source = "TFS Picker" }, false); + + return JsonSuccess(new { success = success, message = message }); + } + + tfsazurepicker.setLoginDetails(userData.Username, password.Trim(), userData.RepositoryUrl); + + WidgetResult result = new WidgetResult(); + + List tfsDetails = new List(); + + IssueWidgetData> data = GeminiContext.IssueWidgetStore.Get>(issueId, Constants.AppId, Constants.ControlId); + + if (data != null && data.Value != null && data.Value.Count > 0) + { + tfsDetails = data.Value; + } + + List details = new List(); + + foreach (var tfs in tfsDetails) + { + try + { + string url; + + UserWidgetDataDetails loginDetails = tfsazurepicker.getLoginDetails(); + + if (Username.IsEmpty()) + { + Username = loginDetails.Username; + } + + if (Password.IsEmpty()) + { + Password = loginDetails.Password; + } + + if (RepositoryUrl.IsEmpty()) + { + RepositoryUrl = loginDetails.RepositoryUrl; + } + + var item = GetItem(tfs, out url); + + if (item != null) + { + details.Add(item); + } + } + catch (Exception ex) + { + success = false; + + message = ex.Message; + + GeminiApp.LogException(new Exception(ex.Message) { Source = "TFS Picker" }, false); + } + } + } + + return JsonSuccess(new { success = success, message = message }); + } + + + + public void SaveLoginDetails(UserDto user, UserWidgetDataDetails userData, GeminiContext gemini) + { + UserWidgetData userDataRaw = gemini.UserWidgetStore.Get(user.Entity.Id, Constants.AppId, Constants.ControlId); + + if (userDataRaw == null) + { + var data = new UserWidgetData(); + + data.Value = new UserWidgetDataDetails(); + + data.Value = userData; + + gemini.UserWidgetStore.Save(user.Entity.Id, Constants.AppId, Constants.ControlId, data.Value); + } + else + { + userDataRaw.Value.Password = userData.Password; + + userDataRaw.Value.Username = userData.Username; + + userDataRaw.Value.RepositoryUrl = userData.RepositoryUrl; + + gemini.UserWidgetStore.Save(user.Entity.Id, Constants.AppId, Constants.ControlId, userDataRaw.Value); + } + } + + */ + + private WorkItemTrackingHttpClient GetClient() + { + var vssCred = new VssOAuthAccessTokenCredential( Token ); + + UserWidgetDataDetails loginDetails = GetUserDetails(); + + var uri = new Uri( "https://dev.azure.com/" + loginDetails.Organisation ); + + return new WorkItemTrackingHttpClient( uri, vssCred ); + } + public WorkItem GetItem( string id ) + { + var qry = string.Format( "Select [Id], [Work Item Type], [Title], [State] From WorkItems WHERE [Id] = '{0}' Order By [Id] Asc", id ); + var wiql = new Wiql() + { + Query = qry + }; + + using ( var httpClient = GetClient() ) + { + var workitem = httpClient.GetWorkItemAsync( id.ToInt() ).Result; + return workitem; + } + } + /* + public WorkItem GetItem1(string id, out string url) + { + url = null; + + TFSAzurePicker.ConnectByImplementingCredentialsProvider connect = new TFSAzurePicker.ConnectByImplementingCredentialsProvider(); + + ICredentials iCred = new NetworkCredential(Username, Password); + + connect.setLoginDetails(Username, Password, RepositoryUrl); + + connect.GetCredentials(new Uri(RepositoryUrl), iCred); + + TfsConfigurationServer configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(new Uri(RepositoryUrl)); + + + configurationServer.Credentials = iCred; + + if (TFSAzurePicker.IsBasicAuth) + { + configurationServer.ClientCredentials = new TfsClientCredentials(new BasicAuthCredential(iCred)); + } + else + { + configurationServer.ClientCredentials = new TfsClientCredentials(new WindowsCredential(iCred)); + } + + try + { + configurationServer.EnsureAuthenticated(); + } + catch + { + System.Threading.Thread.Sleep(1000); + configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(new Uri(RepositoryUrl)); + configurationServer.Credentials = iCred; + + if (TFSAzurePicker.IsBasicAuth) + { + configurationServer.ClientCredentials = new TfsClientCredentials(new BasicAuthCredential(iCred)); + } + else + { + configurationServer.ClientCredentials = new TfsClientCredentials(new WindowsCredential(iCred)); + } + configurationServer.EnsureAuthenticated(); + } + + CatalogNode catalogNode = configurationServer.CatalogNode; + + ReadOnlyCollection tpcNodes = catalogNode.QueryChildren(new Guid[] { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None); + + foreach (CatalogNode tpcNode in tpcNodes) + { + TfsTeamProjectCollection tpc = configurationServer.GetTeamProjectCollection(new Guid(tpcNode.Resource.Properties["InstanceId"])); + //TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(new Uri(string.Concat(RepositoryUrl, '/', tpcNode.Resource.DisplayName)), iCred); + + if (TFSAzurePicker.IsBasicAuth) tpc.ClientCredentials = new TfsClientCredentials(new BasicAuthCredential(iCred)); + + WorkItemStore workItemStore = (WorkItemStore)tpc.GetService(typeof(WorkItemStore)); + + WorkItemCollection queryResults = workItemStore.Query(string.Format("Select [Id], [Work Item Type], [Title], [State] From WorkItems WHERE [Id] = '{0}' Order By [Id] Asc", id)); + + if (queryResults.Count >= 1) + { + var item = queryResults[0]; + + try + { + TswaClientHyperlinkService hyperlinkService = (TswaClientHyperlinkService)tpc.GetService(typeof(TswaClientHyperlinkService)); + url = hyperlinkService.GetWorkItemEditorUrl(item.Id).ToString(); + } + catch + { + } + + return item; + } + } + + return null; + } + + */ + } +} diff --git a/TFSAzurePicker/TfsPickerModel.cs b/TFSAzurePicker/TfsPickerModel.cs new file mode 100644 index 0000000..0692ece --- /dev/null +++ b/TFSAzurePicker/TfsPickerModel.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TFSAzurePicker +{ + public class TfsPickerModel + { + public Dictionary TfsPickModel { get; set; } + } + + public class TfsPickerItem + { + public int Id { get; set; } + public string TypeName { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string ProjectName { get; set; } + } +} diff --git a/TFSAzurePicker/UserWidgetDataDetails.cs b/TFSAzurePicker/UserWidgetDataDetails.cs new file mode 100644 index 0000000..a29e933 --- /dev/null +++ b/TFSAzurePicker/UserWidgetDataDetails.cs @@ -0,0 +1,20 @@ +using System; + +namespace TFSAzurePicker +{ + public class UserWidgetDataDetails + { + + public string Organisation { get; set; } + public string Project { get; set; } + + + public string RepositoryUrl { get; set; } + public string Username { get; set; } + public string Password { get; set; } + + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public DateTime Expiry { get; internal set; } + } +} diff --git a/TFSAzurePicker/app.config b/TFSAzurePicker/app.config new file mode 100644 index 0000000..ca4b097 --- /dev/null +++ b/TFSAzurePicker/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TFSAzurePicker/app.manifest b/TFSAzurePicker/app.manifest new file mode 100644 index 0000000..b97e07c --- /dev/null +++ b/TFSAzurePicker/app.manifest @@ -0,0 +1,12 @@ + + 3af0b23d-177f-43f4-b663-12368289f3c2 + TFS Azure Picker + TFS Azure Picker + 7 + 0 + 0 + Countersoft + 2021-05-28T09:00:00Z + + true + diff --git a/TFSAzurePicker/packages.config b/TFSAzurePicker/packages.config new file mode 100644 index 0000000..63f6139 --- /dev/null +++ b/TFSAzurePicker/packages.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TFSAzurePicker/screenshot.png b/TFSAzurePicker/screenshot.png new file mode 100644 index 0000000..de91c55 Binary files /dev/null and b/TFSAzurePicker/screenshot.png differ diff --git a/TFSAzurePicker/tfsapp.js b/TFSAzurePicker/tfsapp.js new file mode 100644 index 0000000..d4f249a --- /dev/null +++ b/TFSAzurePicker/tfsapp.js @@ -0,0 +1,111 @@ +var tfsSearch = { + issueId: 0, + appId: '', + controlId: '', + // Called when the content widget renders + init: function (id, appId, controlId) { + tfsSearch.issueId = id; + tfsSearch.appId = appId; + tfsSearch.controlId = controlId; + }, + + // Add tfs from the item page + add: function (id) + { + $("#tfs-add").click(function (e) { + + gemini_ui.startBusy('#tfs-add'); + + var id = ""; + $("#table-search-tfs input[type='checkbox']:checked").each( + function () { + id = $(this).closest("td").attr("id") + "," + id; + }); + + gemini_ajax.postCall('apps/tfsazurepicker', 'add', function (response) { + if (response.Success) { + gemini_popup.toast("Added"); + gemini_item.getAppControlValue(tfsSearch.issueId, tfsSearch.appId, tfsSearch.controlId, 'add'); + } + gemini_ui.stopBusy('#tfs-add'); + }, function () { gemini_ui.stopBusy('#tfs-add'); }, { issueId: tfsSearch.issueId, tfsId: id }, null, true); + + }); + + }, + + + logout: function(id) + { + $('#tfspicker-logout').click(function () { + gemini_ajax.postCall("apps/tfsazurepicker", 'logout', function (response) { + gemini_ui.startBusy('#tfspicker-logout'); + var issueid = tfsSearch.issueId; + if (response.success) { + gemini_item.getAppControlValue(issueid, tfsSearch.appId, tfsSearch.controlId, 'logout'); + } + else { + if (response != '') gemini_popup.toast(response, true); + } + gemini_ui.stopBusy('#tfspicker-logout'); + }, function () { gemini_ui.stopBusy('#tfspicker-logout'); }, null, null, true); + }); + }, + + deletetfs: function (id) + { + $("#tfs-details").off('click', ".action-button-delete").on('click', ".action-button-delete", function (e) { + + + var tfs = $(this).closest("span").attr("data-tfs"); + var issueid = tfsSearch.issueId; + + gemini_popup.modalConfirm("Are you sure to delete TFS ID-"+tfs+"?", null, function () { + + gemini_ui.startBusy('#modal-confirm #modal-button-yes'); + gemini_ajax.postCall('apps/tfsazurepicker', 'delete', function (response) { + if (response.Success) { + gemini_popup.toast("Deleted"); + gemini_item.getAppControlValue(issueid, tfsSearch.appId, tfsSearch.controlId, 'delete'); + gemini_ui.stopBusy('#modal-confirm #modal-button-yes'); + } + }, function () { gemini_ui.stopBusy('#modal-confirm #modal-button-yes'); }, { issueId: issueid, tfsRow: tfs }, null, true); + + }); + + }); + }, + + search: function (id) + { + $("#tfs-search").click(function (e) { + + gemini_ui.startBusy('#tfs-search'); + var match = $('#tfs-find').val(); + // do something + gemini_ajax.postCall('apps/tfsazurepicker', 'search', function (response) { + if (response.Result.Data.success) { + gemini_ui.stopBusy('#tfs-search'); + + $("#tfs-search-results").html(response.Result.Data.data.Content); + + $('#tfs-details tbody tr').each(function (key, value) { + if ($('.tfs-' + $(this).attr('data-id'), $('#table-search-tfs')).length > 0) { + $('.tfs-' + $(this).attr('data-id'), $('#table-search-tfs')).remove(); + } + }); + } + else + { + $('#app-tfspicker').html(response.Result.Data.data.Content); + $('#app-tfspicker #app-tfspicker-error').html(response.Result.Data.message); + $('#app-tfspicker-error').show(); + } + gemini_item.setContentHeight(); + gemini_ui.stopBusy('#tfs-search'); + }, function () { gemini_ui.stopBusy('#tfs-search'); }, { id: id, search: match }, null, true); + }); + }, +}; + +//# sourceURL=tfsapp.js \ No newline at end of file diff --git a/TFSAzurePicker/views/Web.config b/TFSAzurePicker/views/Web.config new file mode 100644 index 0000000..1f0ddf7 --- /dev/null +++ b/TFSAzurePicker/views/Web.config @@ -0,0 +1,76 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TFSAzurePicker/views/authenticationForm.cshtml b/TFSAzurePicker/views/authenticationForm.cshtml new file mode 100644 index 0000000..2691117 --- /dev/null +++ b/TFSAzurePicker/views/authenticationForm.cshtml @@ -0,0 +1,86 @@ +@using System.Web.Mvc.Html; +@using System.Linq; +@using Countersoft.Gemini.Models; +@using Countersoft.Gemini; +@using System.Linq +@using TFSAzurePicker; + +
+ @if ( Countersoft.Gemini.GeminiApp.LicenseSummary.IsGeminiTrial() || Countersoft.Gemini.GeminiApp.GeminiLicense.IsFree ) + { +
+
+ +
+ } + + @if ( Model.NoOAuthClient ) + { +
+ No OAuth clients configured. Please configure Configuration | System | Authentications | OAuth +
+ } + + else + { +
+
+
+

Build Azure Url

+

https://dev.azure.com/{organisation}

+
+ https://dev.azure.com/ + @**@ + +
+
+ } + + +
+ + \ No newline at end of file diff --git a/TFSAzurePicker/views/items.cshtml b/TFSAzurePicker/views/items.cshtml new file mode 100644 index 0000000..ce76fed --- /dev/null +++ b/TFSAzurePicker/views/items.cshtml @@ -0,0 +1,78 @@ +@using System.Web.Mvc.Html; +@using Countersoft.Gemini.Models; +@using Countersoft.Gemini; +@using System.Linq; +@using System.Web; +@using TFSPicker; +@using TFSAzurePicker; +@model Dictionary + + + +
+ +
+ @if (Model != null && Model.Count() > 0) + { + + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + + } + +
IDTypeTitleDescriptionCollection
@item.Value.Id@item.Value.TypeName@item.Value.Title@Html.Raw(item.Value.Description)@item.Value.ProjectName + +
+ } + else if (Countersoft.Gemini.GeminiApp.LicenseSummary.IsGeminiTrial() || Countersoft.Gemini.GeminiApp.GeminiLicense.IsFree) + { +
+
+ +
+ } +

+ +
+ + +
+ +
+
+ + diff --git a/TFSAzurePicker/views/search.cshtml b/TFSAzurePicker/views/search.cshtml new file mode 100644 index 0000000..f33b47d --- /dev/null +++ b/TFSAzurePicker/views/search.cshtml @@ -0,0 +1,56 @@ +@using System.Web; +@using TFSAzurePicker; +@model Dictionary + + + +

+ + @if (Model != null && Model.Count > 0) + { + + + + + + + + + + + @foreach (var tfs in Model) + { + + + + + + + + + } + +
IDTypeTitleDescriptionCollection
@Html.CheckBox("SelectTFS", false, new { @class = "fancy" })@tfs.Value.Id@tfs.Value.TypeName@tfs.Value.Title@Html.Raw(tfs.Value.Description)@tfs.Value.ProjectName
+ } + else + { +
No Results Found
+ } +

+ +
+ +
+ + diff --git a/TFSPicker.sln b/TFSPicker.sln index 75bb5e0..73d3359 100644 --- a/TFSPicker.sln +++ b/TFSPicker.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30110.0 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.1500 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TFSPicker", "TFSPicker.csproj", "{42C2D76D-1827-4389-95D9-AF6AAC4769EE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TFSAzurePicker", "TFSAzurePicker\TFSAzurePicker.csproj", "{6C9851AB-A979-4F55-88EB-21BD9E51DC83}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Build|Any CPU = Build|Any CPU @@ -18,8 +20,17 @@ Global {42C2D76D-1827-4389-95D9-AF6AAC4769EE}.Debug|Any CPU.Build.0 = Debug|Any CPU {42C2D76D-1827-4389-95D9-AF6AAC4769EE}.Release|Any CPU.ActiveCfg = Release|Any CPU {42C2D76D-1827-4389-95D9-AF6AAC4769EE}.Release|Any CPU.Build.0 = Release|Any CPU + {6C9851AB-A979-4F55-88EB-21BD9E51DC83}.Build|Any CPU.ActiveCfg = Build|Any CPU + {6C9851AB-A979-4F55-88EB-21BD9E51DC83}.Build|Any CPU.Build.0 = Build|Any CPU + {6C9851AB-A979-4F55-88EB-21BD9E51DC83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C9851AB-A979-4F55-88EB-21BD9E51DC83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C9851AB-A979-4F55-88EB-21BD9E51DC83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C9851AB-A979-4F55-88EB-21BD9E51DC83}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C47C9BC2-CA76-46C5-BDC3-3F1314430BCC} + EndGlobalSection EndGlobal diff --git a/libs/Countersoft.Foundation.Commons.dll b/libs/Countersoft.Foundation.Commons.dll index b328194..29bf235 100644 Binary files a/libs/Countersoft.Foundation.Commons.dll and b/libs/Countersoft.Foundation.Commons.dll differ diff --git a/libs/Countersoft.Foundation.Contracts.dll b/libs/Countersoft.Foundation.Contracts.dll index b1f720c..fc07ed4 100644 Binary files a/libs/Countersoft.Foundation.Contracts.dll and b/libs/Countersoft.Foundation.Contracts.dll differ diff --git a/libs/Countersoft.Gemini.Authentication.dll b/libs/Countersoft.Gemini.Authentication.dll new file mode 100644 index 0000000..5de4ad2 Binary files /dev/null and b/libs/Countersoft.Gemini.Authentication.dll differ diff --git a/libs/Countersoft.Gemini.Commons.dll b/libs/Countersoft.Gemini.Commons.dll index e6d2f22..5dd08c0 100644 Binary files a/libs/Countersoft.Gemini.Commons.dll and b/libs/Countersoft.Gemini.Commons.dll differ diff --git a/libs/Countersoft.Gemini.Contracts.dll b/libs/Countersoft.Gemini.Contracts.dll index bd8e543..7e5c473 100644 Binary files a/libs/Countersoft.Gemini.Contracts.dll and b/libs/Countersoft.Gemini.Contracts.dll differ diff --git a/libs/Countersoft.Gemini.Extensibility.dll b/libs/Countersoft.Gemini.Extensibility.dll index 66ba1f5..f442654 100644 Binary files a/libs/Countersoft.Gemini.Extensibility.dll and b/libs/Countersoft.Gemini.Extensibility.dll differ diff --git a/libs/Countersoft.Gemini.dll b/libs/Countersoft.Gemini.dll index 0d0512c..b9ad5d4 100644 Binary files a/libs/Countersoft.Gemini.dll and b/libs/Countersoft.Gemini.dll differ