Changeset 5887289614d0…
by Lou Manglass
Changes to 60 files · Browse files at 5887289614d0 Diff from another changeset...
|
@@ -0,0 +1,15 @@ + syntax: glob
+
+## User-specific files
+
+*.suo
+*.user
+
+## Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+build/
+[Bb]in/
+[Oo]bj/
|
|
@@ -0,0 +1,51 @@ + using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using FogBugzForVisualStudio.Api;
+
+namespace FogBugzForVisualStudio.Test
+{
+ class Locale : IDisposable {
+ private CultureInfo current;
+
+ public Locale(CultureInfo culture) {
+ current = Thread.CurrentThread.CurrentCulture;
+ Thread.CurrentThread.CurrentCulture = culture;
+ }
+
+ public void Dispose()
+ {
+ Thread.CurrentThread.CurrentCulture = current;
+ }
+ }
+
+ [TestClass]
+ public class ApiTests
+ {
+ [TestMethod]
+ public void CreateBug()
+ {
+ using(new Locale(CultureInfo.InvariantCulture))
+ {
+ var bug = new Case(1, Case.Op.resolve, new MockBug().Dict, new MockClient());
+ }
+ }
+
+ [TestMethod]
+ public void CreateBugLocalized()
+ {
+ using (new Locale(new CultureInfo("fr-FR")))
+ {
+ var bug = new Case(1, Case.Op.resolve, new MockBug{
+ {"hrsElapsed", "0.5"}
+ }.Dict, new MockClient());
+ }
+ }
+ }
+}
|
Change 1 of 1
|
||
---|---|---|
|
@@ -0,0 +1,66 @@ + <?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProductVersion>
+ </ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{91F54069-7C06-4327-9A18-D4DC6BDA3114}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>FogBugzForVisualStudio.Test</RootNamespace>
+ <AssemblyName>FogBugzForVisualStudio.Test</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
+ <Reference Include="System" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ </ItemGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Mocks.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="ApiTests.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\FogBugzForVisualStudio\FogBugzForVisualStudio.csproj">
+ <Project>{1261AF02-2638-4D92-AF2C-8112C72E46E7}</Project>
+ <Name>FogBugzForVisualStudio</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project>
\ No newline at end of file |
|
@@ -0,0 +1,66 @@ + using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+using FogBugzForVisualStudio.Api;
+
+namespace FogBugzForVisualStudio.Test
+{
+ class MockClient : FogBugzClient
+ {
+ public override Person GetPerson(int ixPerson)
+ {
+ return null;
+ }
+
+ public override Status GetStatus(int ixStatus)
+ {
+ return null;
+ }
+
+ public override Category GetCategory(int ixCategory)
+ {
+ return null;
+ }
+ }
+
+ class MockBug : System.Collections.IEnumerable
+ {
+
+ private Dictionary<string, string> fields = new Dictionary<string, string>();
+ public Dictionary<string, string> Dict { get { return fields; } }
+
+ private void Fill(String s, string[] keys)
+ {
+ foreach (var key in keys)
+ {
+ fields[key] = s;
+ }
+ }
+
+ public MockBug()
+ {
+ Fill("0", new string[]{
+ "ixBugParent", "ixBugChildren", "ixBugEventLatest", "ixBugEventLastView", "ixCategory",
+ "ixPriority", "ixStatus", "ixProject", "ixArea", "ixFixFor", "ixPersonAssignedTo",
+ "ixPersonOpenedBy", "ixPersonResolvedBy", "ixPersonClosedBy", "ixPersonLastEditedBy",
+ Case.BacklogFieldName
+ });
+
+ Fill("", new string[] { "sProject", "sArea", "sFixFor", "sPriority", "sTitle" });
+ Fill("0", new string[] { "hrsOrigEst", "hrsCurrEst", "hrsElapsed" });
+ Fill(null, new string[] { "dtOpened", "dtResolved", "dtClosed", "dtDue", "dtLastUpdated" });
+ }
+
+ public void Add(string key, string value)
+ {
+ fields[key] = value;
+ }
+
+ public System.Collections.IEnumerator GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
|
|
@@ -0,0 +1,35 @@ + using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("FogBugzForVisualStudio.Test")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Microsoft")]
+[assembly: AssemblyProduct("FogBugzForVisualStudio.Test")]
+[assembly: AssemblyCopyright("Copyright © Microsoft 2010")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("10b12626-e388-4cb3-b7d3-fc8ced5ce83e")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
|
|
@@ -0,0 +1,36 @@ +
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FogBugzForVisualStudio", "FogBugzForVisualStudio\FogBugzForVisualStudio.csproj", "{1261AF02-2638-4D92-AF2C-8112C72E46E7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FogBugzForVisualStudio.Test", "FogBugzForVisualStudio.Test\FogBugzForVisualStudio.Test.csproj", "{91F54069-7C06-4327-9A18-D4DC6BDA3114}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{67FAB558-D949-40E1-A588-C9B7912068D8}"
+ ProjectSection(SolutionItems) = preProject
+ FogBugzForVisualStudio.vsmdi = FogBugzForVisualStudio.vsmdi
+ Local.testsettings = Local.testsettings
+ TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(TestCaseManagementSettings) = postSolution
+ CategoryFile = FogBugzForVisualStudio.vsmdi
+ EndGlobalSection
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1261AF02-2638-4D92-AF2C-8112C72E46E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {91F54069-7C06-4327-9A18-D4DC6BDA3114}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Area
+ {
+ public readonly int ixArea;
+ public readonly string sArea;
+
+ public Area(int ixArea, string sArea)
+ {
+ this.ixArea = ixArea;
+ this.sArea = sArea;
+ }
+
+ public override string ToString()
+ {
+ return sArea;
+ }
+ }
+}
|
|
|
@@ -0,0 +1,172 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Drawing;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Case
+ {
+ [FlagsAttribute]
+ public enum Op
+ {
+ edit = 0x0001,
+ spam = 0x0002,
+ assign = 0x0004,
+ resolve = 0x0008,
+ move = 0x0010,
+ reactivate = 0x0020,
+ close = 0x0040,
+ reopen = 0x0080,
+ remind = 0x0100,
+ reply = 0x0200,
+ forward = 0x0400,
+ email = 0x0800
+ }
+
+ public int ixBug { get; private set; }
+ public Op ops { get; private set; }
+
+ public int? ixBugParent { get; private set; }
+ public IList<int> ixBugChildren { get; private set; }
+
+ public int ixBugEventLatest { get; private set; }
+ public int ixBugEventLastView { get; private set; }
+
+ public Category category { get; private set; }
+ public Priority priority { get; private set; }
+ public Status status { get; private set; }
+ public Project project { get; private set; }
+ public Area area { get; private set; }
+ public FixFor fixfor { get; private set; }
+
+ public string sTitle { get; private set; }
+
+ public Person assignedTo { get; private set; }
+ public Person openedBy { get; private set; }
+ public Person resolvedBy { get; private set; }
+ public Person closedBy { get; private set; }
+ public Person lastEditedBy { get; private set; }
+
+ public Estimate hrsOrigEst { get; private set; }
+ public Estimate hrsCurrEst { get; private set; }
+ public Estimate hrsElapsed { get; private set; }
+ public Estimate hrsRemaining
+ {
+ get
+ {
+ return new Estimate(Math.Max(this.hrsCurrEst.hrs - this.hrsElapsed.hrs, Decimal.Zero));
+ }
+ }
+
+ public Nullable<DateTime> dtOpened { get; private set; }
+ public Nullable<DateTime> dtResolved { get; private set; }
+ public Nullable<DateTime> dtClosed { get; private set; }
+ public Nullable<DateTime> dtDue { get; private set; }
+ public Nullable<DateTime> dtLastUpdated { get; private set; }
+
+ public int? iBacklog { get; private set; }
+
+ public static string BacklogFieldName = "plugin_projectbacklog_at_fogcreek_com_ibacklog";
+
+ public Case(int ixBug, Op ops, Dictionary<String, String> fields, FogBugzClient parentClient)
+ {
+ this.ixBug = ixBug;
+ this.ops = ops;
+
+ if (!String.IsNullOrEmpty(fields["ixBugParent"]))
+ {
+ this.ixBugParent = Convert.ToInt32(fields["ixBugParent"]);
+ if (this.ixBugParent <= 0) this.ixBugParent = null;
+ }
+ else
+ {
+ this.ixBugParent = null;
+ }
+
+ this.ixBugChildren = new List<int>();
+ if (!String.IsNullOrEmpty(fields["ixBugChildren"]))
+ {
+ foreach (string ix in fields["ixBugChildren"].Split(','))
+ {
+ this.ixBugChildren.Add(Convert.ToInt32(ix));
+ }
+ }
+
+ this.ixBugEventLatest = Convert.ToInt32(fields["ixBugEventLatest"]);
+ this.ixBugEventLastView = Convert.ToInt32(fields["ixBugEventLastView"]);
+
+ this.category = parentClient.GetCategory(Convert.ToInt32(fields["ixCategory"]));
+ this.priority = new Priority(Convert.ToInt32(fields["ixPriority"]), fields["sPriority"]);
+ this.status = parentClient.GetStatus(Convert.ToInt32(fields["ixStatus"]));
+ this.project = new Project(Convert.ToInt32(fields["ixProject"]), fields["sProject"]);
+ this.area = new Area(Convert.ToInt32(fields["ixArea"]), fields["sArea"]);
+ this.fixfor = new FixFor(Convert.ToInt32(fields["ixFixFor"]), fields["sFixFor"]);
+
+ this.sTitle = fields["sTitle"];
+
+ this.assignedTo = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonAssignedTo"]));
+ this.openedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonOpenedBy"]));
+ this.resolvedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonResolvedBy"]));
+ this.closedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonClosedBy"]));
+ this.lastEditedBy = parentClient.GetPerson(Convert.ToInt32(fields["ixPersonLastEditedBy"]));
+
+ this.hrsOrigEst = Estimate.Parse(fields["hrsOrigEst"]);
+ this.hrsCurrEst = Estimate.Parse(fields["hrsCurrEst"]);
+ this.hrsElapsed = Estimate.Parse(fields["hrsElapsed"]);
+
+ this.dtOpened = Util.ParseApiDate(fields["dtOpened"]);
+ this.dtResolved = Util.ParseApiDate(fields["dtResolved"]);
+ this.dtClosed = Util.ParseApiDate(fields["dtClosed"]);
+ this.dtDue = Util.ParseApiDate(fields["dtDue"]);
+ this.dtLastUpdated = Util.ParseApiDate(fields["dtLastUpdated"]);
+
+ if (fields.ContainsKey(BacklogFieldName) && !String.IsNullOrEmpty(fields[BacklogFieldName]))
+ {
+ this.iBacklog = Convert.ToInt32(fields[BacklogFieldName]);
+ }
+ }
+
+ /// <summary>
+ /// DataGridView's binding doesn't support subkeys. So cheat and add a shortcut.
+ /// </summary>
+ public Bitmap categoryImage
+ {
+ get
+ {
+ return category.Image;
+ }
+ }
+
+ public bool Unread
+ {
+ get
+ {
+ return ixBugEventLastView < ixBugEventLatest;
+ }
+ set
+ {
+ // An exception to our usual immutability, since the user might view a case
+ if (value)
+ {
+ ixBugEventLastView = ixBugEventLatest;
+ }
+ else
+ {
+ ixBugEventLastView = 0;
+ }
+ }
+ }
+
+ public void SetNewEstimate(Estimate e)
+ {
+ // A rare exception to Bug's immutability. Thus make a special wrapper method to set this.
+ hrsCurrEst = e;
+ }
+
+ internal void WorkStarted()
+ {
+ hrsElapsed = new Estimate(0.01m);
+ }
+ }
+}
|
|
|
@@ -0,0 +1,112 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Drawing;
+using System.Resources;
+using System.Reflection;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Category
+ {
+ private class IconTypeImageAttribute : Attribute
+ {
+ public readonly string FileName;
+
+ public IconTypeImageAttribute(String fileName)
+ {
+ this.FileName = fileName;
+ }
+ }
+
+ public enum IconType
+ {
+ [IconTypeImageAttribute("icon_none")]
+ None = 0,
+ [IconTypeImageAttribute("icon_bug")]
+ Bug = 1,
+ [IconTypeImageAttribute("icon_feature")]
+ Feature = 2,
+ [IconTypeImageAttribute("icon_inquiry")]
+ Inquiry = 3,
+ [IconTypeImageAttribute("icon_scheduleitem")]
+ ScheduleItem = 4,
+ [IconTypeImageAttribute("icon_bug_error")]
+ BugError = 5,
+ [IconTypeImageAttribute("icon_wrench")]
+ Wrench = 6,
+ [IconTypeImageAttribute("icon_magnifier")]
+ Magnifier = 7,
+ [IconTypeImageAttribute("icon_key")]
+ Key = 8,
+ [IconTypeImageAttribute("icon_error")]
+ Error = 9
+ }
+
+ public int ixCategory { get; private set; }
+ public string sCategory { get; private set; }
+ public bool fDeleted { get; private set; }
+ public int ixStatusDefault { get; private set; }
+ public IconType nIconType { get; private set; }
+
+ public Category(Dictionary<String, String> fields)
+ {
+ this.ixCategory = Convert.ToInt32(fields["ixCategory"]);
+ this.sCategory = fields["sCategory"];
+ this.fDeleted = Convert.ToBoolean(fields["fDeleted"]);
+ this.ixStatusDefault = Convert.ToInt32(fields["ixStatusDefault"]);
+ try
+ {
+ this.nIconType = (IconType)Enum.Parse(typeof(IconType), fields["nIconType"]);
+ }
+ catch
+ {
+ // Ignore in case more icons are added later.
+ }
+ }
+
+ public override string ToString()
+ {
+ return sCategory;
+ }
+
+ private static Dictionary<IconType, Bitmap> imageCache;
+
+ /// <summary>
+ /// Get the Category image. Properly belongs in the view, but .NET's bindings give us grief
+ /// if we try to format an image in the DataGridView.CellFormatting event. Just give in and
+ /// put it in the model.
+ /// </summary>
+ public Bitmap Image
+ {
+ get
+ {
+ if (imageCache == null)
+ {
+ imageCache = new Dictionary<IconType, Bitmap>();
+ ResourceManager manager = new ResourceManager("FogBugzForVisualStudio.Resource1", GetType().Assembly);
+
+ foreach (FieldInfo field in typeof(IconType).GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public))
+ {
+ object[] attrs = field.GetCustomAttributes(true);
+ string fileName = null;
+ foreach (object attr in attrs)
+ {
+ if (attr.GetType().Equals(typeof(IconTypeImageAttribute)))
+ {
+ fileName = ((IconTypeImageAttribute)attr).FileName;
+ break;
+ }
+ }
+
+ if (fileName != null)
+ {
+ imageCache[(IconType)field.GetValue(null)] = (Bitmap)manager.GetObject(fileName);
+ }
+ }
+ }
+ return imageCache[nIconType];
+ }
+ }
+ }
+}
|
|
@@ -0,0 +1,44 @@ + using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Estimate
+ {
+ public decimal hrs { get; private set; }
+
+ public Estimate(decimal hrs)
+ {
+ this.hrs = hrs;
+ }
+
+ public static Estimate Parse(String s) {
+ return new Estimate(Convert.ToDecimal(s, CultureInfo.InvariantCulture));
+ }
+
+ public override string ToString()
+ {
+ if (hrs == 0) return "";
+
+ // Based on SFromHours in util.was
+ int nHours = Convert.ToInt32(Decimal.Floor(hrs));
+ int nMinutes = Convert.ToInt32(Decimal.Floor((hrs - nHours) * 60));
+
+ if (nHours == 0 && nMinutes == 0)
+ {
+ return "0 hours";
+ }
+ else if (nHours == 0)
+ {
+ return nMinutes == 1 ? "1 minute" : (nMinutes + " minutes");
+ }
+ else
+ {
+ double h = Math.Round(nHours + nMinutes/60.0, 2);
+ return h == 1 ? "1 hour" : (h + " hours");
+ }
+ }
+ }
+}
|
|
@@ -0,0 +1,21 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Filter
+ {
+ public enum FilterType
+ {
+ builtin = 1,
+ saved = 2,
+ shared = 4,
+ }
+
+ public FilterType type;
+ public string codeFilter; // opaque code
+ public string s; // user-visible name
+ public bool fIsCurrent; // is this the user's current filter?
+ }
+}
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class FixFor
+ {
+ public readonly int ixFixFor;
+ public readonly string sFixFor;
+
+ public FixFor(int ixFixFor, string sFixFor)
+ {
+ this.ixFixFor = ixFixFor;
+ this.sFixFor = sFixFor;
+ }
+
+ public override string ToString()
+ {
+ return sFixFor;
+ }
+ }
+}
|
|
|
@@ -0,0 +1,1085 @@ + using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+using System.Xml;
+
+
+namespace FogBugzForVisualStudio.Api
+{
+ /// <summary>
+ /// Event fires when Logon is done.
+ /// </summary>
+ /// <param name="bSuccess">true if successful</param>
+ /// <param name="sError">specific error message, if unsuccessful</param>
+ /// <param name="rgNames">if unsuccessful because sEmail was ambiguous, a list of full names user must choose between</param>
+ public delegate void LogonHandler(bool bSuccess, string sError, StringCollection rgNames);
+
+ /// <summary>
+ /// Event fires when List is done.
+ /// </summary>
+ /// <param name="bSuccess">true if successful</param>
+ /// <param name="sError">specific error message, if unsuccessful</param>
+ /// <param name="sListDescription">e.g. "All open cases assigned to Sophia"</param>
+ /// <param name="rgCases">the cases themselves</param>
+ public delegate void ListCasesHandler(bool bSuccess, string sError, string sListDescription, List<Case> rgCasesInCurrentFilter, List<Case>rgParentAndSubcases);
+
+ /// <summary>
+ /// Event fires if user is suddenly discovered to have logged off
+ /// (logon token no longer works)
+ /// </summary>
+ public delegate void UserLoggedOffHandler();
+
+ /// <summary>
+ /// Event fires when ListFilters is done
+ /// </summary>
+ /// <param name="bSuccess"></param>
+ /// <param name="sError"></param>
+ /// <param name="rgFilters"></param>
+ public delegate void ListFiltersHandler(bool bSuccess, string sError, List<Filter> rgFilters);
+
+ /// <summary>
+ /// Event fires when ListIntervals is done
+ /// </summary>
+ /// <param name="bSuccess"></param>
+ /// <param name="rgIntervals"></param>
+ public delegate void ListIntervalsHandler(bool bSuccess, string sError, List<Interval> rgIntervals);
+
+ /// <summary>
+ /// Event fires *instead of* the event you were expecting
+ /// (LogonHandler, ListCasesHandler, UserLoggedOffHandler, etc)
+ /// to tell you that your Stop() request has been honored
+ /// </summary>
+ public delegate void AsyncOperationCancelled();
+
+ /// <summary>
+ /// A C# class that implements a client to the FogBugz version 1 API.
+ /// </summary>
+ public class FogBugzClient
+ {
+ private string _sEmail;
+ private string _sPassword;
+ private string _sToken;
+ private WebClient _wc = null;
+ private string _lastCertError = "";
+ private bool _retryWithHttp = false;
+
+ public string sToken
+ {
+ get { return _sToken; }
+ }
+
+ /// <summary>
+ /// The URL of FogBugz itself
+ /// </summary>
+ private string _url = "" ;
+ private string _params = "";
+
+ private Dictionary<int, Person> dictPeople;
+ private Dictionary<int, Status> dictStatuses;
+ private Dictionary<int, Category> dictCategories;
+
+ public virtual Person GetPerson(int ixPerson)
+ {
+ return (dictPeople.ContainsKey(ixPerson)) ? dictPeople[ixPerson] : null;
+ }
+
+ public virtual Status GetStatus(int ixStatus)
+ {
+ return (dictStatuses.ContainsKey(ixStatus)) ? dictStatuses[ixStatus] : null;
+ }
+
+ public virtual Category GetCategory(int ixCategory)
+ {
+ return (dictCategories.ContainsKey(ixCategory)) ? dictCategories[ixCategory] : null;
+ }
+
+ /// <summary>
+ /// The complete url of the API, e.g. "http://www.example.com/fogbugz/api.asp?"
+ /// Not set until CheckAPIVersion has succeeded
+ /// </summary>
+ private string _urlApi = "";
+
+ public string sApiUrl
+ {
+ get { return _urlApi; }
+ }
+
+ public bool? fHasBacklogPlugin;
+
+ public event LogonHandler OnLogon;
+ public event ListCasesHandler OnListCases;
+ public event UserLoggedOffHandler OnUserLoggedOff;
+ public event ListFiltersHandler OnListFilters;
+ public event ListIntervalsHandler OnListIntervals;
+ public event AsyncOperationCancelled OnStop;
+
+ public FogBugzClient()
+ {
+ }
+
+ public FogBugzClient(string sApiUrl, string sToken)
+ {
+ _urlApi = sApiUrl;
+ _sToken = sToken;
+
+ StartPostLogonInit();
+ }
+
+ public bool LoggedOn()
+ {
+ return !String.IsNullOrEmpty(_sToken) && !String.IsNullOrEmpty(_urlApi)
+ && dictPeople != null && dictStatuses != null && dictCategories != null;
+ }
+
+ /// <summary>
+ /// Stops any operation in progress
+ /// </summary>
+ public void Stop()
+ {
+ if (_wc != null)
+ {
+ _wc.CancelAsync();
+ }
+ }
+
+ /// <summary>
+ /// Starts the logon process asynchronously. Fires LogonCompleted when done.
+ /// </summary>
+ /// <param name="url">FogBugz URL</param>
+ /// <param name="sEmail">email address or full name if email address is ambiguous</param>
+ /// <param name="sPassword">password</param>
+ public void Logon( string url, string sEmail, string sPassword )
+ {
+ _url = url;
+ _url = _url.Replace("default.asp", "");
+ _url = _url.Replace("default.php", "");
+ if (_url.Contains("?"))
+ {
+ _params = _url.Substring(_url.IndexOf("?") + 1);
+ _url = _url.Substring(0, _url.IndexOf("?"));
+ }
+
+ if (!_url.EndsWith("/")) _url += "/";
+
+ _sEmail = sEmail;
+ _sPassword = sPassword;
+
+ System.Net.ServicePointManager.ServerCertificateValidationCallback = OnRemoteCertificateValidation;
+
+ if (Regex.IsMatch(_url, @"http://[^.]+\.fogbugz\.com/"))
+ {
+ _url = "https://" + _url.Substring("http://".Length);
+ _retryWithHttp = true;
+ }
+
+ CheckAPIVersion();
+ }
+
+ /// <summary>
+ /// Check that the server is really a FogBugz server with the right version of the API
+ /// installed
+ /// </summary>
+ public void CheckAPIVersion()
+ {
+ InitiateDownload("", CheckAPIVersion_DownloadStringCompleted,
+ delegate(string sErrorMessage) { OnLogon(false, sErrorMessage, null); });
+ }
+
+ private void CheckAPIVersion_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error == null)
+ {
+ int iVersion = 0;
+ int iMinVersion = 0;
+ int iClientVersionOnServer = 0;
+ string sURL = "";
+ XmlReader xml;
+
+ try
+ {
+ xml = GetXmlReader(e.Result);
+ }
+ catch
+ {
+ var fFBOD = _url.EndsWith(".fogbugz.com/");
+ var msg = fFBOD ?
+ "FogBugz On Demand is currently undergoing maintenance." :
+ "Malformed api.xml.";
+
+ OnLogon(false, msg, null);
+ return;
+ }
+
+
+ bool fContinue = xml.Read();
+ while (fContinue)
+ {
+ if (xml.NodeType == XmlNodeType.Element)
+ {
+ switch (xml.Name)
+ {
+ case "version":
+ iVersion = xml.ReadElementContentAsInt();
+ break;
+
+ case "minversion":
+ iMinVersion = xml.ReadElementContentAsInt();
+ break;
+
+ case "url":
+ sURL = xml.ReadElementContentAsString();
+ break;
+
+ case "private":
+ if (xml.GetAttribute("id") == "FogBugz for Visual Studio")
+ {
+ iClientVersionOnServer = Convert.ToInt32(xml.GetAttribute("version"));
+ }
+ fContinue = xml.Read();
+ break;
+
+ default:
+ fContinue = xml.Read();
+ break;
+ }
+ }
+ else
+ {
+ fContinue = xml.Read();
+ }
+ }
+
+ if (iVersion == 0 || iMinVersion == 0 || sURL.Length == 0)
+ {
+ OnLogon(false, "Incomplete or missing api.xml", null);
+ }
+ else if (iMinVersion > 1)
+ {
+ OnLogon(false, "That version of FogBugz is too new. Please re-install from the \"Extras\" menu in FogBugz", null);
+ }
+ else if (iClientVersionOnServer > 2)
+ {
+ OnLogon(false, "Newer version of add-in available. Please re-install from the \"Extras\" menu in FogBugz", null);
+ }
+ else
+ {
+ _urlApi = _url + sURL;
+ StartLogon();
+ }
+ }
+ else
+ {
+ if (_retryWithHttp && Regex.IsMatch(_url, @"https://[^.]+\.fogbugz\.com/"))
+ {
+ _url = "http://" + _url.Substring("https://".Length);
+ _retryWithHttp = false;
+ CheckAPIVersion();
+ }
+ else if (e.Error is WebException)
+ {
+ WebException we = (WebException)e.Error;
+ if (we.Status == WebExceptionStatus.TrustFailure)
+ {
+ // we can get better error messages for SSL failures.
+ OnLogon(false, _lastCertError, null);
+ }
+ else if (e.Error.Message.Contains("404"))
+ {
+ OnLogon(false, e.Error.Message + " FogBugz API not installed on server.", null);
+ }
+ else
+ {
+ OnLogon(false, e.Error.Message, null);
+ }
+ }
+ else
+ {
+ OnLogon(false, e.Error.Message, null);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Passed version check; start logging on
+ /// </summary>
+ private void StartLogon()
+ {
+ _sToken = ""; // clear any old tokens - otherwise logon won't happen if we're passing an invalid token
+ InitiateDownload( UrlParams("cmd","logon","email",_sEmail,"password",_sPassword),
+ StartLogon_DownloadStringCompleted,
+ delegate(string sErrorMessage) { OnLogon(false, sErrorMessage, null); } );
+ }
+
+ private void StartLogon_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result.Trim());
+
+ bool fAmbiguousLogon = false;
+ StringCollection rgsFullNames = new StringCollection();
+ string sErrAmbiguous = "";
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ xml.Read();
+ continue;
+ }
+
+ switch (xml.Name)
+ {
+ case "token":
+ _sToken = xml.ReadElementContentAsString();
+
+ // We've successfully logged on, so download a list of people.
+ StartPostLogonInit();
+
+ return;
+
+ case "error":
+ switch (xml.GetAttribute("code"))
+ {
+ case "1":
+ OnLogon(false, xml.ReadElementContentAsString(), null);
+ return;
+ case "2":
+ sErrAmbiguous = xml.ReadElementContentAsString();
+ fAmbiguousLogon = true;
+ continue;
+ default:
+ OnLogon(false, "Unknown Error: " + xml.ReadElementContentAsString(), null);
+ return;
+ }
+
+ case "person":
+ rgsFullNames.Add(xml.ReadElementContentAsString());
+ break;
+
+ default:
+ xml.Read();
+ break;
+ }
+ }
+
+ if (fAmbiguousLogon)
+ {
+ OnLogon(false, sErrAmbiguous, rgsFullNames);
+ }
+ else
+ {
+ OnLogon(false, "Unknown error; unrecognized XML response.", null);
+ }
+ }
+
+ private void StartPostLogonInit()
+ {
+ InitiateDownload(UrlParams("cmd", "listPeople", "fIncludeNormal", "1", "fIncludeVirtual", "1"),
+ ListPeople_DownloadStringCompleted,
+ delegate(string s) { OnLogon(false, "Unable to download list of people.", null); });
+ }
+
+ private void ListPeople_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ dictPeople = new Dictionary<int, Person>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element || xml.Name != "person")
+ {
+ xml.Read();
+ continue;
+ }
+
+ try
+ {
+ var fields = ReadSubElementsAsDictionary(xml);
+ var person = new Person(fields);
+ dictPeople[person.ixPerson] = person;
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnLogon(false, "API returned invalid <person>", null);
+ return;
+ }
+ }
+
+ InitiateDownload(UrlParams("cmd", "listStatuses"),
+ ListStatuses_DownloadStringCompleted,
+ delegate(string s) { OnLogon(false, "Unable to download a list of statuses", null); });
+ }
+
+ private void ListStatuses_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ dictStatuses = new Dictionary<int, Status>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element || xml.Name != "status")
+ {
+ xml.Read();
+ continue;
+ }
+
+ try
+ {
+ var fields = ReadSubElementsAsDictionary(xml);
+ var status = new Status(fields);
+ dictStatuses[status.ixStatus] = status;
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnLogon(false, "API returned invalid <status>", null);
+ return;
+ }
+ }
+
+ InitiateDownload(UrlParams("cmd", "listCategories"),
+ ListCategories_DownloadStringCompleted,
+ delegate(string s) { OnLogon(false, "Unable to download a list of categories", null); });
+ }
+
+ private void ListCategories_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnLogon(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ dictCategories = new Dictionary<int, Category>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element || xml.Name != "category")
+ {
+ xml.Read();
+ continue;
+ }
+
+ try
+ {
+ var fields = ReadSubElementsAsDictionary(xml);
+ var category = new Category(fields);
+ dictCategories[category.ixCategory] = category;
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnLogon(false, "API returned invalid <category>", null);
+ return;
+ }
+ }
+
+ OnLogon(true, "", null);
+ }
+
+ public void LogOff()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "logoff"),
+ null,
+ null);
+ _sToken = "";
+ }
+
+ public void SetCaseEstimate(Case c, string estimate)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+
+ var response = SynchronousDownload(UrlParams("cmd", "edit", "ixBug", c.ixBug.ToString(), "hrsCurrEst", estimate, "cols", "hrsCurrEst"));
+
+ XmlReader xml = GetXmlReader(response);
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.Element && xml.Name == "hrsCurrEst")
+ {
+ c.SetNewEstimate(new Estimate(xml.ReadElementContentAsDecimal()));
+ return;
+ }
+ else if (xml.NodeType == XmlNodeType.Element && xml.Name == "error")
+ {
+ throw new FogBugzErrorException(xml.ReadElementContentAsString(), Convert.ToInt32(xml.GetAttribute("code")));
+ }
+ else xml.Read();
+ }
+ }
+
+ private void CheckForErrors(string response)
+ {
+ var xml = GetXmlReader(response);
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.Element && xml.Name == "error")
+ {
+ throw new FogBugzErrorException(xml.ReadElementContentAsString(), Convert.ToInt32(xml.GetAttribute("code")));
+ }
+ else xml.Read();
+ }
+ }
+
+ public void StartWork(Case c)
+ {
+ StartWork(c.ixBug);
+ c.WorkStarted();
+ }
+
+ public void StartWork(int ixBug)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ CheckForErrors(SynchronousDownload(UrlParams("cmd", "startWork", "ixBug", ixBug.ToString())));
+ }
+
+ public void StopWork()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ CheckForErrors(SynchronousDownload(UrlParams("cmd", "stopWork")));
+ }
+
+ public void ListIntervals(DateTime? dtStart, DateTime? dtEnd)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "listIntervals", "cols", sListCols, "dtStart", dtStart.HasValue ? Util.FormatApiDate(dtStart.Value) : "", "dtEnd", dtEnd.HasValue ? Util.FormatApiDate(dtEnd.Value) : ""),
+ delegate(Object sender, DownloadStringCompletedEventArgs e) { ListIntervals_DownloadStringCompleted(sender, e); },
+ delegate(string s) { OnListIntervals(false, s, null); });
+ }
+
+ private void ListIntervals_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnListIntervals(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ var rgIntervals = new List<Interval>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ xml.Read();
+ continue;
+ }
+ switch (xml.Name)
+ {
+ case "error":
+ if (xml.GetAttribute("code") == "3")
+ {
+ OnListIntervals(false, xml.ReadElementContentAsString(), null);
+ OnUserLoggedOff();
+ }
+ else
+ {
+ OnListIntervals(false, xml.ReadElementContentAsString(), null);
+ }
+ return;
+
+ case "interval":
+ try
+ {
+ rgIntervals.Add(new Interval(ReadSubElementsAsDictionary(xml), this));
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnListIntervals(false, "API returned invalid <interval>", null);
+ return;
+ }
+ break;
+
+ default:
+ xml.Read();
+ break;
+ }
+ }
+
+ OnListIntervals(true, "", rgIntervals);
+ }
+
+ private static string sListCols = "ixBug,ixBugParent,ixBugChildren,sTitle,ixProject,sProject,ixArea,sArea,ixPersonAssignedTo,ixPersonOpenedBy,ixPersonResolvedBy,ixPersonClosedBy,ixPersonLastEditedBy,ixStatus,ixPriority,sPriority,ixFixFor,sFixFor,hrsOrigEst,hrsCurrEst,hrsElapsed,ixCategory,dtOpened,dtResolved,dtClosed,dtLastUpdated,dtDue,ixBugEventLatest,ixBugEventLastView,plugin";
+
+ /// <summary>
+ /// Starts downloading a list of cases in the user's current filter.
+ /// </summary>
+ public void ListCases()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload( UrlParams("cmd", "search", "cols", sListCols),
+ delegate(Object sender, DownloadStringCompletedEventArgs e) { ListCases_DownloadStringCompleted(sender, e, null, null, null); },
+ delegate(string s) { OnListCases(false, s, null, null, null); });
+ }
+
+ /// <summary>
+ /// Responds to a payload from a list cases command. This may be the main call from ListCases, listing the cases in the
+ /// user's current filter, or from a followup call that is resolving subcases and parent cases. The two cases can be
+ /// differentiated by rgCasesInCurrentFilter.
+ /// </summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ /// <param name="rgCasesInCurrentFilter">If null, the payload contains list of cases in current filter. Otherwise, payload contains assorted parent cases and subcases for the cases in this array.</param>
+ /// <param name="sDescription">If null, fill in from payload. Otherwise this is a supplementary call loading parent and subcases, and the value is already loaded.</param>
+ /// <param name="rgParentAndSubcases">It may take multiple passes to get a complete tree of cases (resolve all parent and subcases). If so, this list holds an intermediate array.</param>
+ private void ListCases_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e, List<Case> rgCasesInCurrentFilter, string sDescription, List<Case> rgParentAndSubcases)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnListCases(false, e.Error.Message, null, null, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+
+ if (rgParentAndSubcases == null)
+ {
+ rgParentAndSubcases = new List<Case>();
+ }
+
+ List<Case> rgCaseDestination;
+ if (rgCasesInCurrentFilter == null)
+ {
+ rgCasesInCurrentFilter = new List<Case>();
+ rgCaseDestination = rgCasesInCurrentFilter;
+ }
+ else
+ {
+ rgCaseDestination = rgParentAndSubcases;
+ }
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ xml.Read();
+ continue;
+ }
+ switch (xml.Name)
+ {
+ case "error":
+ if (xml.GetAttribute("code") == "3")
+ {
+ OnListCases(false, xml.ReadElementContentAsString(), null, null, null);
+ OnUserLoggedOff();
+ }
+ else
+ {
+ OnListCases(false, xml.ReadElementContentAsString(), null, null, null);
+ }
+ return;
+
+ case "description":
+ sDescription = xml.ReadElementContentAsString();
+ break;
+
+ case "case":
+ try
+ {
+ var ixBug = Convert.ToInt32(xml.GetAttribute("ixBug"));
+ var ops = (Case.Op)Enum.Parse(typeof(Case.Op), xml.GetAttribute("operations"));
+ var els = ReadSubElementsAsDictionary(xml);
+ fHasBacklogPlugin = els.ContainsKey(Case.BacklogFieldName);
+ rgCaseDestination.Add(new Case(ixBug, ops, els, this));
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ OnListCases(false, "API returned invalid <case>", null, null, null);
+ return;
+ }
+ break;
+
+ default:
+ xml.Read();
+ break;
+ }
+ }
+
+ var rgIxBugUnresolved = ComputeUnresolvedCases(rgCasesInCurrentFilter, rgParentAndSubcases);
+ if (rgIxBugUnresolved.Count > 0)
+ {
+ string sIxBugQuery = "";
+ foreach (int ixBug in rgIxBugUnresolved)
+ {
+ sIxBugQuery += "," + Convert.ToString(ixBug);
+ }
+ sIxBugQuery = sIxBugQuery.Substring(1);
+
+ Debug.WriteLine("Requesting bugs " + sIxBugQuery);
+
+ InitiateDownload( UrlParams("cmd", "search", "cols", sListCols, "q", sIxBugQuery),
+ delegate(Object sender2, DownloadStringCompletedEventArgs e2) { ListCases_DownloadStringCompleted(sender2, e2, rgCasesInCurrentFilter, sDescription, rgParentAndSubcases); },
+ delegate(string s) { OnListCases(false, s, null, null, null); });
+ }
+ else
+ {
+ OnListCases(true, "", sDescription, rgCasesInCurrentFilter, rgParentAndSubcases);
+ }
+ }
+
+ private List<int> ComputeUnresolvedCases(List<Case> rgCasesInCurrentFilter, List<Case> rgParentAndSubcases)
+ {
+ var dictAllCases = new Dictionary<int, Case>();
+
+ var queue = new Queue<Case>();
+ foreach (var c in rgCasesInCurrentFilter)
+ {
+ queue.Enqueue(c);
+ dictAllCases[c.ixBug] = c;
+ }
+ foreach (var c in rgParentAndSubcases)
+ {
+ queue.Enqueue(c);
+ dictAllCases[c.ixBug] = c;
+ }
+
+ // Dictionary with a dummy value: the poor man's Set class.
+ var dictUnresolved = new Dictionary<int, int>();
+ while (queue.Count > 0)
+ {
+ var c = queue.Dequeue();
+ if (c.ixBugParent.HasValue && !dictAllCases.ContainsKey((int)c.ixBugParent)) dictUnresolved[(int)c.ixBugParent] = 0;
+ foreach (var ixBugChild in c.ixBugChildren)
+ {
+ if (!dictAllCases.ContainsKey(ixBugChild)) dictUnresolved[ixBugChild] = 0;
+ }
+ }
+
+ var rgUnresolved = new List<int>();
+ foreach (var ixBug in dictUnresolved.Keys)
+ {
+ rgUnresolved.Add(ixBug);
+ }
+ return rgUnresolved;
+ }
+
+ private Dictionary<String,String> ReadSubElementsAsDictionary(XmlReader xml)
+ {
+ var fields = new Dictionary<String, String>();
+
+ if (xml.NodeType != XmlNodeType.Element)
+ {
+ throw new InvalidOperationException("ReadSubElementsAsDictionary was called when not positioned on an element");
+ }
+
+ String elementName = xml.Name;
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.EndElement && xml.Name == elementName)
+ {
+ xml.Read(); // Advance past end element, just like ReadElementContentAsString
+ return fields;
+ }
+
+ if (xml.NodeType == XmlNodeType.Element)
+ fields[xml.Name] = xml.ReadElementContentAsString();
+ else
+ xml.Read();
+ }
+
+ throw new XmlException("ReadSubElementsAsDictionary reached EOF without reaching end of element.");
+ }
+
+ public void ListFilters()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "listFilters"),
+ ListFilters_DownloadStringCompleted,
+ delegate(string s) { OnListFilters(false, s, null); });
+
+ }
+
+ private void ListFilters_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error != null)
+ {
+ OnListFilters(false, e.Error.Message, null);
+ return;
+ }
+
+ XmlReader xml = GetXmlReader(e.Result);
+ List<Filter> rgFilters = new List<Filter>();
+
+ xml.Read();
+ while (!xml.EOF)
+ {
+ if (xml.NodeType == XmlNodeType.Element && xml.Name == "filter")
+ {
+ Filter f = new Filter();
+ f.codeFilter = xml.GetAttribute("sFilter");
+ f.type = (Filter.FilterType)Enum.Parse(typeof(Filter.FilterType), xml.GetAttribute("type"));
+ f.fIsCurrent = (xml.GetAttribute("status") == "current");
+ f.s = xml.ReadElementContentAsString().Trim();
+ rgFilters.Add(f);
+ }
+ else
+ {
+ xml.Read();
+ }
+ }
+ OnListFilters(true, "", rgFilters);
+ }
+
+ public void SaveFilterAndList(string codeFilter)
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ InitiateDownload(UrlParams("cmd", "setCurrentFilter", "sFilter", codeFilter),
+ SaveFilter_DownloadStringCompleted,
+ delegate(string s) { OnListCases(false, s, null, null, null); });
+
+ }
+
+ private void SaveFilter_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
+ {
+ if (e.Cancelled)
+ {
+ OnStop();
+ return;
+ }
+
+ if (e.Error == null)
+ {
+ ListCases();
+ }
+ }
+
+ public string GetDefaultUrl()
+ {
+ if (!LoggedOn()) throw new FogBugzNotLoggedOnException();
+ return _urlApi.Replace("api.", "default.");
+ }
+
+ //
+ // UTILITY FUNCTIONS
+ //
+
+ private delegate void InitiateDownloadFailureHandler( string sErrorMessage );
+
+ /// <summary>
+ /// Initiates a download.
+ /// </summary>
+ /// <param name="sParams">GET params, for example, "cmd=logoff"; use UrlParams to generate. If blank, downloads api.xml.</param>
+ /// <param name="h">handler to call when string is received</param>
+ /// <param name="hFail">handler to call if exception is raised even before download begins</param>
+ private void InitiateDownload(string sParams,
+ DownloadStringCompletedEventHandler h,
+ InitiateDownloadFailureHandler hFail
+ )
+ {
+ _wc = new WebClient();
+ _wc.UseDefaultCredentials = true;
+ _wc.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ _wc.Encoding = Encoding.UTF8;
+
+ if (h != null)
+ {
+ _wc.DownloadStringCompleted += BugScout.CrashProof(h);
+ }
+
+ try
+ {
+ string surl = "";
+ if (sParams.Length == 0)
+ {
+ surl = _url + "api.xml?" + _params;
+ }
+ else
+ {
+ surl = _urlApi;
+ if (_params.Length > 0)
+ {
+ surl += _params + "&";
+ }
+ surl += sParams;
+ }
+
+ _wc.DownloadStringAsync(new Uri(surl));
+ }
+ catch(Exception e)
+ {
+ BugScout.ReportException(e);
+ if (hFail != null)
+ {
+ BugScout.CatchAndReport(() => {
+ hFail(e.Message);
+ });
+ }
+ }
+ }
+
+ private string SynchronousDownload(string sParams)
+ {
+ _wc = new WebClient();
+ _wc.UseDefaultCredentials = true;
+ _wc.Proxy.Credentials = CredentialCache.DefaultCredentials;
+ _wc.Encoding = Encoding.UTF8;
+
+ string surl = _urlApi;
+ if (_params.Length > 0)
+ {
+ surl += _params + "&";
+ }
+ surl += sParams;
+
+ return _wc.DownloadString(new Uri(surl));
+ }
+
+ private string UrlParams(params string[] rgs)
+ {
+ Debug.Assert(rgs.Length % 2 == 0);
+
+ string s = "token=" + HttpUtility.UrlEncode(_sToken);
+ for (int i = 0; i < rgs.Length; i += 2)
+ {
+ s += "&" + HttpUtility.UrlEncode(rgs[i]) + "=" + HttpUtility.UrlEncode(rgs[i + 1]);
+ }
+ return s;
+ }
+
+ private XmlReader GetXmlReader(string sxml)
+ {
+ XmlReaderSettings st = new XmlReaderSettings();
+ st.IgnoreComments = true;
+ st.IgnoreWhitespace = true;
+ st.ConformanceLevel = ConformanceLevel.Fragment;
+ st.ValidationType = ValidationType.None;
+
+ XmlReader xml = XmlReader.Create(new StringReader(sxml.Trim()), st); // Trim() removes BOM so <?xml is at the beginning
+
+ xml.MoveToContent();
+
+ return xml;
+ }
+
+ public bool OnRemoteCertificateValidation(Object sender,
+ X509Certificate certificate,
+ X509Chain chain,
+ SslPolicyErrors sslPolicyErrors)
+ {
+ switch (sslPolicyErrors)
+ {
+ case SslPolicyErrors.None:
+ return true;
+
+ case SslPolicyErrors.RemoteCertificateChainErrors:
+ _lastCertError = "SSL Error: " + chain.ChainElements[0].ChainElementStatus[0].StatusInformation.Trim();
+ break;
+
+ case SslPolicyErrors.RemoteCertificateNameMismatch:
+ _lastCertError = "SSL Error: The certificate name does not match the name in the URL.";
+ break;
+
+ case SslPolicyErrors.RemoteCertificateNotAvailable:
+ _lastCertError = "SSL Error: The remote certificate is not available.";
+ break;
+ }
+
+ return false;
+ }
+ }
+
+ class FogBugzNotLoggedOnException : Exception { }
+
+ class FogBugzErrorException : Exception
+ {
+ public int Code { get; private set; }
+
+ public FogBugzErrorException(string msg, int code) : base(msg)
+ {
+ Code = code;
+ }
+ }
+}
|
|
@@ -0,0 +1,36 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Interval
+ {
+ public int ixInterval { get; private set; }
+
+ public Person Person { get; private set; }
+
+ public int ixBug { get; private set; }
+ public string sTitle { get; private set; }
+
+ public DateTime? dtStart { get; private set; }
+ public DateTime? dtEnd { get; private set; }
+
+ public bool fDeleted { get; private set; }
+
+ public Interval(Dictionary<String, String> fields, FogBugzClient parentClient)
+ {
+ this.ixInterval = Convert.ToInt32(fields["ixInterval"]);
+
+ this.Person = parentClient.GetPerson(Convert.ToInt32(fields["ixPerson"]));
+
+ this.ixBug = Convert.ToInt32(fields["ixBug"]);
+ this.sTitle = fields["sTitle"];
+
+ this.dtStart = Util.ParseApiDate(fields["dtStart"]);
+ this.dtEnd = Util.ParseApiDate(fields["dtEnd"]);
+
+ this.fDeleted = Convert.ToBoolean(fields["fDeleted"]);
+ }
+ }
+}
|
|
@@ -0,0 +1,25 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Person
+ {
+ public readonly int ixPerson;
+ public readonly string sFullName;
+ public readonly string sEmail;
+
+ public Person(Dictionary<String, String> fields)
+ {
+ this.ixPerson = Convert.ToInt32(fields["ixPerson"]);
+ this.sFullName = fields["sFullName"];
+ this.sEmail = fields["sEmail"];
+ }
+
+ public override string ToString()
+ {
+ return sFullName;
+ }
+ }
+}
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Priority
+ {
+ public readonly int ixPriority;
+ public readonly string sPriority;
+
+ public Priority(int ixPriority, string sPriority)
+ {
+ this.ixPriority = ixPriority;
+ this.sPriority = sPriority;
+ }
+
+ public override string ToString()
+ {
+ return ixPriority.ToString() + " - " + sPriority;
+ }
+ }
+}
|
|
@@ -0,0 +1,23 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Project
+ {
+ public readonly int ixProject;
+ public readonly string sProject;
+
+ public Project(int ixProject, string sProject)
+ {
+ this.ixProject = ixProject;
+ this.sProject = sProject;
+ }
+
+ public override string ToString()
+ {
+ return sProject;
+ }
+ }
+}
|
|
@@ -0,0 +1,31 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FogBugzForVisualStudio.Api
+{
+ public class Status
+ {
+ public readonly int ixStatus;
+ public readonly string sStatus;
+ public readonly bool fDeleted;
+ public readonly int ixCategory;
+ public readonly bool fResolved;
+ public readonly int iOrder;
+
+ public Status(Dictionary<String, String> fields)
+ {
+ this.ixStatus = Convert.ToInt32(fields["ixStatus"]);
+ this.sStatus = fields["sStatus"];
+ this.fDeleted = Convert.ToBoolean(fields["fDeleted"]);
+ this.ixCategory = Convert.ToInt32(fields["ixCategory"]);
+ this.fResolved = Convert.ToBoolean(fields["fResolved"]);
+ this.iOrder = Convert.ToInt32(fields["iOrder"]);
+ }
+
+ public override string ToString()
+ {
+ return sStatus;
+ }
+ }
+}
|
|
@@ -0,0 +1,30 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Globalization;
+
+namespace FogBugzForVisualStudio.Api
+{
+ static class Util
+ {
+ public static Nullable<DateTime> ParseApiDate(string s)
+ {
+ // Shortcut for empty strings
+ if (String.IsNullOrEmpty(s)) return null;
+
+ try
+ {
+ return DateTime.ParseExact(s, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToLocalTime();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public static string FormatApiDate(DateTime dt)
+ {
+ return dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
+ }
+ }
+}
|
|
@@ -0,0 +1,51 @@ + using System.Reflection;
+using System.Runtime.CompilerServices;
+
+//
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+//
+[assembly: AssemblyTitle("Fog Creek Software, Inc.")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+//
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Revision
+// Build Number
+//
+// You can specify all the value or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("3.2.*")] // mhp - bump for working with vs2012
+
+//
+// In order to sign your assembly you must specify a key to use. Refer to the
+// Microsoft .NET Framework documentation for more information on assembly signing.
+//
+// Use the attributes below to control which key is used for signing.
+//
+// Notes:
+// (*) If no key is specified - the assembly cannot be signed.
+// (*) KeyName refers to a key that has been installed in the Crypto Service
+// Provider (CSP) on your machine.
+// (*) If the key file and a key name attributes are both specified, the
+// following processing occurs:
+// (1) If the KeyName can be found in the CSP - that key is used.
+// (2) If the KeyName does not exist and the KeyFile does exist, the key
+// in the file is installed into the CSP and used.
+// (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
+// documentation for more information on this.
+//
+[assembly: AssemblyDelaySign(false)]
+[assembly: AssemblyKeyFile("")]
+[assembly: AssemblyKeyName("")]
|
|
@@ -0,0 +1,101 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Net;
+using System.Web;
+
+namespace FogBugzForVisualStudio
+{
+ class UrlParams : System.Collections.IEnumerable {
+ private List<KeyValuePair<string, string>> parms = new List<KeyValuePair<string, string>>();
+
+ public void Add(string name, string value) {
+ parms.Add(new KeyValuePair<string, string>(name, value));
+ }
+
+ public override String ToString() {
+ return String.Join("&", System.Array.ConvertAll(parms.ToArray(), (pair) => {
+ return String.Format("{0}={1}", Encode(pair.Key), Encode(pair.Value));
+ }));
+ }
+
+ private String Encode(String s) {
+ return HttpUtility.UrlEncode(s);
+ }
+
+ public System.Collections.IEnumerator GetEnumerator()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ class BugScout
+ {
+ public static void ReportException(Exception ex)
+ {
+ ReportException(ex, "");
+ }
+
+ public static void ReportException(Exception ex, String extra)
+ {
+ ReportError(ex.Message, String.Format(
+ "[code]\n{0}\n[/code]\n\n{1}",
+ ex.ToString(), extra
+ ));
+ }
+
+ public static void ReportError(String title)
+ {
+ ReportError(title, "");
+ }
+
+ public static void ReportError(String title, String extra){
+ #if !DEBUG
+ if (!Connect.ReportErrors)
+ {
+ return;
+ }
+ // This is where errors can be reported to any system you prefer.
+ // We use BugzScout, and have included some sample code to get
+ // you started capturing error reports in FogBugz. You'll
+ // need to update the URL and parameter values below. For more
+ // information, please visit:
+ // http://help.fogcreek.com/7566/bugzscout-for-automatic-crash-reporting
+ //
+ //var url = "https://your-site.fogbugz.com/scoutSubmit.asp";
+ //var client = new WebClient();
+ //client.DownloadStringAsync(new Uri(url + "?" + new UrlParams{
+ // {"ScoutUserName", "Your BugzScout User},
+ // {"ScoutProject", "Your BugzScout Project"},
+ // {"ScoutArea", "Your BugzScout Area"},
+ // {"description", "VS Add-In: " title},
+ // {"extra", extra}
+ //}.ToString()));
+
+ #endif
+ }
+
+ public delegate void Action();
+
+ public static DownloadStringCompletedEventHandler CrashProof(DownloadStringCompletedEventHandler action)
+ {
+ return (a, b) =>
+ {
+ CatchAndReport(() => {
+ action(a, b);
+ });
+ };
+ }
+
+ public static void CatchAndReport(Action action){
+ try
+ {
+ action();
+ }
+ catch (Exception ex)
+ {
+ ReportException(ex);
+ }
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
@@ -0,0 +1,63 @@ + //------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.1
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace FogBugzForVisualStudio {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class CommandBar {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CommandBar() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FogBugzForVisualStudio.CommandBar", typeof(CommandBar).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
|
@@ -0,0 +1,1095 @@ + using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Data;
+using System.Net;
+using System.Text;
+using System.Windows.Forms;
+using System.Collections.Specialized;
+using System.Xml.Serialization;
+using System.IO;
+using System.Diagnostics;
+using FogBugzForVisualStudio.Api;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Resources;
+
+namespace FogBugzForVisualStudio
+{
+ public partial class FogBugzCtl : UserControl
+ {
+ /// <summary>
+ /// Event fires when the control wants its container to show a URL.
+ /// </summary>
+ /// <param name="sURL"></param>
+ public delegate void ShowUrlHandler(string sURL);
+ public event ShowUrlHandler OnShowUrl;
+
+ private FogBugzClient fb;
+ private string sURL;
+ private string sUser;
+ private FogBugzUIState state = FogBugzUIState.LoggedOff;
+ private string sFilterLableText = "";
+
+ private bool settingsLoaded;
+
+ private ToolStripMenuItem toolReply, toolEdit, toolAssign, toolResolve, toolClose, toolAddSubcase, toolReactivate, toolReopen, toolWorkOn;
+
+ public FogBugzCtl()
+ {
+ InitializeComponent();
+
+ sURL = RegistryHelper.FogBugzVSKey.GetValue("URL") as string;
+ sUser = RegistryHelper.FogBugzVSKey.GetValue("User") as string;
+
+ var sApiUrl = RegistryHelper.FogBugzVSKey.GetValue("ApiUrl") as string;
+ var sToken = RegistryHelper.FogBugzVSKey.GetValue("Token") as string;
+
+ if (!String.IsNullOrEmpty(sURL) && !String.IsNullOrEmpty(sToken))
+ {
+ fb = new FogBugzClient(sApiUrl, sToken);
+ // Although the token was saved, we are not logged on yet.
+ // People, statuses, categories need to be downloaded.
+ // We'll get a LoggedOn event when that is complete.
+ State(FogBugzUIState.LoggingOn);
+ }
+ else
+ {
+ fb = new FogBugzClient();
+ State(FogBugzUIState.LoggedOff);
+ }
+
+ fb.OnLogon += LoggedOn;
+ fb.OnListIntervals += ListIntervalsResult;
+ fb.OnListCases += ListCasesResult;
+ fb.OnUserLoggedOff += UserLoggedOff;
+ fb.OnListFilters += ListFiltersResult;
+ fb.OnStop += Stopped;
+
+ grid.AutoGenerateColumns = false;
+
+ // Restore columns
+ try
+ {
+ var str = new MemoryStream((byte[])RegistryHelper.FogBugzVSKey.GetValue("Columns"));
+ var visibleColumns = (new BinaryFormatter()).Deserialize(str) as List<KeyValuePair<string, int>>;
+ if (visibleColumns != null)
+ {
+ foreach (DataGridViewColumn col in grid.Columns)
+ {
+ col.Visible = false;
+ }
+
+ int i = 0;
+ foreach (var c in visibleColumns)
+ {
+ DataGridViewColumn col = grid.Columns[c.Key];
+ col.Visible = true;
+ col.DisplayIndex = i++;
+ col.Width = c.Value;
+ }
+ }
+ }
+ catch
+ {
+ // Ignore errors.
+ }
+
+ var gridMenu = new ContextMenuStrip();
+ gridMenu.Items.Add(showColumnsMenu());
+ gridMenu.Items.Add(new ToolStripSeparator());
+
+ // Case actions.
+ gridMenu.Items.Add(toolReply = new ToolStripMenuItem("Reply", null, toolReply_Click));
+ gridMenu.Items.Add(toolEdit = new ToolStripMenuItem("Edit", null, toolEdit_Click));
+ gridMenu.Items.Add(toolAssign = new ToolStripMenuItem("Assign", null, toolAssign_Click));
+ gridMenu.Items.Add(toolResolve = new ToolStripMenuItem("Resolve", null, toolResolve_Click));
+ gridMenu.Items.Add(toolClose = new ToolStripMenuItem("Close", null, toolClose_Click));
+ gridMenu.Items.Add(toolAddSubcase = new ToolStripMenuItem("Add Subcase", null, toolAddSubcase_Click));
+ gridMenu.Items.Add(toolReactivate = new ToolStripMenuItem("Reactivate", null, toolReactivate_Click));
+ gridMenu.Items.Add(toolReopen = new ToolStripMenuItem("Reopen", null, toolReopen_Click));
+ gridMenu.Items.Add(new ToolStripSeparator());
+ gridMenu.Items.Add(toolWorkOn = new ToolStripMenuItem("Work On", null, toolWorkOn_Click));
+
+ grid.ContextMenuStrip = gridMenu;
+
+ settingsLoaded = true;
+ }
+
+ private ToolStripMenuItem showColumnsMenu()
+ {
+ var showColumnsMenu = new ToolStripMenuItem("Show Columns");
+
+ var columnMenuGroups = new Dictionary<String, List<String>>();
+ columnMenuGroups["Date"] = new List<String> { "dtClosed", "dtOpened", "dtResolved", "dtDue", "dtLastUpdated" };
+ columnMenuGroups["Estimate"] = new List<String> { "hrsElapsed", "hrsCurrEst", "hrsOrigEst", "hrsRemaining" };
+ columnMenuGroups["Person"] = new List<String> { "ixPersonAssignedTo", "ixPersonClosedBy", "ixPersonLastEditedBy", "ixPersonOpenedBy", "ixPersonResolvedBy" };
+
+ // Construct a list of all the columns that are in groups
+ var allGroupedColumns = new List<string>();
+ foreach (var columnList in columnMenuGroups.Values)
+ {
+ allGroupedColumns.AddRange(columnList);
+ }
+
+ // Get a list of all columns that *aren't* in groups
+ var menuTopLevel = new List<ColumnOrGroupWrapper>();
+ foreach (DataGridViewColumn col in grid.Columns)
+ {
+ if (!allGroupedColumns.Contains(col.Name))
+ {
+ menuTopLevel.Add(new ColumnOrGroupWrapper(col));
+ }
+ }
+ foreach (var groupName in columnMenuGroups.Keys)
+ {
+ menuTopLevel.Add(new ColumnOrGroupWrapper(groupName));
+ }
+ menuTopLevel.Sort();
+
+ foreach (ColumnOrGroupWrapper col in menuTopLevel)
+ {
+ if (col.ColumnOrGroup is string)
+ {
+ // We've reached a group. Add an item for the header:
+ var m = new ToolStripMenuItem((string)col.ColumnOrGroup);
+ m.Font = new Font(m.Font, FontStyle.Bold);
+ m.Enabled = false;
+ showColumnsMenu.DropDownItems.Add(m);
+
+ foreach (string colName in columnMenuGroups[(string)col.ColumnOrGroup])
+ {
+ var item = menuItemForColumn(grid.Columns[colName]);
+ item.Text = " " + item.Text; // Ugly hack, but it works.
+ showColumnsMenu.DropDownItems.Add(item);
+ }
+ }
+ else
+ {
+ showColumnsMenu.DropDownItems.Add(menuItemForColumn((DataGridViewColumn)col.ColumnOrGroup));
+ }
+ }
+
+ return showColumnsMenu;
+ }
+
+ // We want to override certain menu items. Notably, the Category menu item doesn't have HeaderText since it's only 18px wide.
+ private static Dictionary<string, string> columnMenuItemOverride = new Dictionary<string, string> { { "ixCategory", "Category" } };
+
+ private ToolStripMenuItem backlogMenuItem;
+
+ private ToolStripMenuItem menuItemForColumn(DataGridViewColumn col)
+ {
+ var text = col.HeaderText;
+ if (columnMenuItemOverride.ContainsKey(col.Name)) text = columnMenuItemOverride[col.Name];
+
+ var m = new ToolStripMenuItem(text);
+ m.Checked = col.Visible;
+ m.Tag = col;
+ m.Click += showColumn_Click;
+ if (col.Name == "iBacklog") backlogMenuItem = m;
+ return m;
+ }
+
+ private class ColumnOrGroupWrapper : IComparable<ColumnOrGroupWrapper>
+ {
+ public object ColumnOrGroup { get; private set; }
+
+ public ColumnOrGroupWrapper(DataGridViewColumn col)
+ {
+ ColumnOrGroup = col;
+ }
+
+ public ColumnOrGroupWrapper(string groupName)
+ {
+ ColumnOrGroup = groupName;
+ }
+
+ public int CompareTo(ColumnOrGroupWrapper other)
+ {
+ return this.ToString().CompareTo(other.ToString());
+ }
+
+ public override string ToString()
+ {
+ if (ColumnOrGroup is string)
+ {
+ return (string)ColumnOrGroup;
+ }
+ else if (ColumnOrGroup is DataGridViewColumn)
+ {
+ var col = (DataGridViewColumn)ColumnOrGroup;
+ if (columnMenuItemOverride.ContainsKey(col.Name)) return columnMenuItemOverride[col.Name];
+ else return col.HeaderText;
+ }
+ throw new InvalidOperationException("ColumnOrGroupWrapper has an object that is neither a group nor a column.");
+ }
+ }
+
+ void showColumn_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ ToolStripMenuItem m = (ToolStripMenuItem)sender;
+ DataGridViewColumn col = (DataGridViewColumn)m.Tag;
+ m.Checked = !m.Checked;
+ col.Visible = m.Checked;
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private void toolRefresh_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ State(FogBugzUIState.Busy);
+ fb.ListCases();
+ UpdateIntervals();
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private void SaveSettings()
+ {
+ if (!settingsLoaded) return;
+
+ RegistryHelper.FogBugzVSKey.SetValue("URL", sURL ?? "");
+ RegistryHelper.FogBugzVSKey.SetValue("User", sUser ?? "");
+
+ RegistryHelper.FogBugzVSKey.SetValue("ApiUrl", fb.sApiUrl ?? "");
+ RegistryHelper.FogBugzVSKey.SetValue("Token", fb.sToken ?? "");
+
+ var formatter = new BinaryFormatter();
+ MemoryStream str;
+
+ if (Connector != null)
+ {
+ if (Connector.SortField != null)
+ {
+ // Convert from DataPropertyName to Name
+ string columnName = null;
+ for (int i = 0; i < grid.Columns.Count; i++)
+ {
+ if (grid.Columns[i].DataPropertyName == Connector.SortField)
+ {
+ columnName = grid.Columns[i].Name;
+ break;
+ }
+ }
+ if (!String.IsNullOrEmpty(columnName))
+ {
+ RegistryHelper.FogBugzVSKey.SetValue("SortColumn", columnName);
+ }
+ }
+ else
+ {
+ RegistryHelper.FogBugzVSKey.DeleteValue("SortColumn", false);
+ }
+ RegistryHelper.FogBugzVSKey.SetValue("SortOrder", Connector.SortOrder);
+
+ str = new MemoryStream();
+ formatter.Serialize(str, Connector.GetCasesExpanded());
+ RegistryHelper.FogBugzVSKey.SetValue("CasesExpanded", str.ToArray(), Microsoft.Win32.RegistryValueKind.Binary);
+
+ str = new MemoryStream();
+ formatter.Serialize(str, Connector.GetCasesShowingSubcases());
+ RegistryHelper.FogBugzVSKey.SetValue("CasesShowingSubcases", str.ToArray(), Microsoft.Win32.RegistryValueKind.Binary);
+ }
+
+ // Persist column state
+ DataGridViewColumn col = grid.Columns.GetFirstColumn(DataGridViewElementStates.Visible);
+ var visibleColumns = new List<KeyValuePair<string, int>>();
+ while (col != null)
+ {
+ visibleColumns.Add(new KeyValuePair<string, int>(col.Name, col.Width));
+ col = grid.Columns.GetNextColumn(col, DataGridViewElementStates.Visible, DataGridViewElementStates.None);
+ }
+
+ str = new MemoryStream();
+ formatter.Serialize(str, visibleColumns);
+ RegistryHelper.FogBugzVSKey.SetValue("Columns", str.ToArray(), Microsoft.Win32.RegistryValueKind.Binary);
+ }
+
+ private void Stopped()
+ {
+ State(FogBugzUIState.Normal);
+ }
+
+ private void LoggedOn(bool fSuccess, string message, StringCollection rgsNames)
+ {
+ if (fSuccess)
+ {
+ State(FogBugzUIState.Busy);
+ fb.ListCases();
+ UpdateIntervals();
+ timUpdateWorkingOn.Enabled = true;
+ SaveSettings(); // Save token and user
+ }
+ else
+ {
+ State(FogBugzUIState.LoggedOff);
+ ShowLogOnWindow(message, rgsNames);
+ }
+ }
+
+ private void UserLoggedOff()
+ {
+ timUpdateWorkingOn.Enabled = false;
+ State(FogBugzUIState.LoggedOff);
+ SaveSettings(); // In case this was an unprompted log off
+ }
+
+ private List<ToolStripMenuItem> recentCasesMenuItems;
+
+ private void ListIntervalsResult(bool bSuccess, string sError, List<Interval> rgIntervals)
+ {
+ if (!bSuccess) return; // Fail silently
+
+ var rgRecent = new List<Interval>();
+
+ var foundCurrent = false;
+ foreach (var interval in rgIntervals)
+ {
+ if (!interval.fDeleted && interval.dtStart.HasValue && !interval.dtEnd.HasValue)
+ {
+ // Found current case
+ UpdateWorkingOn(interval.ixBug, interval.sTitle);
+ foundCurrent = true;
+ }
+ else if (!interval.fDeleted && interval.dtStart.HasValue && interval.dtEnd.HasValue)
+ {
+ rgRecent.Add(interval);
+ }
+ }
+ if (!foundCurrent)
+ {
+ UpdateWorkingOn(null, null);
+ }
+
+ rgRecent.Sort(delegate(Interval i1, Interval i2) { return i1.dtEnd.Value.CompareTo(i2.dtEnd.Value); });
+ rgRecent.Reverse();
+
+ if (recentCasesMenuItems != null)
+ {
+ foreach (ToolStripMenuItem item in recentCasesMenuItems)
+ {
+ recentlyWorkedOnToolStripMenuItem.DropDownItems.Remove(item);
+ item.Dispose();
+ }
+ }
+ recentCasesMenuItems = new List<ToolStripMenuItem>();
+
+ int cRecentCases = 0;
+ var dictRecent = new Dictionary<int, Interval>(); // Uniquify multiple intervals for same case
+ foreach (var interval in rgRecent)
+ {
+ if (dictRecent.ContainsKey(interval.ixBug)) continue;
+
+ var item = new ToolStripMenuItem("Case " + interval.ixBug + ": " + interval.sTitle, null, recentMenuItem_Click);
+ item.Tag = interval;
+ recentCasesMenuItems.Add(item);
+ recentlyWorkedOnToolStripMenuItem.DropDownItems.Add(item);
+
+ dictRecent[interval.ixBug] = interval;
+ cRecentCases++;
+ if (cRecentCases == 5) break;
+ }
+
+ menuNoRecentWorkedOn.Visible = (cRecentCases == 0);
+ }
+
+ private void recentMenuItem_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ var interval = ((Interval)((ToolStripMenuItem)sender).Tag);
+
+ fb.StartWork(interval.ixBug);
+ UpdateWorkingOn(interval.ixBug, interval.sTitle);
+ }
+ catch (FogBugzErrorException exp)
+ {
+ BugScout.ReportException(exp);
+ MessageBox.Show(String.IsNullOrEmpty(exp.Message) ? ("FogBugz returned an unknown error (" + exp.Code + ")") : exp.Message);
+ }
+ }
+
+ private CaseListConnector _connector;
+ private CaseListConnector Connector
+ {
+ get
+ {
+ return _connector;
+ }
+
+ set
+ {
+ if (_connector != null)
+ {
+ _connector.OnShowUrl -= OnShowUrl;
+ _connector.OnRowCountChanged -= Connector_OnRowCountChanged;
+ _connector.OnViewStateChanged -= Connector_OnViewStateChanged;
+ }
+
+ _connector = value;
+ if (_connector != null)
+ {
+ _connector.OnShowUrl += OnShowUrl;
+ _connector.OnRowCountChanged += Connector_OnRowCountChanged;
+ _connector.OnViewStateChanged += Connector_OnViewStateChanged;
+
+ grid.RowCount = _connector.RowCount;
+ }
+ else
+ {
+ grid.RowCount = 0;
+ }
+ grid.Invalidate();
+ }
+ }
+
+ private void ListCasesResult(bool bSuccess, string sError, string sListDescription, List<Case> rgCasesInCurrentFilter, List<Case> rgParentAndSubcases)
+ {
+ State(FogBugzUIState.Normal);
+
+ if (bSuccess)
+ {
+ sFilterLableText = lblFilter.Text = sListDescription;
+ lblFilter.ForeColor = SystemColors.ControlText;
+
+ var formatter = new BinaryFormatter();
+ Dictionary<int, bool> casesExpanded = null;
+ Dictionary<int, bool> casesShowingSubcases = null;
+
+ try
+ {
+ var serCasesExpanded = (byte[])RegistryHelper.FogBugzVSKey.GetValue("CasesExpanded");
+ if (serCasesExpanded != null)
+ {
+ casesExpanded = formatter.Deserialize(new MemoryStream(serCasesExpanded)) as Dictionary<int, bool>;
+ }
+
+ var serCasesShowingSubcases = (byte[])RegistryHelper.FogBugzVSKey.GetValue("CasesShowingSubcases");
+ if (serCasesShowingSubcases != null)
+ {
+ casesShowingSubcases = formatter.Deserialize(new MemoryStream(serCasesShowingSubcases)) as Dictionary<int, bool>;
+ }
+ }
+ catch { }
+
+ var sortColumn = RegistryHelper.FogBugzVSKey.GetValue("SortColumn") as string;
+ var savedSortOrder = RegistryHelper.FogBugzVSKey.GetValue("SortOrder") as string;
+ var sortOrder = (SortOrder)Enum.Parse(typeof(SortOrder), savedSortOrder ?? "Ascending");
+
+ string colName = null;
+ if (sortColumn != null && grid.Columns.Contains(sortColumn))
+ {
+ colName = grid.Columns[sortColumn].DataPropertyName;
+ }
+
+ setSortGlyphColumn(sortColumn, sortOrder);
+ Connector = new CaseListConnector(fb, rgCasesInCurrentFilter, rgParentAndSubcases, colName, sortOrder, casesExpanded, casesShowingSubcases);
+
+ // fHasBacklogPlugin should now be set
+ if (fb.fHasBacklogPlugin.HasValue)
+ {
+ if (!fb.fHasBacklogPlugin.Value)
+ {
+ grid.Columns["iBacklog"].Visible = false;
+ backlogMenuItem.Visible = false;
+ }
+
+ backlogMenuItem.Visible = fb.fHasBacklogPlugin.Value;
+ }
+ }
+ else
+ {
+ BugScout.ReportError(sError);
+ lblFilter.Text = sError;
+ lblFilter.ForeColor = Color.Red;
+ }
+ }
+
+ void Connector_OnViewStateChanged(object sender, EventArgs e)
+ {
+ SaveSettings();
+ }
+
+ void Connector_OnRowCountChanged(object sender, EventArgs e)
+ {
+ grid.RowCount = Connector.RowCount;
+ grid.Invalidate();
+ }
+
+ private enum FogBugzUIState
+ {
+ LoggedOff,
+ LoggingOn,
+ Busy,
+ Normal
+ }
+
+ /// <summary>
+ /// Disables UI and displays wait cursor while busy
+ /// </summary>
+ /// <param name="s">The state UI is transitioning to.</param>
+ private void State(FogBugzUIState s)
+ {
+ bool loggedOn = !(s == FogBugzUIState.LoggedOff || s == FogBugzUIState.LoggingOn);
+
+ lblFilter.Visible = grid.Visible = loggedOn;
+ lblNotLoggedOn.Visible = !loggedOn;
+ lblNotLoggedOn.Text = (s == FogBugzUIState.LoggedOff ? "Log on to start using FogBugz for Visual Studio." : "Logging on...");
+
+ toolStripButtonStop.Visible = (s == FogBugzUIState.Busy);
+ toolRefresh.Visible = (s == FogBugzUIState.Normal);
+
+ lblFilter.Text = sFilterLableText;
+ if (s == FogBugzUIState.Busy)
+ {
+ grid.ClearSelection();
+ lblFilter.Text = "Loading...";
+ }
+ else if (s == FogBugzUIState.LoggedOff)
+ {
+ grid.DataSource = null;
+ }
+
+ ddWorkingOn.Enabled = ddFilters.Enabled = grid.Enabled = (s == FogBugzUIState.Normal);
+
+ btnLogOff.Text = loggedOn ? "Log Off" : "Log On";
+ btnLogOff.Enabled = (s == FogBugzUIState.Normal || s == FogBugzUIState.LoggedOff);
+ toolSendEmail.Enabled = toolNewCase.Enabled = toolRefresh.Enabled = loggedOn;
+
+ Cursor.Current = (s == FogBugzUIState.Busy || s == FogBugzUIState.LoggingOn) ? Cursors.WaitCursor : Cursors.Default;
+ reportErrorsAutomaticallyToolStripMenuItem.Checked = Connect.ReportErrors;
+
+ state = s;
+ }
+
+ public void reportErrorsAutomaticallyToolStripMenuItem_CheckedChanged(object sender, object args)
+ {
+ Connect.ReportErrors = reportErrorsAutomaticallyToolStripMenuItem.Checked;
+ }
+
+ private void ShowLogOnWindow(String sError = null, StringCollection rgsNames = null)
+ {
+ var f = new frmLogOn();
+
+ if (!String.IsNullOrEmpty(sError))
+ {
+ f.ShowError(sError);
+ }
+
+ f.fldURL.Text = sURL;
+ if (rgsNames == null)
+ {
+ f.fldUser.Text = sUser;
+ }
+ else
+ {
+ f.fldUser.Visible = false;
+ f.cmbUser.Visible = true;
+ foreach (String s in rgsNames)
+ {
+ f.cmbUser.Items.Add(s);
+ }
+ }
+
+ if (f.ShowDialog() == DialogResult.OK)
+ {
+ sURL = f.fldURL.Text;
+ sUser = (rgsNames == null) ? f.fldUser.Text : f.cmbUser.Text;
+
+ State(FogBugzUIState.LoggingOn);
+ fb.Logon(sURL, sUser, f.fldPassword.Text);
+ }
+ f.Dispose();
+ }
+
+ private void btnLogOff_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ if (fb.LoggedOn())
+ {
+ State(FogBugzUIState.LoggedOff);
+ fb.LogOff();
+ Connector = null;
+ grid.Invalidate();
+ sUser = "";
+ SaveSettings();
+ }
+ else
+ {
+ ShowLogOnWindow();
+ }
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private void grid_SelectionChanged(object sender, EventArgs e)
+ {
+ // determine what operations are possible
+ Case.Op ops = (Case.Op)0xFFFF;
+ var c = 0;
+ if (Connector != null)
+ {
+ foreach (DataGridViewRow rw in grid.SelectedRows)
+ {
+ var row = Connector.Row(rw.Index);
+ if (row.Bug != null) // Skip "Click to Show Hidden Subcases" rows
+ {
+ ops &= row.Bug.ops;
+ c++;
+ }
+ }
+ }
+
+ if (c == 0) ops = (Case.Op)0;
+
+ toolReply.Enabled = (ops & Case.Op.reply) != 0;
+ toolEdit.Enabled = (ops & Case.Op.edit) != 0;
+ toolAssign.Enabled = (ops & Case.Op.assign) != 0;
+ toolClose.Enabled = (ops & Case.Op.close) != 0;
+ toolReactivate.Enabled = (ops & Case.Op.reactivate) != 0;
+ toolReopen.Enabled = (ops & Case.Op.reopen) != 0;
+ toolResolve.Enabled = (ops & Case.Op.resolve) != 0;
+
+ toolWorkOn.Enabled = toolAddSubcase.Enabled = (grid.SelectedRows.Count == 1);
+ }
+
+ private void ddFilters_DropDownOpening(object sender, EventArgs e)
+ {
+ fb.ListFilters();
+ }
+
+ private void ListFiltersResult(bool bSuccess, string sError, List<Filter> rgFilters)
+ {
+ if (bSuccess)
+ {
+ /*
+ * Optimization - check list of filters to see if
+ * it's the same as the currently showing list.
+ * If so, don't refresh, to avoid flashing.
+ */
+ if (!NeedToRefreshList(rgFilters))
+ {
+ return;
+ }
+
+ var ft = Filter.FilterType.builtin;
+ ddFilters.DropDownItems.Clear();
+ foreach (Filter f in rgFilters)
+ {
+ if (ft != f.type)
+ {
+ ddFilters.DropDownItems.Add(new ToolStripSeparator());
+ }
+ ft = f.type;
+ ToolStripItem tsi = ddFilters.DropDownItems.Add(f.s);
+ tsi.Tag = f.codeFilter;
+ tsi.Font = new Font(tsi.Font, f.fIsCurrent ? FontStyle.Bold : FontStyle.Regular);
+ }
+ }
+ }
+
+ private bool NeedToRefreshList(List<Filter> rgFilters) {
+ int ix = 0;
+ var ft = Filter.FilterType.builtin;
+
+ foreach (Filter f in rgFilters)
+ {
+ if (ft != f.type)
+ {
+ ix++;
+ }
+ ft = f.type;
+
+ var items = ddFilters.DropDownItems;
+ if (items.Count - 1 < ix || items[ix].Text != f.s || items[ix].Tag.ToString() != f.codeFilter)
+ {
+ return true;
+ }
+
+ ix++;
+ }
+
+ return (ddFilters.DropDownItems.Count - 1 >= ix);
+ }
+
+ private void ddFilters_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e)
+ {
+ try
+ {
+ if (e.ClickedItem.Tag.ToString() != "LoadingDoNotList")
+ {
+ State(FogBugzUIState.Busy);
+
+ // Reset view state
+ Connector = null;
+ sFilterLableText = "";
+
+ RegistryHelper.FogBugzVSKey.DeleteValue("CasesExpanded", false);
+ RegistryHelper.FogBugzVSKey.DeleteValue("CasesShowingSubcases", false);
+
+ fb.SaveFilterAndList(e.ClickedItem.Tag.ToString());
+ //
+ // make current item bold
+ // UNDONE bug: if list is cancelled because you press
+ // stop fast enough, the wrong item is boldened.
+ //
+ foreach (ToolStripItem tsi in ddFilters.DropDownItems)
+ {
+ tsi.Font = new Font(tsi.Font, (tsi == e.ClickedItem) ? FontStyle.Bold : FontStyle.Regular);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private void toolNewCase_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "command=new&pg=pgEditBug");
+ }
+
+ private void toolSendEmail_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "command=newemail&pg=pgEditBug");
+ }
+
+ private void grid_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
+ {
+ try
+ {
+ if (e.RowIndex < 0) return; // Header click
+
+ var sBugs = ListSelectedRows();
+ if (!String.IsNullOrEmpty(sBugs))
+ {
+ OnShowUrl(fb.GetDefaultUrl() + ListSelectedRows());
+ }
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private void grid_CellContentClick(object sender, DataGridViewCellEventArgs e)
+ {
+ try
+ {
+ if (Connector == null) return;
+
+ if (e.RowIndex >= 0 && grid.Columns[e.ColumnIndex].Name != "hrsCurrEst") // Not headers; and not the estimate column (see CellMouseClick)
+ {
+ Connector.Row(e.RowIndex).OnClicked(grid, e);
+ }
+
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private string ListSelectedRows()
+ {
+ if (Connector == null) return "";
+
+ Debug.Assert(grid.SelectedRows.Count > 0);
+ var rgsIxBug = new List<string>();
+ foreach (DataGridViewRow rw in grid.SelectedRows)
+ {
+ var row = Connector.Row(rw.Index);
+ if (row.Bug != null) rgsIxBug.Add(row.Bug.ixBug.ToString());
+ }
+ return String.Join(",", rgsIxBug.ToArray());
+ }
+
+ private void toolReply_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=reply&ixBug=" + ListSelectedRows());
+ }
+
+ private void toolEdit_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=edit&ixBug=" + ListSelectedRows());
+ }
+
+ private void toolAssign_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=assign&ixBug=" + ListSelectedRows());
+ }
+
+ private void toolResolve_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=resolve&ixBug=" + ListSelectedRows());
+ }
+
+ private void toolAddSubcase_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=new&ixBugParent=" + Connector.Row(grid.SelectedRows[0].Index).Bug.ixBug);
+ }
+
+ private void toolReactivate_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=reactivate&ixBug=" + ListSelectedRows());
+ }
+
+ private void toolClose_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=close&ixBug=" + ListSelectedRows());
+ }
+
+ private void toolReopen_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgEditBug&command=reopen&ixBug=" + ListSelectedRows());
+ }
+
+ private void toolWorkOn_Click(object sender, EventArgs e)
+ {
+ if (Connector == null) return;
+
+ try
+ {
+ var bug = Connector.Row(grid.SelectedRows[0].Index).Bug;
+
+ if (bug.hrsCurrEst.hrs == 0.0m)
+ {
+ // Set estimate first
+ var estimateForm = new frmSetEstimate();
+ estimateForm.lblEstimate.Text = "Enter an estimate to start working on Case " + bug.ixBug + ", " + bug.sTitle + ".";
+ if (estimateForm.ShowDialog() == DialogResult.OK)
+ {
+ Connector.FogBugzClient.SetCaseEstimate(bug, estimateForm.fldEstimate.Text);
+ grid.InvalidateRow(grid.SelectedRows[0].Index); // invalidate all cells as remaining time might change
+ }
+ else return;
+ estimateForm.Dispose();
+ }
+
+ fb.StartWork(bug);
+ grid.InvalidateRow(grid.SelectedRows[0].Index);
+ UpdateWorkingOn(bug.ixBug, bug.sTitle);
+ }
+ catch (FogBugzErrorException exp)
+ {
+ BugScout.ReportException(exp);
+ MessageBox.Show(String.IsNullOrEmpty(exp.Message) ? ("FogBugz returned an unknown error (" + exp.Code + ")") : exp.Message);
+ }
+ }
+
+ private void toolStripButtonStop_Click(object sender, EventArgs e)
+ {
+ fb.Stop();
+ }
+
+ private void btnCancelLogon_Click(object sender, EventArgs e)
+ {
+ fb.Stop();
+ }
+
+ private bool IsValidRow(int index)
+ {
+ return Connector != null && 0 <= index && index < Connector.RowCount;
+ }
+
+ private void grid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
+ {
+ if (IsValidRow(e.RowIndex))
+ {
+ Connector.Row(e.RowIndex).CellFormatting(e, grid);
+ }
+ }
+
+ private void grid_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
+ {
+ SaveSettings();
+ }
+
+ private void grid_ColumnStateChanged(object sender, DataGridViewColumnStateChangedEventArgs e)
+ {
+ SaveSettings();
+ }
+
+ private void grid_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
+ {
+ if (IsValidRow(e.RowIndex))
+ {
+ e.Value = Connector.Row(e.RowIndex).GetCellValue(grid.Columns[e.ColumnIndex].DataPropertyName);
+ }
+ }
+
+ private void grid_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
+ {
+ try
+ {
+ if (Connector == null) return;
+
+ string col = grid.Columns[e.ColumnIndex].DataPropertyName;
+ SortOrder order = SortOrder.Ascending;
+ if (Connector.SortField == col)
+ {
+ order = (Connector.SortOrder == SortOrder.Ascending) ? SortOrder.Descending : SortOrder.Ascending;
+ }
+
+ Connector.Sort(col, order);
+ setSortGlyphColumn(grid.Columns[e.ColumnIndex].Name, order);
+ grid.Invalidate();
+
+ SaveSettings();
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private void setSortGlyphColumn(string colName, SortOrder order)
+ {
+ for (int i = 0; i < grid.Columns.Count; i++)
+ {
+ grid.Columns[i].HeaderCell.SortGlyphDirection = SortOrder.None;
+ }
+ if (colName != null && grid.Columns.Contains(colName))
+ {
+ grid.Columns[colName].HeaderCell.SortGlyphDirection = order;
+ }
+ }
+
+ private Bitmap t_d, t_r;
+ private void grid_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
+ {
+ if (Connector != null && grid.Columns[e.ColumnIndex].Name == "sTitle" && e.RowIndex >= 0)
+ {
+ e.Paint(e.ClipBounds, e.PaintParts);
+
+ var row = Connector.Row(e.RowIndex);
+ if (row.Expandable)
+ {
+ if (t_d == null || t_r == null)
+ {
+ var manager = new ResourceManager("FogBugzForVisualStudio.Resource1", GetType().Assembly);
+ t_d = (Bitmap)manager.GetObject("t_d");
+ t_r = (Bitmap)manager.GetObject("t_r");
+ }
+
+ e.Graphics.DrawImage(row.Expanded ? t_d : t_r, e.CellBounds.Location.X + 32 * row.Indent + 5, e.CellBounds.Location.Y + 6);
+ }
+
+ e.Handled = true;
+ }
+ }
+
+ private void grid_CellMouseClick(object sender, DataGridViewCellMouseEventArgs e)
+ {
+ try
+ {
+ if (Connector != null && e.RowIndex >= 0 && grid.Columns[e.ColumnIndex].Name == "sTitle")
+ {
+ var row = Connector.Row(e.RowIndex);
+ if (row.Expandable && e.X >= row.Indent * 32 + 3 && e.X <= row.Indent * 32 + 4 + 9 && e.Y >= 6 && e.Y <= 6 + 9)
+ {
+ row.Expanded = !row.Expanded;
+ }
+ }
+ else if (e.RowIndex >= 0 && grid.Columns[e.ColumnIndex].Name == "hrsCurrEst")
+ {
+ Connector.Row(e.RowIndex).OnClicked(grid, new DataGridViewCellEventArgs(e.ColumnIndex, e.RowIndex));
+ }
+ }
+ catch (Exception ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+ }
+
+ private void grid_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
+ {
+ if (e.RowIndex >= 0 && e.Button == MouseButtons.Right)
+ {
+ if (!grid.Rows[e.RowIndex].Selected)
+ {
+ grid.ClearSelection();
+ grid.Rows[e.RowIndex].Selected = true;
+ }
+ }
+ }
+
+ private void menuEditTimesheet_Click(object sender, EventArgs e)
+ {
+ OnShowUrl(fb.GetDefaultUrl() + "pg=pgTimesheet");
+ }
+
+ private void menuWorkOnNothing_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ fb.StopWork();
+ UpdateWorkingOn(null, null);
+ }
+ catch (FogBugzErrorException ex)
+ {
+ BugScout.ReportException(ex);
+ MessageBox.Show(ex.Message);
+ }
+ }
+
+ private void UpdateWorkingOn(int? ixBug, string sTitle)
+ {
+ ddWorkingOn.Text = (ixBug.HasValue ? "Working On " + ixBug : "Working On");
+ menuViewCurrentCase.Text = (ixBug.HasValue ? "View Current Case: " + ixBug + ": " + sTitle : "View Current Case");
+ menuViewCurrentCase.Enabled = ixBug.HasValue;
+ menuViewCurrentCase.Tag = ixBug;
+ }
+
+ private void menuViewCurrentCase_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ Nullable<int> ixBug = menuViewCurrentCase.Tag as Nullable<int>;
+ if (!ixBug.HasValue) return;
+
+ OnShowUrl(fb.GetDefaultUrl() + ixBug.Value);
+ }
+ catch (FogBugzErrorException ex)
+ {
+ BugScout.ReportException(ex);
+ throw;
+ }
+
+ }
+
+ private void timUpdateWorkingOn_Tick(object sender, EventArgs e)
+ {
+ UpdateIntervals();
+ }
+
+ private void UpdateIntervals()
+ {
+ fb.ListIntervals(DateTime.Now - new TimeSpan(7, 0, 0, 0), null);
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
|
|
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
@@ -0,0 +1,36 @@ + using System;
+using System.Collections.Generic;
+using System.Text;
+
+using Microsoft.Win32;
+
+namespace FogBugzForVisualStudio
+{
+ static class RegistryHelper
+ {
+ public static RegistryKey FogBugzVSKey
+ {
+ get
+ {
+ return Registry.CurrentUser.CreateSubKey(@"Software\Fog Creek Software\FogBugz For Visual Studio");
+ }
+ }
+
+ public static RegistryKey FogBugzVSMachineKey
+ {
+ get
+ {
+ return Registry.LocalMachine.CreateSubKey(@"Software\Fog Creek Software\FogBugz For Visual Studio");
+ }
+ }
+
+ public static bool? GetBool(RegistryKey obj, String key) {
+ var result = obj.GetValue(key, null);
+ if (result == null) {
+ return null;
+ }
+
+ return Convert.ToBoolean(result);
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
|
@@ -0,0 +1,18 @@ + using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Text;
+using System.Windows.Forms;
+
+namespace FogBugzForVisualStudio
+{
+ public partial class frmSetEstimate : Form
+ {
+ public frmSetEstimate()
+ {
+ InitializeComponent();
+ }
+ }
+}
|
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
This file's diff was not loaded because this changeset is very large. Load changes Loading... |
Loading...