diff --git a/README.md b/README.md
index ba4299c..267aae1 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,47 @@
-# Jira Worklogger
-
-
Licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
-
-## The problem
-
-Do you have to track your work by means of Jira issue worklogs? Is the Jira GUI _clickfest_ approach making your neurotic inner beast emerge on the surface?
-
-## The solution
-
-Meet the scripted Jira worklogging! Give it your worklogs in a CSV file (and your server URI and your user credentials in a config file) and let the automaton do the rest!
-
-## Prerequisites
-
-- .NET 6 run-time installed (for simple, cross-platform build) or no .NET runtime necessary (for self-contained, single-exe, Windows-only build); You choose!
-- Jira server (with version 2 REST API)
- - "vanilla" Jira server support: ✔️
- - "Tempo Timesheets" plugin support: ✔️
- - "ICTime" plugin support: ❎ (planned)
-
-## Configuration
-
-The jwl.config file is a simple JSON structure. It can be placed in (and will be read by jwl in the priority order of)
-- "current folder" (as in "where your shell's %CD%
or ${PWD}
is at the moment")
-- local application data (%USERPROFILE%\AppData\Local
)
-- roaming application data (%USERPROFILE%\AppData\Roaming
)
-- jwl's "installation" folder
-
-As for the CLI worklogger binary, there are command-line options available as well. Any partial options supplied via CLI will override their respective jwl.config counterparts with the highest priority.
-
-### "ServerClass" setting
-
-Available values are:
-- Vanilla
-- TempoTimeSheets
-- ICTime (not implemented yet)
-
-## The input CSV structure
-
-Five columns, data delimited (by default) by a colon:
- - Date
(string) - worklog day date (valid formats: YYYY-MM-DD
, YYYY/MM/DD
, DD.MM.YYYY
, all with optional HH:MI:SS
part)
- - IssueKey
(string) - Jira issue key (SOMEPROJECT-1234
and the likes)
- - Activity
(string) - Tempo Timesheets worklog type or ICTime activity; values are remapped
- - TimeSpent
(string) - time to be logged for the Jira issue and the date (valid formats: HH:MI
, MI
, HH h MI
, HH h MI m
)
- - Comment
(string) - optional worklog comment
-
-The Activity
values are remapped via config $.JiraServer.ActivityMap
. The mapping configuration depends on your Jira server+plugins configuration and is a subject of manual setup by yourself.
+# Jira Worklogger
+
+
Licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
+
+## The problem
+
+Do you have to track your work by means of Jira issue worklogs? Is the Jira GUI _clickfest_ approach making your neurotic inner beast emerge on the surface?
+
+## The solution
+
+Meet the scripted Jira worklogging! Give it your worklogs in a CSV file (and your server URI and your user credentials in a config file) and let the automaton do the rest!
+
+## Prerequisites
+
+- .NET 6 run-time installed (for simple, cross-platform build) or no .NET runtime necessary (for self-contained, single-exe, Windows-only build); You choose!
+- Jira server (with version 2 REST API)
+ - "vanilla" Jira server support: ✔️
+ - "Tempo Timesheets" plugin support: ✔️
+ - "ICTime" plugin support: ❎ (planned)
+
+## Configuration
+
+The jwl.config file is a simple JSON structure. It can be placed in (and will be read by jwl in the priority order of)
+- "current folder" (as in "where your shell's %CD%
or ${PWD}
is at the moment")
+- local application data (%USERPROFILE%\AppData\Local
)
+- roaming application data (%USERPROFILE%\AppData\Roaming
)
+- jwl's "installation" folder
+
+As for the CLI worklogger binary, there are command-line options available as well. Any partial options supplied via CLI will override their respective jwl.config counterparts with the highest priority.
+
+### "ServerClass" setting
+
+Available values are:
+- Vanilla
+- TempoTimeSheets
+- ICTime (not implemented yet)
+
+## The input CSV structure
+
+Five columns, data delimited (by default) by a colon:
+ - Date
(string) - worklog day date (valid formats: YYYY-MM-DD
, YYYY/MM/DD
, DD.MM.YYYY
, all with optional HH:MI:SS
part)
+ - IssueKey
(string) - Jira issue key (SOMEPROJECT-1234
and the likes)
+ - Activity
(string) - Tempo Timesheets worklog type or ICTime activity; values are remapped
+ - TimeSpent
(string) - time to be logged for the Jira issue and the date (valid formats: HH:MI
, MI
, HH h MI
, HH h MI m
)
+ - Comment
(string) - optional worklog comment
+
+The Activity
values are remapped via config $.JiraServer.ActivityMap
. The mapping configuration depends on your Jira server+plugins configuration and is a subject of manual setup by yourself.
diff --git a/_local_build.cmd b/_local_build.cmd
index cad8fb4..61b3ce6 100644
--- a/_local_build.cmd
+++ b/_local_build.cmd
@@ -1,3 +1,3 @@
-pushd jwl.console
-call global_build.cmd jira-worklogger
-@exit /b %ERRORLEVEL%
+pushd jwl.console
+call global_build.cmd jira-worklogger
+@exit /b %ERRORLEVEL%
diff --git a/jwl.console/CLI.cs b/jwl.console/CLI.cs
index 0bd11a2..6ab5a59 100644
--- a/jwl.console/CLI.cs
+++ b/jwl.console/CLI.cs
@@ -1,76 +1,76 @@
-namespace jwl.console;
-using CommandLine;
-
-[Verb("fill", isDefault: true, HelpText = "Fill Jira with worklogs")]
-public class FillCLI
-{
- [Option('v', "verbose", HelpText = "\nGive more verbose feedback\nNote: Not implemented yet! 2do! :-)", Default = false, Hidden = true)]
- public bool UseVerboseFeedback { get; set; }
-
- [Option('i', "input", HelpText = "\nInput CSVs with the worklogs", Separator = ',', Required = true)]
- public IEnumerable InputFiles { get; set; } = new string[0];
-
- [Option("ifs", HelpText = "Input CSV fields delimiter"
- + $"\nJSON config: $.{nameof(core.AppConfig.CsvOptions)}.{nameof(inputs.CsvFormatConfig.FieldDelimiter)}")]
- public string? FieldDelimiter { get; set; }
-
- [Option('t', "target", HelpText = "Connection string to Jira server in the form of @[:]"
- + "\nNote: The \"https://\" scheme is automatically asserted with this option!"
- + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.BaseUrl)} for [:]"
- + $"\nJSON config: $.{nameof(core.AppConfig.User)}.{nameof(core.UserConfig.Name)} for ")]
- public string? UserCredentials { get; set; }
-
- [Option("server-flavour", HelpText = "Jira server flavour (whether vanilla or with some timesheet plugins)"
- + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.Flavour)}"
- + $"\nAvailable values: {nameof(jira.JiraServerFlavour.Vanilla)}, {nameof(jira.JiraServerFlavour.TempoTimeSheets)}, {nameof(jira.JiraServerFlavour.ICTime)}")]
- public string? ServerFlavour { get; set; }
-
- [Option("no-proxy", HelpText = "Turn off proxying the HTTP(S) connections to Jira server"
- + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.UseProxy)} (negated!)")]
- public bool? NoProxy { get; set; }
-
- [Option("max-connections-per-server", HelpText = "Limit the number of concurrent connections to Jira server"
- + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.MaxConnectionsPerServer)}")]
- public int? MaxConnectionsPerServer { get; set; }
-
- [Option("no-ssl-cert-check", HelpText = "Skip SSL/TLS certificate checks (for self-signed certificates)"
- + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.SkipSslCertificateCheck)}")]
- public bool? SkipSslCertificateCheck { get; set; }
-
- public core.AppConfig ToAppConfig()
- {
- string[] connectionSpecifierSplit = UserCredentials?.Split('@', 2, StringSplitOptions.TrimEntries) ?? Array.Empty();
-
- string? jiraServerSpecification = connectionSpecifierSplit.Length > 1 ? connectionSpecifierSplit[1] : null;
- if (!jiraServerSpecification?.Contains(@"://") ?? false)
- jiraServerSpecification = @"https://" + jiraServerSpecification;
-
- string? jiraUserCredentials = connectionSpecifierSplit.Any() ? connectionSpecifierSplit[0] : null;
- string[] jiraUserCredentialsSplit = jiraUserCredentials?.Split('/', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty();
- string? jiraUserName = jiraUserCredentialsSplit.Any() ? jiraUserCredentialsSplit[0] : null;
- string? jiraUserPassword = jiraUserCredentialsSplit.Length > 1 ? jiraUserCredentialsSplit[1] : null;
-
- return new core.AppConfig()
- {
- UseVerboseFeedback = UseVerboseFeedback,
- JiraServer = new jira.ServerConfig()
- {
- Flavour = ServerFlavour,
- FlavourOptions = null,
- BaseUrl = jiraServerSpecification,
- UseProxy = !NoProxy,
- MaxConnectionsPerServer = MaxConnectionsPerServer,
- SkipSslCertificateCheck = SkipSslCertificateCheck
- },
- User = new core.UserConfig()
- {
- Name = jiraUserName,
- Password = jiraUserPassword
- },
- CsvOptions = new inputs.CsvFormatConfig()
- {
- FieldDelimiter = FieldDelimiter
- }
- };
- }
+namespace jwl.console;
+using CommandLine;
+
+[Verb("fill", isDefault: true, HelpText = "Fill Jira with worklogs")]
+public class FillCLI
+{
+ [Option('v', "verbose", HelpText = "\nGive more verbose feedback\nNote: Not implemented yet! 2do! :-)", Default = false, Hidden = true)]
+ public bool UseVerboseFeedback { get; set; }
+
+ [Option('i', "input", HelpText = "\nInput CSVs with the worklogs", Separator = ',', Required = true)]
+ public IEnumerable InputFiles { get; set; } = new string[0];
+
+ [Option("ifs", HelpText = "Input CSV fields delimiter"
+ + $"\nJSON config: $.{nameof(core.AppConfig.CsvOptions)}.{nameof(inputs.CsvFormatConfig.FieldDelimiter)}")]
+ public string? FieldDelimiter { get; set; }
+
+ [Option('t', "target", HelpText = "Connection string to Jira server in the form of @[:]"
+ + "\nNote: The \"https://\" scheme is automatically asserted with this option!"
+ + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.BaseUrl)} for [:]"
+ + $"\nJSON config: $.{nameof(core.AppConfig.User)}.{nameof(core.UserConfig.Name)} for ")]
+ public string? UserCredentials { get; set; }
+
+ [Option("server-flavour", HelpText = "Jira server flavour (whether vanilla or with some timesheet plugins)"
+ + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.Flavour)}"
+ + $"\nAvailable values: {nameof(jira.JiraServerFlavour.Vanilla)}, {nameof(jira.JiraServerFlavour.TempoTimeSheets)}, {nameof(jira.JiraServerFlavour.ICTime)}")]
+ public string? ServerFlavour { get; set; }
+
+ [Option("no-proxy", HelpText = "Turn off proxying the HTTP(S) connections to Jira server"
+ + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.UseProxy)} (negated!)")]
+ public bool? NoProxy { get; set; }
+
+ [Option("max-connections-per-server", HelpText = "Limit the number of concurrent connections to Jira server"
+ + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.MaxConnectionsPerServer)}")]
+ public int? MaxConnectionsPerServer { get; set; }
+
+ [Option("no-ssl-cert-check", HelpText = "Skip SSL/TLS certificate checks (for self-signed certificates)"
+ + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.SkipSslCertificateCheck)}")]
+ public bool? SkipSslCertificateCheck { get; set; }
+
+ public core.AppConfig ToAppConfig()
+ {
+ string[] connectionSpecifierSplit = UserCredentials?.Split('@', 2, StringSplitOptions.TrimEntries) ?? Array.Empty();
+
+ string? jiraServerSpecification = connectionSpecifierSplit.Length > 1 ? connectionSpecifierSplit[1] : null;
+ if (!jiraServerSpecification?.Contains(@"://") ?? false)
+ jiraServerSpecification = @"https://" + jiraServerSpecification;
+
+ string? jiraUserCredentials = connectionSpecifierSplit.Any() ? connectionSpecifierSplit[0] : null;
+ string[] jiraUserCredentialsSplit = jiraUserCredentials?.Split('/', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty();
+ string? jiraUserName = jiraUserCredentialsSplit.Any() ? jiraUserCredentialsSplit[0] : null;
+ string? jiraUserPassword = jiraUserCredentialsSplit.Length > 1 ? jiraUserCredentialsSplit[1] : null;
+
+ return new core.AppConfig()
+ {
+ UseVerboseFeedback = UseVerboseFeedback,
+ JiraServer = new jira.ServerConfig()
+ {
+ Flavour = ServerFlavour,
+ FlavourOptions = null,
+ BaseUrl = jiraServerSpecification,
+ UseProxy = !NoProxy,
+ MaxConnectionsPerServer = MaxConnectionsPerServer,
+ SkipSslCertificateCheck = SkipSslCertificateCheck
+ },
+ User = new core.UserConfig()
+ {
+ Name = jiraUserName,
+ Password = jiraUserPassword
+ },
+ CsvOptions = new inputs.CsvFormatConfig()
+ {
+ FieldDelimiter = FieldDelimiter
+ }
+ };
+ }
}
\ No newline at end of file
diff --git a/jwl.console/ScrollingConsoleProcessFeedback.cs b/jwl.console/ScrollingConsoleProcessFeedback.cs
index 82c0c69..4fd7e0f 100644
--- a/jwl.console/ScrollingConsoleProcessFeedback.cs
+++ b/jwl.console/ScrollingConsoleProcessFeedback.cs
@@ -1,171 +1,171 @@
-namespace jwl.console;
-using jwl.core;
-using jwl.infra;
-
-public class ScrollingConsoleProcessFeedback
- : ICoreProcessFeedback, IDisposable
-{
- public Action? FeedbackDelay { get; init; } = null;
-
- private bool _isDisposed;
- private int _numberOfWorklogsToInsert = 0;
- private int _numberOfWorklogsToDelete = 0;
-
- public ScrollingConsoleProcessFeedback(int totalSteps)
- {
- }
-
- public void Dispose()
- {
- // note: Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
- Dispose(disposing: true);
- GC.SuppressFinalize(this);
- }
-
- public void FillJiraWithWorklogsStart()
- {
- _numberOfWorklogsToInsert = 0;
- _numberOfWorklogsToDelete = 0;
- Console.Error.Write(@"Filling Jira with worklogs...");
- }
-
- public void FillJiraWithWorklogsSetTarget(int numberOfWorklogsToInsert, int numberOfWorklogsToDelete)
- {
- _numberOfWorklogsToInsert = numberOfWorklogsToInsert;
- _numberOfWorklogsToDelete = numberOfWorklogsToDelete;
- Console.Error.Write($"\rFilling Jira with worklogs (+{_numberOfWorklogsToInsert}/-{_numberOfWorklogsToDelete})...");
- }
-
- public void FillJiraWithWorklogsProcess(MultiTaskStats progress)
- {
- Console.Error.Write($"\rFilling Jira with worklogs (+{_numberOfWorklogsToInsert}/-{_numberOfWorklogsToDelete})... {ProgressPercentageAsString(progress)}");
- }
-
- public void FillJiraWithWorklogsEnd()
- {
- _numberOfWorklogsToInsert = 0;
- _numberOfWorklogsToDelete = 0;
- Console.Error.WriteLine();
- }
-
- public void NoExistingWorklogsToDelete()
- {
- }
-
- public void NoFilesOnInput()
- {
- Console.Error.WriteLine(@"Note: No files on input - no work to be done");
- }
-
- public void NoWorklogsToFill()
- {
- Console.Error.WriteLine(@"Note: Empty files on input - no work to be done");
- }
-
- public void OverallProcessStart()
- {
- Console.Error.WriteLine(@"Jira Worklogger CLI 1.1.0"); // 2do! read from assembly
- Console.Error.WriteLine(@"by Peter H., practically copyleft"); // 2do! read from assembly
- Console.Error.WriteLine(new string('-', Console.WindowWidth - 1));
- }
-
- public void OverallProcessEnd()
- {
- Console.Error.WriteLine(@"DONE");
- }
-
- public void PreloadAvailableWorklogTypesStart()
- {
- Console.Error.Write(@"Preloading available worklog types from server...");
- }
-
- public void PreloadAvailableWorklogTypesEnd()
- {
- Console.Error.WriteLine(@" OK");
- }
-
- public void PreloadUserInfoStart(string userName)
- {
- Console.Error.Write($"Preloading user \"{userName}\" info from server...");
- }
-
- public void PreloadUserInfoEnd()
- {
- Console.Error.WriteLine(@" OK");
- }
-
- public void ReadCsvInputStart()
- {
- Console.Error.Write(@"Reading input files...");
- }
-
- public void ReadCsvInputSetTarget(int numberOfInputFiles)
- {
- Console.Error.Write($"\rReading {numberOfInputFiles} input files...");
- }
-
- public void ReadCsvInputProcess(MultiTaskStats progress)
- {
- Console.Error.Write($"\rReading {progress.Total} input files... {ProgressPercentageAsString(progress)}");
- }
-
- public void ReadCsvInputEnd()
- {
- Console.Error.WriteLine();
- }
-
- public void RetrieveWorklogsForDeletionStart()
- {
- Console.Error.Write(@"Retrieving list of worklogs to be deleted...");
- }
-
- public void RetrieveWorklogsForDeletionSetTarget(int count)
- {
- Console.Error.Write($"\rRetrieving list of worklogs ({count} Jira issues) to be deleted...");
- }
-
- public void RetrieveWorklogsForDeletionProcess(MultiTaskStats progress)
- {
- throw new NotImplementedException(@"--- checkpoint ---");
- }
-
- public void RetrieveWorklogsForDeletionEnd()
- {
- Console.Error.WriteLine(" OK");
- }
-
- protected static string ProgressPercentageAsString(MultiTaskStats progress)
- {
- string result;
-
- if (progress.Total <= 0)
- {
- result = @"done";
- }
- else
- {
- result = progress.SucceededPct.ToString("P");
-
- if (progress.ErredSoFar > 0)
- {
- result += " OK, " + progress.ErredSoFarPct.ToString("P") + " failed";
- }
- }
-
- return result;
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (!_isDisposed)
- {
- if (disposing)
- {
- }
-
- // TODO: free unmanaged resources (unmanaged objects) and override finalizer
- // TODO: set large fields to null
- _isDisposed = true;
- }
- }
-}
+namespace jwl.console;
+using jwl.core;
+using jwl.infra;
+
+public class ScrollingConsoleProcessFeedback
+ : ICoreProcessFeedback, IDisposable
+{
+ public Action? FeedbackDelay { get; init; } = null;
+
+ private bool _isDisposed;
+ private int _numberOfWorklogsToInsert = 0;
+ private int _numberOfWorklogsToDelete = 0;
+
+ public ScrollingConsoleProcessFeedback(int totalSteps)
+ {
+ }
+
+ public void Dispose()
+ {
+ // note: Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ public void FillJiraWithWorklogsStart()
+ {
+ _numberOfWorklogsToInsert = 0;
+ _numberOfWorklogsToDelete = 0;
+ Console.Error.Write(@"Filling Jira with worklogs...");
+ }
+
+ public void FillJiraWithWorklogsSetTarget(int numberOfWorklogsToInsert, int numberOfWorklogsToDelete)
+ {
+ _numberOfWorklogsToInsert = numberOfWorklogsToInsert;
+ _numberOfWorklogsToDelete = numberOfWorklogsToDelete;
+ Console.Error.Write($"\rFilling Jira with worklogs (+{_numberOfWorklogsToInsert}/-{_numberOfWorklogsToDelete})...");
+ }
+
+ public void FillJiraWithWorklogsProcess(MultiTaskStats progress)
+ {
+ Console.Error.Write($"\rFilling Jira with worklogs (+{_numberOfWorklogsToInsert}/-{_numberOfWorklogsToDelete})... {ProgressPercentageAsString(progress)}");
+ }
+
+ public void FillJiraWithWorklogsEnd()
+ {
+ _numberOfWorklogsToInsert = 0;
+ _numberOfWorklogsToDelete = 0;
+ Console.Error.WriteLine();
+ }
+
+ public void NoExistingWorklogsToDelete()
+ {
+ }
+
+ public void NoFilesOnInput()
+ {
+ Console.Error.WriteLine(@"Note: No files on input - no work to be done");
+ }
+
+ public void NoWorklogsToFill()
+ {
+ Console.Error.WriteLine(@"Note: Empty files on input - no work to be done");
+ }
+
+ public void OverallProcessStart()
+ {
+ Console.Error.WriteLine(@"Jira Worklogger CLI 1.1.0"); // 2do! read from assembly
+ Console.Error.WriteLine(@"by Peter H., practically copyleft"); // 2do! read from assembly
+ Console.Error.WriteLine(new string('-', Console.WindowWidth - 1));
+ }
+
+ public void OverallProcessEnd()
+ {
+ Console.Error.WriteLine(@"DONE");
+ }
+
+ public void PreloadAvailableWorklogTypesStart()
+ {
+ Console.Error.Write(@"Preloading available worklog types from server...");
+ }
+
+ public void PreloadAvailableWorklogTypesEnd()
+ {
+ Console.Error.WriteLine(@" OK");
+ }
+
+ public void PreloadUserInfoStart(string userName)
+ {
+ Console.Error.Write($"Preloading user \"{userName}\" info from server...");
+ }
+
+ public void PreloadUserInfoEnd()
+ {
+ Console.Error.WriteLine(@" OK");
+ }
+
+ public void ReadCsvInputStart()
+ {
+ Console.Error.Write(@"Reading input files...");
+ }
+
+ public void ReadCsvInputSetTarget(int numberOfInputFiles)
+ {
+ Console.Error.Write($"\rReading {numberOfInputFiles} input files...");
+ }
+
+ public void ReadCsvInputProcess(MultiTaskStats progress)
+ {
+ Console.Error.Write($"\rReading {progress.Total} input files... {ProgressPercentageAsString(progress)}");
+ }
+
+ public void ReadCsvInputEnd()
+ {
+ Console.Error.WriteLine();
+ }
+
+ public void RetrieveWorklogsForDeletionStart()
+ {
+ Console.Error.Write(@"Retrieving list of worklogs to be deleted...");
+ }
+
+ public void RetrieveWorklogsForDeletionSetTarget(int count)
+ {
+ Console.Error.Write($"\rRetrieving list of worklogs ({count} Jira issues) to be deleted...");
+ }
+
+ public void RetrieveWorklogsForDeletionProcess(MultiTaskStats progress)
+ {
+ throw new NotImplementedException(@"--- checkpoint ---");
+ }
+
+ public void RetrieveWorklogsForDeletionEnd()
+ {
+ Console.Error.WriteLine(" OK");
+ }
+
+ protected static string ProgressPercentageAsString(MultiTaskStats progress)
+ {
+ string result;
+
+ if (progress.Total <= 0)
+ {
+ result = @"done";
+ }
+ else
+ {
+ result = progress.SucceededPct.ToString("P");
+
+ if (progress.ErredSoFar > 0)
+ {
+ result += " OK, " + progress.ErredSoFarPct.ToString("P") + " failed";
+ }
+ }
+
+ return result;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_isDisposed)
+ {
+ if (disposing)
+ {
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override finalizer
+ // TODO: set large fields to null
+ _isDisposed = true;
+ }
+ }
+}
diff --git a/jwl.core/AppConfigFactory.cs b/jwl.core/AppConfigFactory.cs
index b1794ba..6437dcb 100644
--- a/jwl.core/AppConfigFactory.cs
+++ b/jwl.core/AppConfigFactory.cs
@@ -1,91 +1,91 @@
-namespace jwl.core;
-using jwl.inputs;
-using jwl.jira;
-using jwl.jira.Flavours;
-using Microsoft.Extensions.Configuration;
-using System.Xml.XPath;
-
-public static class AppConfigFactory
-{
- public const int DefaultMaxConnectionsPerServer = 4;
-
- private const string ConfigFileName = @"jwl.config";
- private const string FlavourConfigFileNameTemplate = @"jwl.{0}.config";
-
- public static AppConfig CreateWithDefaults()
- {
- return new AppConfig()
- {
- JiraServer = new ServerConfig()
- {
- Flavour = nameof(JiraServerFlavour.Vanilla),
- FlavourOptions = null,
- BaseUrl = @"http://jira.my-domain.xyz",
- MaxConnectionsPerServer = DefaultMaxConnectionsPerServer,
- SkipSslCertificateCheck = false,
- UseProxy = false
- },
- CsvOptions = new CsvFormatConfig()
- {
- FieldDelimiter = ","
- },
- User = new UserConfig()
- {
- Name = string.Empty,
- Password = null
- }
- };
- }
-
- public static AppConfig ReadConfig()
- {
- AppConfig? result;
-
- IConfiguration config = new ConfigurationBuilder()
- .AddJsonFile(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigFileName), optional: true)
- .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ConfigFileName), optional: true)
- .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ConfigFileName), optional: true)
- .AddJsonFile(Path.Combine(Path.GetFullPath("."), ConfigFileName), optional: true)
- .Build();
-
- result = config.Get(opt =>
- {
- opt.BindNonPublicProperties = false;
- opt.ErrorOnUnknownConfiguration = true;
- });
-
- if (result?.JiraServer != null)
- {
- JiraServerFlavour flavour = result.JiraServer.FlavourId;
- string flavourConfigFileName = string.Format(FlavourConfigFileNameTemplate, flavour.ToString().ToLower());
- IConfiguration flavourConfig = new ConfigurationBuilder()
- .AddJsonFile(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, flavourConfigFileName), optional: true)
- .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), flavourConfigFileName), optional: true)
- .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), flavourConfigFileName), optional: true)
- .AddJsonFile(Path.Combine(Path.GetFullPath("."), flavourConfigFileName), optional: true)
- .Build();
-
- result!.JiraServer.FlavourOptions = flavour switch
- {
- JiraServerFlavour.Vanilla => flavourConfig.Get(opt =>
- {
- opt.BindNonPublicProperties = false;
- opt.ErrorOnUnknownConfiguration = true;
- }),
- JiraServerFlavour.TempoTimeSheets => flavourConfig.Get(opt =>
- {
- opt.BindNonPublicProperties = false;
- opt.ErrorOnUnknownConfiguration = true;
- }),
- JiraServerFlavour.ICTime => flavourConfig.Get(opt =>
- {
- opt.BindNonPublicProperties = false;
- opt.ErrorOnUnknownConfiguration = true;
- }),
- _ => throw new ArgumentOutOfRangeException(nameof(result.JiraServer.Flavour), $"Don't know how to read flavour {flavour} specific options config")
- };
- }
-
- return result ?? CreateWithDefaults();
- }
-}
+namespace jwl.core;
+using jwl.inputs;
+using jwl.jira;
+using jwl.jira.Flavours;
+using Microsoft.Extensions.Configuration;
+using System.Xml.XPath;
+
+public static class AppConfigFactory
+{
+ public const int DefaultMaxConnectionsPerServer = 4;
+
+ private const string ConfigFileName = @"jwl.config";
+ private const string FlavourConfigFileNameTemplate = @"jwl.{0}.config";
+
+ public static AppConfig CreateWithDefaults()
+ {
+ return new AppConfig()
+ {
+ JiraServer = new ServerConfig()
+ {
+ Flavour = nameof(JiraServerFlavour.Vanilla),
+ FlavourOptions = null,
+ BaseUrl = @"http://jira.my-domain.xyz",
+ MaxConnectionsPerServer = DefaultMaxConnectionsPerServer,
+ SkipSslCertificateCheck = false,
+ UseProxy = false
+ },
+ CsvOptions = new CsvFormatConfig()
+ {
+ FieldDelimiter = ","
+ },
+ User = new UserConfig()
+ {
+ Name = string.Empty,
+ Password = null
+ }
+ };
+ }
+
+ public static AppConfig ReadConfig()
+ {
+ AppConfig? result;
+
+ IConfiguration config = new ConfigurationBuilder()
+ .AddJsonFile(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ConfigFileName), optional: true)
+ .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ConfigFileName), optional: true)
+ .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ConfigFileName), optional: true)
+ .AddJsonFile(Path.Combine(Path.GetFullPath("."), ConfigFileName), optional: true)
+ .Build();
+
+ result = config.Get(opt =>
+ {
+ opt.BindNonPublicProperties = false;
+ opt.ErrorOnUnknownConfiguration = true;
+ });
+
+ if (result?.JiraServer != null)
+ {
+ JiraServerFlavour flavour = result.JiraServer.FlavourId;
+ string flavourConfigFileName = string.Format(FlavourConfigFileNameTemplate, flavour.ToString().ToLower());
+ IConfiguration flavourConfig = new ConfigurationBuilder()
+ .AddJsonFile(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, flavourConfigFileName), optional: true)
+ .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), flavourConfigFileName), optional: true)
+ .AddJsonFile(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), flavourConfigFileName), optional: true)
+ .AddJsonFile(Path.Combine(Path.GetFullPath("."), flavourConfigFileName), optional: true)
+ .Build();
+
+ result!.JiraServer.FlavourOptions = flavour switch
+ {
+ JiraServerFlavour.Vanilla => flavourConfig.Get(opt =>
+ {
+ opt.BindNonPublicProperties = false;
+ opt.ErrorOnUnknownConfiguration = true;
+ }),
+ JiraServerFlavour.TempoTimeSheets => flavourConfig.Get(opt =>
+ {
+ opt.BindNonPublicProperties = false;
+ opt.ErrorOnUnknownConfiguration = true;
+ }),
+ JiraServerFlavour.ICTime => flavourConfig.Get(opt =>
+ {
+ opt.BindNonPublicProperties = false;
+ opt.ErrorOnUnknownConfiguration = true;
+ }),
+ _ => throw new ArgumentOutOfRangeException(nameof(result.JiraServer.Flavour), $"Don't know how to read flavour {flavour} specific options config")
+ };
+ }
+
+ return result ?? CreateWithDefaults();
+ }
+}
diff --git a/jwl.core/ICoreProcessFeedback.cs b/jwl.core/ICoreProcessFeedback.cs
index 6cd208d..cb0511a 100644
--- a/jwl.core/ICoreProcessFeedback.cs
+++ b/jwl.core/ICoreProcessFeedback.cs
@@ -1,28 +1,28 @@
-namespace jwl.core;
-using jwl.infra;
-
-public interface ICoreProcessFeedback
- : IDisposable
-{
- void FillJiraWithWorklogsStart();
- void FillJiraWithWorklogsSetTarget(int numberOfWorklogsToInsert, int numbeOfWorklogsToDelete);
- void FillJiraWithWorklogsProcess(MultiTaskStats progress);
- void FillJiraWithWorklogsEnd();
- void NoExistingWorklogsToDelete();
- void NoFilesOnInput();
- void NoWorklogsToFill();
- void OverallProcessStart();
- void OverallProcessEnd();
- void PreloadAvailableWorklogTypesStart();
- void PreloadAvailableWorklogTypesEnd();
- void PreloadUserInfoStart(string userName);
- void PreloadUserInfoEnd();
- void ReadCsvInputStart();
- void ReadCsvInputSetTarget(int numberOfInputFiles);
- void ReadCsvInputProcess(MultiTaskStats progress);
- void ReadCsvInputEnd();
- void RetrieveWorklogsForDeletionStart();
- void RetrieveWorklogsForDeletionSetTarget(int count);
- void RetrieveWorklogsForDeletionProcess(MultiTaskStats progress);
- void RetrieveWorklogsForDeletionEnd();
-}
+namespace jwl.core;
+using jwl.infra;
+
+public interface ICoreProcessFeedback
+ : IDisposable
+{
+ void FillJiraWithWorklogsStart();
+ void FillJiraWithWorklogsSetTarget(int numberOfWorklogsToInsert, int numbeOfWorklogsToDelete);
+ void FillJiraWithWorklogsProcess(MultiTaskStats progress);
+ void FillJiraWithWorklogsEnd();
+ void NoExistingWorklogsToDelete();
+ void NoFilesOnInput();
+ void NoWorklogsToFill();
+ void OverallProcessStart();
+ void OverallProcessEnd();
+ void PreloadAvailableWorklogTypesStart();
+ void PreloadAvailableWorklogTypesEnd();
+ void PreloadUserInfoStart(string userName);
+ void PreloadUserInfoEnd();
+ void ReadCsvInputStart();
+ void ReadCsvInputSetTarget(int numberOfInputFiles);
+ void ReadCsvInputProcess(MultiTaskStats progress);
+ void ReadCsvInputEnd();
+ void RetrieveWorklogsForDeletionStart();
+ void RetrieveWorklogsForDeletionSetTarget(int count);
+ void RetrieveWorklogsForDeletionProcess(MultiTaskStats progress);
+ void RetrieveWorklogsForDeletionEnd();
+}
diff --git a/jwl.core/MultiTaskStats.cs b/jwl.core/MultiTaskStats.cs
index fda1898..faacf2e 100644
--- a/jwl.core/MultiTaskStats.cs
+++ b/jwl.core/MultiTaskStats.cs
@@ -1,63 +1,63 @@
-namespace jwl.core;
-using jwl.infra;
-
-public class MultiTaskStats
-{
- public int Total { get; private set; }
- public int Succeeded { get; private set; }
- public float SucceededPct => Total > 0 ? (float)Succeeded / Total : float.NaN;
- public int Faulted { get; private set; }
- public float FaultedPct => Total > 0 ? (float)Faulted / Total : float.NaN;
- public int Cancelled { get; private set; }
- public float CancelledPct => Total > 0 ? (float)Cancelled / Total : float.NaN;
- public int Unknown { get; private set; }
- public float UnknownPct => Total > 0 ? (float)Unknown / Total : float.NaN;
-
- public int ErredSoFar => Faulted + Cancelled + Unknown;
- public float ErredSoFarPct => Total > 0 ? (float)ErredSoFar / Total : float.NaN;
- public int DoneSoFar => Succeeded + ErredSoFar;
- public float DoneSoFarPct => Total > 0 ? (float)DoneSoFar / Total : float.NaN;
-
- public MultiTaskStats(int total)
- {
- Total = total;
- Succeeded = 0;
- Faulted = 0;
- Cancelled = 0;
- Unknown = 0;
- }
-
- private object _locker = new object();
-
- public MultiTaskStats ApplyTaskStatus(TaskStatus? taskStatus)
- {
- if (taskStatus == null)
- return this;
-
- lock (_locker)
- {
- switch (taskStatus)
- {
- case TaskStatus.RanToCompletion:
- Succeeded++;
- break;
- case TaskStatus.Canceled:
- Cancelled++;
- break;
- case TaskStatus.Faulted:
- Faulted++;
- break;
- case TaskStatus.Created:
- case TaskStatus.WaitingForActivation:
- case TaskStatus.WaitingToRun:
- case TaskStatus.WaitingForChildrenToComplete:
- break;
- default:
- Unknown++;
- break;
- }
- }
-
- return this;
- }
-}
+namespace jwl.core;
+using jwl.infra;
+
+public class MultiTaskStats
+{
+ public int Total { get; private set; }
+ public int Succeeded { get; private set; }
+ public float SucceededPct => Total > 0 ? (float)Succeeded / Total : float.NaN;
+ public int Faulted { get; private set; }
+ public float FaultedPct => Total > 0 ? (float)Faulted / Total : float.NaN;
+ public int Cancelled { get; private set; }
+ public float CancelledPct => Total > 0 ? (float)Cancelled / Total : float.NaN;
+ public int Unknown { get; private set; }
+ public float UnknownPct => Total > 0 ? (float)Unknown / Total : float.NaN;
+
+ public int ErredSoFar => Faulted + Cancelled + Unknown;
+ public float ErredSoFarPct => Total > 0 ? (float)ErredSoFar / Total : float.NaN;
+ public int DoneSoFar => Succeeded + ErredSoFar;
+ public float DoneSoFarPct => Total > 0 ? (float)DoneSoFar / Total : float.NaN;
+
+ public MultiTaskStats(int total)
+ {
+ Total = total;
+ Succeeded = 0;
+ Faulted = 0;
+ Cancelled = 0;
+ Unknown = 0;
+ }
+
+ private object _locker = new object();
+
+ public MultiTaskStats ApplyTaskStatus(TaskStatus? taskStatus)
+ {
+ if (taskStatus == null)
+ return this;
+
+ lock (_locker)
+ {
+ switch (taskStatus)
+ {
+ case TaskStatus.RanToCompletion:
+ Succeeded++;
+ break;
+ case TaskStatus.Canceled:
+ Cancelled++;
+ break;
+ case TaskStatus.Faulted:
+ Faulted++;
+ break;
+ case TaskStatus.Created:
+ case TaskStatus.WaitingForActivation:
+ case TaskStatus.WaitingToRun:
+ case TaskStatus.WaitingForChildrenToComplete:
+ break;
+ default:
+ Unknown++;
+ break;
+ }
+ }
+
+ return this;
+ }
+}
diff --git a/jwl.core/_assets/jwl.config b/jwl.core/_assets/jwl.config
index c2200e7..704078c 100644
--- a/jwl.core/_assets/jwl.config
+++ b/jwl.core/_assets/jwl.config
@@ -1,17 +1,17 @@
-{
- "UseVerboseFeedback": false,
- "JiraServer": {
- "BaseUrl": "https://example.company.org/jira/",
- "Flavour": "Vanilla",
- "MaxConnectionsPerServer": 4,
- "UseProxy": false,
- "SkipSslCertificateCheck": false
- },
- "User": {
- "Name": "my Jira user name",
- "Password": "my Jira password"
- },
- "CsvOptions": {
- "FieldDelimiter": ","
- }
-}
+{
+ "UseVerboseFeedback": false,
+ "JiraServer": {
+ "BaseUrl": "https://example.company.org/jira/",
+ "Flavour": "Vanilla",
+ "MaxConnectionsPerServer": 4,
+ "UseProxy": false,
+ "SkipSslCertificateCheck": false
+ },
+ "User": {
+ "Name": "my Jira user name",
+ "Password": "my Jira password"
+ },
+ "CsvOptions": {
+ "FieldDelimiter": ","
+ }
+}
diff --git a/jwl.core/_assets/jwl.ictime.config b/jwl.core/_assets/jwl.ictime.config
index 545ec91..9067358 100644
--- a/jwl.core/_assets/jwl.ictime.config
+++ b/jwl.core/_assets/jwl.ictime.config
@@ -1,10 +1,10 @@
-{
- "PluginBaseUri": "rest/ictime/1.0",
- "DateFormat": "dd/MMM/yyyy",
- "TimeSpanFormat": "ddd\" d \"hh\" h \"mm\" m\"",
- "ActivityMap": {
- "none": "0",
- "dev": "1",
- "business analysis": "2"
- }
-}
+{
+ "PluginBaseUri": "rest/ictime/1.0",
+ "DateFormat": "dd/MMM/yyyy",
+ "TimeSpanFormat": "ddd\" d \"hh\" h \"mm\" m\"",
+ "ActivityMap": {
+ "none": "0",
+ "dev": "1",
+ "business analysis": "2"
+ }
+}
diff --git a/jwl.infra/DateOnlyUtils.cs b/jwl.infra/DateOnlyUtils.cs
index a2e8082..1b25ca0 100644
--- a/jwl.infra/DateOnlyUtils.cs
+++ b/jwl.infra/DateOnlyUtils.cs
@@ -1,31 +1,31 @@
-namespace jwl.infra;
-
-public static class DateOnlyUtils
-{
- public static (DateTime, DateTime) DateOnlyRangeToDateTimeRange(DateOnly from, DateOnly to)
- {
- if (from > to)
- throw new ArgumentException($"{nameof(to)} cannot be less than {nameof(from)}", $"{nameof(from)}, {nameof(to)}");
-
- DateTime minDt = from.ToDateTime(TimeOnly.MinValue);
- DateTime supDt = to.ToDateTime(TimeOnly.MinValue).AddDays(1);
-
- return (minDt, supDt);
- }
-
- public static int NumberOfDaysInRange(DateOnly from, DateOnly to)
- {
- (DateTime minDt, DateTime supDt) = DateOnlyRangeToDateTimeRange(from, to);
- return (int)(supDt - minDt).TotalDays;
- }
-
- public static int NumberOfDaysTo(this DateOnly from, DateOnly to)
- {
- return NumberOfDaysInRange(from, to) - 1;
- }
-
- public static int NumberOfDaysSince(this DateOnly to, DateOnly from)
- {
- return NumberOfDaysInRange(from, to) - 1;
- }
-}
+namespace jwl.infra;
+
+public static class DateOnlyUtils
+{
+ public static (DateTime, DateTime) DateOnlyRangeToDateTimeRange(DateOnly from, DateOnly to)
+ {
+ if (from > to)
+ throw new ArgumentException($"{nameof(to)} cannot be less than {nameof(from)}", $"{nameof(from)}, {nameof(to)}");
+
+ DateTime minDt = from.ToDateTime(TimeOnly.MinValue);
+ DateTime supDt = to.ToDateTime(TimeOnly.MinValue).AddDays(1);
+
+ return (minDt, supDt);
+ }
+
+ public static int NumberOfDaysInRange(DateOnly from, DateOnly to)
+ {
+ (DateTime minDt, DateTime supDt) = DateOnlyRangeToDateTimeRange(from, to);
+ return (int)(supDt - minDt).TotalDays;
+ }
+
+ public static int NumberOfDaysTo(this DateOnly from, DateOnly to)
+ {
+ return NumberOfDaysInRange(from, to) - 1;
+ }
+
+ public static int NumberOfDaysSince(this DateOnly to, DateOnly from)
+ {
+ return NumberOfDaysInRange(from, to) - 1;
+ }
+}
diff --git a/jwl.infra/HttpClientJsonExt.cs b/jwl.infra/HttpClientJsonExt.cs
index 09a9ccf..b9d92b9 100644
--- a/jwl.infra/HttpClientJsonExt.cs
+++ b/jwl.infra/HttpClientJsonExt.cs
@@ -1,31 +1,31 @@
-namespace jwl.infra;
-using System.Text.Json;
-
-public static class HttpClientJsonExt
-{
- public static async Task GetAsJsonAsync(this HttpClient self, string uri)
- {
- using Stream response = await self.GetStreamAsync(uri);
- TResponse result = await DeserializeJsonStreamAsync(response);
- return result;
- }
-
- public static async Task GetAsJsonAsync(this HttpClient self, Uri uri)
- {
- using Stream response = await self.GetStreamAsync(uri);
- TResponse result = await DeserializeJsonStreamAsync(response);
- return result;
- }
-
- public static async Task DeserializeJsonStreamAsync(Stream stream)
- {
- JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
- {
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- IncludeFields = true
- };
- TResponse result = await JsonSerializer.DeserializeAsync(stream, jsonSerializerOptions)
- ?? throw new NullReferenceException("JSON deserialization NULL result");
- return result;
- }
+namespace jwl.infra;
+using System.Text.Json;
+
+public static class HttpClientJsonExt
+{
+ public static async Task GetAsJsonAsync(this HttpClient self, string uri)
+ {
+ using Stream response = await self.GetStreamAsync(uri);
+ TResponse result = await DeserializeJsonStreamAsync(response);
+ return result;
+ }
+
+ public static async Task GetAsJsonAsync(this HttpClient self, Uri uri)
+ {
+ using Stream response = await self.GetStreamAsync(uri);
+ TResponse result = await DeserializeJsonStreamAsync(response);
+ return result;
+ }
+
+ public static async Task DeserializeJsonStreamAsync(Stream stream)
+ {
+ JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ IncludeFields = true
+ };
+ TResponse result = await JsonSerializer.DeserializeAsync(stream, jsonSerializerOptions)
+ ?? throw new NullReferenceException("JSON deserialization NULL result");
+ return result;
+ }
}
\ No newline at end of file
diff --git a/jwl.infra/JiraIssueKey.cs b/jwl.infra/JiraIssueKey.cs
index dc96d17..9ce6442 100644
--- a/jwl.infra/JiraIssueKey.cs
+++ b/jwl.infra/JiraIssueKey.cs
@@ -1,60 +1,60 @@
-namespace jwl.infra;
-using System.Collections;
-using System.Diagnostics.CodeAnalysis;
-
-public struct JiraIssueKey
-{
- public string ProjectKey { get; private set; }
- public int IssueNumber { get; private set; }
-
- public class ProjectOnlyEqualityComparer : IEqualityComparer
- {
- public ProjectOnlyEqualityComparer()
- {
- }
-
- public bool Equals(JiraIssueKey x, JiraIssueKey y)
- {
- return x.ProjectKey.Equals(y.ProjectKey);
- }
-
- public int GetHashCode([DisallowNull] JiraIssueKey obj)
- {
- return obj.ProjectKey.GetHashCode();
- }
- }
-
- public JiraIssueKey(string project, int issueNumber)
- {
- ProjectKey = project;
- IssueNumber = issueNumber;
- }
-
- public JiraIssueKey(string issueKey)
- {
- (ProjectKey, IssueNumber) = SplitIssueKey(issueKey);
- }
-
- public override string ToString()
- {
- return ProjectKey + "-" + IssueNumber.ToString();
- }
-
- private static (string, int) SplitIssueKey(string issueKey)
- {
- (string, int) result;
-
- string[] issueKeySplit = issueKey.Split('-', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- if (issueKeySplit.Length != 2)
- throw new ArgumentOutOfRangeException(nameof(issueKey), issueKey, $"Invalid format of the issue key; must be -");
-
- result.Item1 = issueKeySplit[0];
-
- if (!int.TryParse(issueKeySplit[1], out result.Item2))
- throw new ArgumentOutOfRangeException(nameof(issueKey), issueKey, @"Issue number not an actual number");
- if (result.Item2 <= 0)
- throw new ArgumentOutOfRangeException(nameof(issueKey), issueKey, @"Issue number must be positive");
-
- return result;
- }
-}
+namespace jwl.infra;
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+public struct JiraIssueKey
+{
+ public string ProjectKey { get; private set; }
+ public int IssueNumber { get; private set; }
+
+ public class ProjectOnlyEqualityComparer : IEqualityComparer
+ {
+ public ProjectOnlyEqualityComparer()
+ {
+ }
+
+ public bool Equals(JiraIssueKey x, JiraIssueKey y)
+ {
+ return x.ProjectKey.Equals(y.ProjectKey);
+ }
+
+ public int GetHashCode([DisallowNull] JiraIssueKey obj)
+ {
+ return obj.ProjectKey.GetHashCode();
+ }
+ }
+
+ public JiraIssueKey(string project, int issueNumber)
+ {
+ ProjectKey = project;
+ IssueNumber = issueNumber;
+ }
+
+ public JiraIssueKey(string issueKey)
+ {
+ (ProjectKey, IssueNumber) = SplitIssueKey(issueKey);
+ }
+
+ public override string ToString()
+ {
+ return ProjectKey + "-" + IssueNumber.ToString();
+ }
+
+ private static (string, int) SplitIssueKey(string issueKey)
+ {
+ (string, int) result;
+
+ string[] issueKeySplit = issueKey.Split('-', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (issueKeySplit.Length != 2)
+ throw new ArgumentOutOfRangeException(nameof(issueKey), issueKey, $"Invalid format of the issue key; must be -");
+
+ result.Item1 = issueKeySplit[0];
+
+ if (!int.TryParse(issueKeySplit[1], out result.Item2))
+ throw new ArgumentOutOfRangeException(nameof(issueKey), issueKey, @"Issue number not an actual number");
+ if (result.Item2 <= 0)
+ throw new ArgumentOutOfRangeException(nameof(issueKey), issueKey, @"Issue number must be positive");
+
+ return result;
+ }
+}
diff --git a/jwl.infra/StringExt.cs b/jwl.infra/StringExt.cs
index 0104818..510d68d 100644
--- a/jwl.infra/StringExt.cs
+++ b/jwl.infra/StringExt.cs
@@ -1,18 +1,18 @@
-namespace jwl.infra;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-public static class StringExt
-{
- public static string ToLowerFirstChar(this string str)
- => string.Create(str.Length, str, (output, input)
- =>
- {
- input.CopyTo(output);
- output[0] = char.ToLowerInvariant(input[0]);
- });
-}
+namespace jwl.infra;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+public static class StringExt
+{
+ public static string ToLowerFirstChar(this string str)
+ => string.Create(str.Length, str, (output, input)
+ =>
+ {
+ input.CopyTo(output);
+ output[0] = char.ToLowerInvariant(input[0]);
+ });
+}
diff --git a/jwl.inputs/IWorklogReader.cs b/jwl.inputs/IWorklogReader.cs
index 8c674a0..fdff042 100644
--- a/jwl.inputs/IWorklogReader.cs
+++ b/jwl.inputs/IWorklogReader.cs
@@ -1,6 +1,6 @@
-namespace jwl.inputs;
-
-public interface IWorklogReader : IDisposable
-{
- IEnumerable Read(Action? validateResult = null);
-}
+namespace jwl.inputs;
+
+public interface IWorklogReader : IDisposable
+{
+ IEnumerable Read(Action? validateResult = null);
+}
diff --git a/jwl.inputs/InputWorkLog.cs b/jwl.inputs/InputWorkLog.cs
index 7685368..24d773e 100644
--- a/jwl.inputs/InputWorkLog.cs
+++ b/jwl.inputs/InputWorkLog.cs
@@ -1,12 +1,12 @@
-#pragma warning disable SA1313
-namespace jwl.inputs;
-using jwl.infra;
-
-public struct InputWorkLog
-{
- public JiraIssueKey IssueKey;
- public DateTime Date;
- public TimeSpan TimeSpent;
- public string WorkLogActivity;
- public string WorkLogComment;
-}
+#pragma warning disable SA1313
+namespace jwl.inputs;
+using jwl.infra;
+
+public struct InputWorkLog
+{
+ public JiraIssueKey IssueKey;
+ public DateTime Date;
+ public TimeSpan TimeSpent;
+ public string WorkLogActivity;
+ public string WorkLogComment;
+}
diff --git a/jwl.inputs/JiraWorklogRawCsv.cs b/jwl.inputs/JiraWorklogRawCsv.cs
index 98a34fe..bde6cfb 100644
--- a/jwl.inputs/JiraWorklogRawCsv.cs
+++ b/jwl.inputs/JiraWorklogRawCsv.cs
@@ -1,12 +1,12 @@
-#pragma warning disable SA1313
-namespace jwl.inputs;
-
-public record JiraWorklogRawCsv
-(
- string IssueKey,
- string Date,
- string TimeSpent,
- string WorkLogActivity,
- string WorkLogComment
-)
-{ }
+#pragma warning disable SA1313
+namespace jwl.inputs;
+
+public record JiraWorklogRawCsv
+(
+ string IssueKey,
+ string Date,
+ string TimeSpent,
+ string WorkLogActivity,
+ string WorkLogComment
+)
+{ }
diff --git a/jwl.inputs/WorklogCsvReader.cs b/jwl.inputs/WorklogCsvReader.cs
index 297cbcc..4f2dfd3 100644
--- a/jwl.inputs/WorklogCsvReader.cs
+++ b/jwl.inputs/WorklogCsvReader.cs
@@ -1,97 +1,97 @@
-namespace jwl.inputs;
-using System.Globalization;
-using CsvHelper;
-using CsvHelper.Configuration;
-using jwl.infra;
-using jwl.jira;
-
-public class WorklogCsvReader : IWorklogReader
-{
- public bool ErrorOnEmptyRow { get; init; } = true;
-
- private CsvReader _csvReader;
- private WorklogReaderAggregatedConfig _readerConfig;
-
- public WorklogCsvReader(TextReader inputFile, WorklogReaderAggregatedConfig readerConfig)
- {
- _readerConfig = readerConfig;
- CsvConfiguration config = new (CultureInfo.InvariantCulture)
- {
- Delimiter = readerConfig.CsvFormatConfig?.FieldDelimiter ?? ","
- };
-
- _csvReader = new CsvReader(inputFile, config);
- }
-
- public IEnumerable Read(Action? postProcessResult = null)
- {
- string[] dateFormats =
- {
- @"yyyy-MM-dd",
- @"yyyy-MM-dd hh:mm:ss",
- @"yyyy/MM/dd",
- @"yyyy/MM/dd hh:mm:ss",
- @"dd.MM.YYYY",
- @"dd.MM.YYYY hh:mm:ss"
- };
-
- string[] timespanTimeFormats =
- {
- @"hh\:mm\:ss",
- @"hh\:mm",
- @"mm",
- @"hh'h'mm",
- @"hh'h'mm'm'"
- };
-
- foreach (JiraWorklogRawCsv row in _csvReader.GetRecords())
- {
- if (row == null)
- {
- if (ErrorOnEmptyRow)
- throw new InvalidDataException("Empty row on input");
- else
- continue;
- }
-
- InputWorkLog result;
-
- try
- {
- JiraIssueKey worklogIssueKey = new JiraIssueKey(row.IssueKey);
-
- if (!DateTime.TryParseExact(row.Date, dateFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime worklogDate))
- throw new FormatException($"Invalid date/datetime value \"{row.Date}\"");
-
- TimeSpan worklogTimeSpent = HumanReadableTimeSpan.Parse(row.TimeSpent, timespanTimeFormats);
-
- result = new InputWorkLog()
- {
- IssueKey = worklogIssueKey,
- Date = worklogDate,
- TimeSpent = worklogTimeSpent,
- WorkLogActivity = row.WorkLogActivity,
- WorkLogComment = row.WorkLogComment
- };
-
- postProcessResult?.Invoke(result);
- }
- catch (Exception e)
- {
- throw new InputRowException(_csvReader.Parser.RawRow, e);
- }
-
- yield return result;
- }
- }
-
- public void Dispose()
- {
- _csvReader.Dispose();
- }
-
- private void ParseHeader()
- {
- // 2do!
- }
-}
+namespace jwl.inputs;
+using System.Globalization;
+using CsvHelper;
+using CsvHelper.Configuration;
+using jwl.infra;
+using jwl.jira;
+
+public class WorklogCsvReader : IWorklogReader
+{
+ public bool ErrorOnEmptyRow { get; init; } = true;
+
+ private CsvReader _csvReader;
+ private WorklogReaderAggregatedConfig _readerConfig;
+
+ public WorklogCsvReader(TextReader inputFile, WorklogReaderAggregatedConfig readerConfig)
+ {
+ _readerConfig = readerConfig;
+ CsvConfiguration config = new (CultureInfo.InvariantCulture)
+ {
+ Delimiter = readerConfig.CsvFormatConfig?.FieldDelimiter ?? ","
+ };
+
+ _csvReader = new CsvReader(inputFile, config);
+ }
+
+ public IEnumerable Read(Action? postProcessResult = null)
+ {
+ string[] dateFormats =
+ {
+ @"yyyy-MM-dd",
+ @"yyyy-MM-dd hh:mm:ss",
+ @"yyyy/MM/dd",
+ @"yyyy/MM/dd hh:mm:ss",
+ @"dd.MM.YYYY",
+ @"dd.MM.YYYY hh:mm:ss"
+ };
+
+ string[] timespanTimeFormats =
+ {
+ @"hh\:mm\:ss",
+ @"hh\:mm",
+ @"mm",
+ @"hh'h'mm",
+ @"hh'h'mm'm'"
+ };
+
+ foreach (JiraWorklogRawCsv row in _csvReader.GetRecords())
+ {
+ if (row == null)
+ {
+ if (ErrorOnEmptyRow)
+ throw new InvalidDataException("Empty row on input");
+ else
+ continue;
+ }
+
+ InputWorkLog result;
+
+ try
+ {
+ JiraIssueKey worklogIssueKey = new JiraIssueKey(row.IssueKey);
+
+ if (!DateTime.TryParseExact(row.Date, dateFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime worklogDate))
+ throw new FormatException($"Invalid date/datetime value \"{row.Date}\"");
+
+ TimeSpan worklogTimeSpent = HumanReadableTimeSpan.Parse(row.TimeSpent, timespanTimeFormats);
+
+ result = new InputWorkLog()
+ {
+ IssueKey = worklogIssueKey,
+ Date = worklogDate,
+ TimeSpent = worklogTimeSpent,
+ WorkLogActivity = row.WorkLogActivity,
+ WorkLogComment = row.WorkLogComment
+ };
+
+ postProcessResult?.Invoke(result);
+ }
+ catch (Exception e)
+ {
+ throw new InputRowException(_csvReader.Parser.RawRow, e);
+ }
+
+ yield return result;
+ }
+ }
+
+ public void Dispose()
+ {
+ _csvReader.Dispose();
+ }
+
+ private void ParseHeader()
+ {
+ // 2do!
+ }
+}
diff --git a/jwl.jira/Flavours/FlavourICTimeOptions.cs b/jwl.jira/Flavours/FlavourICTimeOptions.cs
index 9d0dc1e..39961f1 100644
--- a/jwl.jira/Flavours/FlavourICTimeOptions.cs
+++ b/jwl.jira/Flavours/FlavourICTimeOptions.cs
@@ -1,9 +1,9 @@
-namespace jwl.jira.Flavours;
-
-public class FlavourICTimeOptions : IFlavourOptions
-{
- public string PluginBaseUri { get; init; } = "rest/ictime/1.0";
- public string DateFormat { get; init; } = "dd/MMM/yyyy";
- public string TimeSpanFormat { get; init; } = "ddd\" d \"hh\" h \"mm\" m\"";
- public Dictionary? ActivityMap { get; init; }
+namespace jwl.jira.Flavours;
+
+public class FlavourICTimeOptions : IFlavourOptions
+{
+ public string PluginBaseUri { get; init; } = "rest/ictime/1.0";
+ public string DateFormat { get; init; } = "dd/MMM/yyyy";
+ public string TimeSpanFormat { get; init; } = "ddd\" d \"hh\" h \"mm\" m\"";
+ public Dictionary? ActivityMap { get; init; }
}
\ No newline at end of file
diff --git a/jwl.jira/Flavours/FlavourTempoTimesheetsOptions.cs b/jwl.jira/Flavours/FlavourTempoTimesheetsOptions.cs
index 8c81ebd..46b1a27 100644
--- a/jwl.jira/Flavours/FlavourTempoTimesheetsOptions.cs
+++ b/jwl.jira/Flavours/FlavourTempoTimesheetsOptions.cs
@@ -1,8 +1,8 @@
-namespace jwl.jira.Flavours;
-
-public class FlavourTempoTimesheetsOptions
- : IFlavourOptions
-{
- public string PluginBaseUri { get; init; } = @"rest/tempo-timesheets/4";
- public string PluginCoreUri { get; init; } = @"rest/tempo-core/1";
-}
+namespace jwl.jira.Flavours;
+
+public class FlavourTempoTimesheetsOptions
+ : IFlavourOptions
+{
+ public string PluginBaseUri { get; init; } = @"rest/tempo-timesheets/4";
+ public string PluginCoreUri { get; init; } = @"rest/tempo-core/1";
+}
diff --git a/jwl.jira/Flavours/FlavourVanillaJiraOptions.cs b/jwl.jira/Flavours/FlavourVanillaJiraOptions.cs
index 798afa5..9ce2491 100644
--- a/jwl.jira/Flavours/FlavourVanillaJiraOptions.cs
+++ b/jwl.jira/Flavours/FlavourVanillaJiraOptions.cs
@@ -1,7 +1,7 @@
-namespace jwl.jira.Flavours;
-
-public class FlavourVanillaJiraOptions
- : IFlavourOptions
-{
- public string PluginBaseUri { get; init; } = @"rest/api/2";
-}
+namespace jwl.jira.Flavours;
+
+public class FlavourVanillaJiraOptions
+ : IFlavourOptions
+{
+ public string PluginBaseUri { get; init; } = @"rest/api/2";
+}
diff --git a/jwl.jira/Flavours/JiraServerFlavour.cs b/jwl.jira/Flavours/JiraServerFlavour.cs
index 10efe2b..5e55975 100644
--- a/jwl.jira/Flavours/JiraServerFlavour.cs
+++ b/jwl.jira/Flavours/JiraServerFlavour.cs
@@ -1,8 +1,8 @@
-namespace jwl.jira;
-
-public enum JiraServerFlavour
-{
- Vanilla = 0,
- TempoTimeSheets = 1,
- ICTime = 2
-}
+namespace jwl.jira;
+
+public enum JiraServerFlavour
+{
+ Vanilla = 0,
+ TempoTimeSheets = 1,
+ ICTime = 2
+}
diff --git a/jwl.jira/Flavours/JiraWithICTimePluginApi.cs b/jwl.jira/Flavours/JiraWithICTimePluginApi.cs
index cd9c42a..ad5e71e 100644
--- a/jwl.jira/Flavours/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/Flavours/JiraWithICTimePluginApi.cs
@@ -1,222 +1,222 @@
-namespace jwl.jira;
-
-using System.Globalization;
-using System.Xml.Serialization;
-using jwl.infra;
-using jwl.jira.api.rest.request;
-using jwl.jira.Flavours;
-using jwl.wadl;
-
-// https://interconcept.atlassian.net/wiki/spaces/ICTIME/pages/31686672/API
-public class JiraWithICTimePluginApi
- : IJiraClient
-{
- public string UserName { get; }
- public api.rest.common.JiraUserInfo UserInfo => _vanillaJiraApi.UserInfo;
-
- public Lazy> Endpoints =>
- new Lazy>(() => this.GetWADL().Result
- .AsEnumerable()
- .Where(res => !string.IsNullOrEmpty(res.Id))
- .ToDictionary(res => res.Id ?? string.Empty)
- );
-
- public const string CreateWorkLogMethodName = "createWorklog";
- public wadl.ComposedWadlMethodDefinition CreateWorkLogMethodDefinition => Endpoints.Value[CreateWorkLogMethodName];
-
- public const string GetActivityTypesForProjectMethodName = "getActivityTypesForProject";
- public wadl.ComposedWadlMethodDefinition GetActivityTypesForProjectMethodDefinition => Endpoints.Value[GetActivityTypesForProjectMethodName];
-
- private readonly HttpClient _httpClient;
- private readonly FlavourICTimeOptions _flavourOptions;
- private readonly FlavourICTimeOptions _defaultFlavourOptions = new FlavourICTimeOptions();
- private readonly VanillaJiraClient _vanillaJiraApi;
-
- public JiraWithICTimePluginApi(HttpClient httpClient, string userName, FlavourICTimeOptions? flavourOptions)
- {
- _httpClient = httpClient;
- UserName = userName;
- _flavourOptions = flavourOptions ?? _defaultFlavourOptions;
- _vanillaJiraApi = new VanillaJiraClient(httpClient, userName, null);
- }
-
- public async Task GetAvailableActivities(string issueKey)
- {
- var result = await GetAvailableActivities(new string[] { issueKey });
- return result[issueKey];
- }
-
- public async Task> GetAvailableActivities(IEnumerable issueKeys)
- {
- IEnumerable issueKeysParsed = issueKeys
- .Select(issueKey => new JiraIssueKey(issueKey))
- .ToArray();
-
- IEqualityComparer projectComparer = new JiraIssueKey.ProjectOnlyEqualityComparer();
- IEnumerable distinctProjects = issueKeysParsed
- .Distinct(projectComparer);
-
- ComposedWadlMethodDefinition endPoint = GetActivityTypesForProjectMethodDefinition;
-
- HashSet missingParameters = endPoint.Parameters
- .Where(par => !string.IsNullOrEmpty(par.Name))
- .Select(par => par.Name ?? string.Empty)
- .ToHashSet();
-
- // define
- IEnumerable> uris = distinctProjects
- .Select(issueKey => new KeyValuePair(issueKey, endPoint.ResourcePath.Replace("{issueKey}", issueKey.ToString())));
-
- missingParameters.Remove("issueKey");
-
- // check
- HttpMethod expectedMethod = HttpMethod.Get;
- bool isCorrectHttpMethod = endPoint.HttpMethod == expectedMethod;
- if (!isCorrectHttpMethod)
- throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} executes via ${endPoint.HttpMethod} method (${expectedMethod.ToString().ToUpperInvariant()} expected)");
-
- if (missingParameters.Any())
- throw new ArgumentNullException($"Missing assignment of {string.Join(',', missingParameters)} in the call of {endPoint.Id} at resource path {endPoint.ResourcePath}");
-
- bool providesJsonResponses = endPoint.Response?.Representations?
- .Any(repr => repr.MediaType == WadlRepresentation.MediaTypes.Json) ?? false;
-
- if (providesJsonResponses)
- throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} does not respond in JSON");
-
- // execute
- KeyValuePair>[] responseTaks = uris
- .Select(uri => new KeyValuePair>(uri.Key, _httpClient.GetAsJsonAsync(uri.Value)))
- .ToArray();
-
- await Task.WhenAll(responseTaks.Select(x => x.Value));
-
- Dictionary result = responseTaks
- .Join(
- inner: issueKeysParsed,
- outerKeySelector: outer => outer.Key,
- innerKeySelector: inner => inner,
- resultSelector: (outer, inner) => new KeyValuePair(
- outer.Key.ToString(),
- outer.Value.Result
- .Select((def, ix) => new WorkLogType(
- Key: def.Id.ToString(),
- Value: def.Name,
- Sequence: ix
- ))
- .ToArray()
- ),
- comparer: projectComparer
- )
- .ToDictionary(
- keySelector: x => x.Key,
- elementSelector: x => x.Value
- );
-
- return result;
- }
-
- public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, string issueKey)
- {
- return await _vanillaJiraApi.GetIssueWorkLogs(from, to, issueKey);
- }
-
- public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, IEnumerable? issueKeys)
- {
- return await _vanillaJiraApi.GetIssueWorkLogs(from, to, issueKeys);
- }
-
- public async Task AddWorkLog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
- {
- ComposedWadlMethodDefinition endPoint = CreateWorkLogMethodDefinition;
-
- HashSet missingParameters = endPoint.Parameters
- .Where(par => !string.IsNullOrEmpty(par.Name))
- .Select(par => par.Name ?? string.Empty)
- .ToHashSet();
-
- // define
- string uri = endPoint.ResourcePath
- .Replace("issueKey", issueKey);
- missingParameters.Remove("issueKey");
-
- Dictionary args = new ()
- {
- ["date"] = day.ToString(_flavourOptions.DateFormat, CultureInfo.InvariantCulture),
- ["logWorkOption"] = ICTimeAddWorklogByIssueKey.LogWorkOption.Summary.ToString().ToLowerFirstChar(),
- ["comment"] = comment ?? string.Empty,
- ["timeLogged"] = TimeSpan.FromSeconds(timeSpentSeconds).ToString(_flavourOptions.TimeSpanFormat),
- ["activity"] = _flavourOptions.ActivityMap == null || !_flavourOptions.ActivityMap.Any() || string.IsNullOrEmpty(activity)
- ? activity ?? string.Empty
- : _flavourOptions.ActivityMap[activity],
- ["user"] = this.UserName,
- ["charged"] = true.ToString().ToLowerInvariant()
- };
- missingParameters.RemoveWhere(elm => args.ContainsKey(elm));
-
- // check
- HttpMethod expectedMethod = HttpMethod.Post;
- bool isCorrectHttpMethod = endPoint.HttpMethod == expectedMethod;
- if (!isCorrectHttpMethod)
- throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} executes via ${endPoint.HttpMethod} method (${expectedMethod.ToString().ToUpperInvariant()} expected)");
-
- if (missingParameters.Any())
- throw new ArgumentNullException($"Missing assignment of {string.Join(',', missingParameters)} in the call of {endPoint.Id} at resource path {endPoint.ResourcePath}");
-
- bool providesJsonResponses = endPoint.Response?.Representations?
- .Any(repr => repr.MediaType == WadlRepresentation.MediaTypes.Json) ?? false;
-
- if (providesJsonResponses)
- throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} does not respond in JSON");
-
- // execute
- HttpContent content = new FormUrlEncodedContent(args);
- HttpResponseMessage response = await _httpClient.PostAsync(uri, content);
-
- if (providesJsonResponses)
- await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
- }
-
- public async Task AddWorkLogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
- {
- DateOnly[] daysInPeriod = Enumerable.Range(0, dayFrom.NumberOfDaysTo(dayTo))
- .Select(i => dayFrom.AddDays(i))
- .Where(day => includeNonWorkingDays || day.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
- .ToArray();
-
- if (!daysInPeriod.Any())
- return;
-
- int timeSpentSecondsPerSingleDay = timeSpentSeconds / daysInPeriod.Length;
-
- Task[] addWorklogTasks = daysInPeriod
- .Select(day => AddWorkLog(issueKey, day, timeSpentSecondsPerSingleDay, activity, comment))
- .ToArray();
-
- await Task.WhenAll(addWorklogTasks);
- }
-
- public async Task DeleteWorkLog(long issueId, long worklogId, bool notifyUsers = false)
- {
- await _vanillaJiraApi.DeleteWorkLog(issueId, worklogId, notifyUsers);
- }
-
- public async Task UpdateWorkLog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
- {
- await _vanillaJiraApi.UpdateWorkLog(issueKey, worklogId, day, timeSpentSeconds, activity, comment);
- }
-
- private async Task GetWADL()
- {
- Uri uri = new Uri($"{_flavourOptions.PluginBaseUri}/application.wadl", UriKind.Relative);
- using Stream response = await _httpClient.GetStreamAsync(uri);
- if (response == null || response.Length <= 0)
- throw new HttpRequestException($"Empty content received from ${uri}");
-
- XmlSerializer serializer = new XmlSerializer(typeof(WadlApplication));
- object resultObj = serializer.Deserialize(response) ?? throw new InvalidDataException($"Empty/null content deserialization result");
-
- WadlApplication result = (WadlApplication)resultObj;
- return result;
- }
-}
+namespace jwl.jira;
+
+using System.Globalization;
+using System.Xml.Serialization;
+using jwl.infra;
+using jwl.jira.api.rest.request;
+using jwl.jira.Flavours;
+using jwl.wadl;
+
+// https://interconcept.atlassian.net/wiki/spaces/ICTIME/pages/31686672/API
+public class JiraWithICTimePluginApi
+ : IJiraClient
+{
+ public string UserName { get; }
+ public api.rest.common.JiraUserInfo UserInfo => _vanillaJiraApi.UserInfo;
+
+ public Lazy> Endpoints =>
+ new Lazy>(() => this.GetWADL().Result
+ .AsEnumerable()
+ .Where(res => !string.IsNullOrEmpty(res.Id))
+ .ToDictionary(res => res.Id ?? string.Empty)
+ );
+
+ public const string CreateWorkLogMethodName = "createWorklog";
+ public wadl.ComposedWadlMethodDefinition CreateWorkLogMethodDefinition => Endpoints.Value[CreateWorkLogMethodName];
+
+ public const string GetActivityTypesForProjectMethodName = "getActivityTypesForProject";
+ public wadl.ComposedWadlMethodDefinition GetActivityTypesForProjectMethodDefinition => Endpoints.Value[GetActivityTypesForProjectMethodName];
+
+ private readonly HttpClient _httpClient;
+ private readonly FlavourICTimeOptions _flavourOptions;
+ private readonly FlavourICTimeOptions _defaultFlavourOptions = new FlavourICTimeOptions();
+ private readonly VanillaJiraClient _vanillaJiraApi;
+
+ public JiraWithICTimePluginApi(HttpClient httpClient, string userName, FlavourICTimeOptions? flavourOptions)
+ {
+ _httpClient = httpClient;
+ UserName = userName;
+ _flavourOptions = flavourOptions ?? _defaultFlavourOptions;
+ _vanillaJiraApi = new VanillaJiraClient(httpClient, userName, null);
+ }
+
+ public async Task GetAvailableActivities(string issueKey)
+ {
+ var result = await GetAvailableActivities(new string[] { issueKey });
+ return result[issueKey];
+ }
+
+ public async Task> GetAvailableActivities(IEnumerable issueKeys)
+ {
+ IEnumerable issueKeysParsed = issueKeys
+ .Select(issueKey => new JiraIssueKey(issueKey))
+ .ToArray();
+
+ IEqualityComparer projectComparer = new JiraIssueKey.ProjectOnlyEqualityComparer();
+ IEnumerable distinctProjects = issueKeysParsed
+ .Distinct(projectComparer);
+
+ ComposedWadlMethodDefinition endPoint = GetActivityTypesForProjectMethodDefinition;
+
+ HashSet missingParameters = endPoint.Parameters
+ .Where(par => !string.IsNullOrEmpty(par.Name))
+ .Select(par => par.Name ?? string.Empty)
+ .ToHashSet();
+
+ // define
+ IEnumerable> uris = distinctProjects
+ .Select(issueKey => new KeyValuePair(issueKey, endPoint.ResourcePath.Replace("{issueKey}", issueKey.ToString())));
+
+ missingParameters.Remove("issueKey");
+
+ // check
+ HttpMethod expectedMethod = HttpMethod.Get;
+ bool isCorrectHttpMethod = endPoint.HttpMethod == expectedMethod;
+ if (!isCorrectHttpMethod)
+ throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} executes via ${endPoint.HttpMethod} method (${expectedMethod.ToString().ToUpperInvariant()} expected)");
+
+ if (missingParameters.Any())
+ throw new ArgumentNullException($"Missing assignment of {string.Join(',', missingParameters)} in the call of {endPoint.Id} at resource path {endPoint.ResourcePath}");
+
+ bool providesJsonResponses = endPoint.Response?.Representations?
+ .Any(repr => repr.MediaType == WadlRepresentation.MediaTypes.Json) ?? false;
+
+ if (providesJsonResponses)
+ throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} does not respond in JSON");
+
+ // execute
+ KeyValuePair>[] responseTaks = uris
+ .Select(uri => new KeyValuePair>(uri.Key, _httpClient.GetAsJsonAsync(uri.Value)))
+ .ToArray();
+
+ await Task.WhenAll(responseTaks.Select(x => x.Value));
+
+ Dictionary result = responseTaks
+ .Join(
+ inner: issueKeysParsed,
+ outerKeySelector: outer => outer.Key,
+ innerKeySelector: inner => inner,
+ resultSelector: (outer, inner) => new KeyValuePair(
+ outer.Key.ToString(),
+ outer.Value.Result
+ .Select((def, ix) => new WorkLogType(
+ Key: def.Id.ToString(),
+ Value: def.Name,
+ Sequence: ix
+ ))
+ .ToArray()
+ ),
+ comparer: projectComparer
+ )
+ .ToDictionary(
+ keySelector: x => x.Key,
+ elementSelector: x => x.Value
+ );
+
+ return result;
+ }
+
+ public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, string issueKey)
+ {
+ return await _vanillaJiraApi.GetIssueWorkLogs(from, to, issueKey);
+ }
+
+ public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, IEnumerable? issueKeys)
+ {
+ return await _vanillaJiraApi.GetIssueWorkLogs(from, to, issueKeys);
+ }
+
+ public async Task AddWorkLog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+ {
+ ComposedWadlMethodDefinition endPoint = CreateWorkLogMethodDefinition;
+
+ HashSet missingParameters = endPoint.Parameters
+ .Where(par => !string.IsNullOrEmpty(par.Name))
+ .Select(par => par.Name ?? string.Empty)
+ .ToHashSet();
+
+ // define
+ string uri = endPoint.ResourcePath
+ .Replace("issueKey", issueKey);
+ missingParameters.Remove("issueKey");
+
+ Dictionary args = new ()
+ {
+ ["date"] = day.ToString(_flavourOptions.DateFormat, CultureInfo.InvariantCulture),
+ ["logWorkOption"] = ICTimeAddWorklogByIssueKey.LogWorkOption.Summary.ToString().ToLowerFirstChar(),
+ ["comment"] = comment ?? string.Empty,
+ ["timeLogged"] = TimeSpan.FromSeconds(timeSpentSeconds).ToString(_flavourOptions.TimeSpanFormat),
+ ["activity"] = _flavourOptions.ActivityMap == null || !_flavourOptions.ActivityMap.Any() || string.IsNullOrEmpty(activity)
+ ? activity ?? string.Empty
+ : _flavourOptions.ActivityMap[activity],
+ ["user"] = this.UserName,
+ ["charged"] = true.ToString().ToLowerInvariant()
+ };
+ missingParameters.RemoveWhere(elm => args.ContainsKey(elm));
+
+ // check
+ HttpMethod expectedMethod = HttpMethod.Post;
+ bool isCorrectHttpMethod = endPoint.HttpMethod == expectedMethod;
+ if (!isCorrectHttpMethod)
+ throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} executes via ${endPoint.HttpMethod} method (${expectedMethod.ToString().ToUpperInvariant()} expected)");
+
+ if (missingParameters.Any())
+ throw new ArgumentNullException($"Missing assignment of {string.Join(',', missingParameters)} in the call of {endPoint.Id} at resource path {endPoint.ResourcePath}");
+
+ bool providesJsonResponses = endPoint.Response?.Representations?
+ .Any(repr => repr.MediaType == WadlRepresentation.MediaTypes.Json) ?? false;
+
+ if (providesJsonResponses)
+ throw new InvalidOperationException($"Method {endPoint.Id} at resource path {endPoint.ResourcePath} does not respond in JSON");
+
+ // execute
+ HttpContent content = new FormUrlEncodedContent(args);
+ HttpResponseMessage response = await _httpClient.PostAsync(uri, content);
+
+ if (providesJsonResponses)
+ await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
+ }
+
+ public async Task AddWorkLogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
+ {
+ DateOnly[] daysInPeriod = Enumerable.Range(0, dayFrom.NumberOfDaysTo(dayTo))
+ .Select(i => dayFrom.AddDays(i))
+ .Where(day => includeNonWorkingDays || day.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
+ .ToArray();
+
+ if (!daysInPeriod.Any())
+ return;
+
+ int timeSpentSecondsPerSingleDay = timeSpentSeconds / daysInPeriod.Length;
+
+ Task[] addWorklogTasks = daysInPeriod
+ .Select(day => AddWorkLog(issueKey, day, timeSpentSecondsPerSingleDay, activity, comment))
+ .ToArray();
+
+ await Task.WhenAll(addWorklogTasks);
+ }
+
+ public async Task DeleteWorkLog(long issueId, long worklogId, bool notifyUsers = false)
+ {
+ await _vanillaJiraApi.DeleteWorkLog(issueId, worklogId, notifyUsers);
+ }
+
+ public async Task UpdateWorkLog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+ {
+ await _vanillaJiraApi.UpdateWorkLog(issueKey, worklogId, day, timeSpentSeconds, activity, comment);
+ }
+
+ private async Task GetWADL()
+ {
+ Uri uri = new Uri($"{_flavourOptions.PluginBaseUri}/application.wadl", UriKind.Relative);
+ using Stream response = await _httpClient.GetStreamAsync(uri);
+ if (response == null || response.Length <= 0)
+ throw new HttpRequestException($"Empty content received from ${uri}");
+
+ XmlSerializer serializer = new XmlSerializer(typeof(WadlApplication));
+ object resultObj = serializer.Deserialize(response) ?? throw new InvalidDataException($"Empty/null content deserialization result");
+
+ WadlApplication result = (WadlApplication)resultObj;
+ return result;
+ }
+}
diff --git a/jwl.jira/Flavours/JiraWithTempoPluginApi.cs b/jwl.jira/Flavours/JiraWithTempoPluginApi.cs
index be479bb..598513e 100644
--- a/jwl.jira/Flavours/JiraWithTempoPluginApi.cs
+++ b/jwl.jira/Flavours/JiraWithTempoPluginApi.cs
@@ -1,187 +1,187 @@
-namespace jwl.jira;
-using System.Net.Http.Json;
-using jwl.infra;
-using jwl.jira.api.rest.common;
-using jwl.jira.Flavours;
-using NoP77svk.Linq;
-
-// https://www.tempo.io/server-api-documentation/timesheets
-public class JiraWithTempoPluginApi
- : IJiraClient
-{
- private const string WorklogTypeAttributeKey = @"_WorklogType_";
-
- private readonly HttpClient _httpClient;
- private readonly FlavourTempoTimesheetsOptions _flavourOptions;
- private readonly VanillaJiraClient _vanillaJiraApi;
-
- public string UserName { get; }
- public api.rest.common.JiraUserInfo UserInfo => _vanillaJiraApi.UserInfo;
-
- public JiraWithTempoPluginApi(HttpClient httpClient, string userName, FlavourTempoTimesheetsOptions? flavourOptions)
- {
- _httpClient = httpClient;
- UserName = userName;
- _flavourOptions = flavourOptions ?? new FlavourTempoTimesheetsOptions();
- _vanillaJiraApi = new VanillaJiraClient(httpClient, userName, null);
- }
-
- public async Task GetWorklogAttributeDefinitions()
- {
- return await _httpClient.GetAsJsonAsync($"{_flavourOptions.PluginCoreUri}/work-attribute");
- }
-
- public async Task GetAvailableActivities(string issueKey)
- {
- api.rest.response.TempoWorklogAttributeDefinition[] attrEnumDefs = await GetWorklogAttributeDefinitions();
-
- WorkLogType[] result = attrEnumDefs
- .Where(attrDef => attrDef.Key?.Equals(WorklogTypeAttributeKey) ?? false)
- .Where(attrDef => attrDef.Type != null
- && attrDef.Type?.Value == TempoWorklogAttributeTypeIdentifier.StaticList
- )
- .SelectMany(attrDef => attrDef.StaticListValues)
- .Where(staticListItem => !string.IsNullOrEmpty(staticListItem.Name) && !string.IsNullOrEmpty(staticListItem.Value))
- .Select(staticListItem => new WorkLogType(
- Key: staticListItem.Name ?? string.Empty,
- Value: staticListItem.Value ?? string.Empty,
- Sequence: staticListItem.Sequence ?? -1
- ))
- .ToArray();
-
- return result;
- }
-
- public async Task> GetAvailableActivities(IEnumerable issueKeys)
- {
- WorkLogType[] activities = await GetAvailableActivities(string.Empty);
-
- Dictionary result = issueKeys
- .Select(issueKey => new ValueTuple(issueKey, activities))
- .ToDictionary(
- keySelector: x => x.Item1,
- elementSelector: x => x.Item2
- );
-
- return result;
- }
-
- public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, string issueKey)
- {
- return await GetIssueWorkLogs(from, to, new string[] { issueKey });
- }
-
- public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, IEnumerable? issueKeys)
- {
- string userKey = UserInfo.Key ?? throw new ArgumentNullException($"{nameof(UserInfo)}.{nameof(UserInfo.Key)}");
-
- var request = new api.rest.request.TempoFindWorklogs(from, to)
- {
- IssueKey = issueKeys?.ToArray(),
- UserKey = new string[] { userKey }
- };
- var response = await _httpClient.PostAsJsonAsync($"{_flavourOptions.PluginBaseUri}/worklogs/search", request);
- var tempoWorkLogs = await HttpClientJsonExt.DeserializeJsonStreamAsync(await response.Content.ReadAsStreamAsync());
-
- var result = tempoWorkLogs
- .Select(wl => new WorkLog(
- Id: wl.Id ?? -1,
- IssueId: wl.IssueId ?? -1,
- AuthorName: wl.WorkerKey == userKey ? UserName : null,
- AuthorKey: wl.WorkerKey,
- Created: wl.Created?.Value ?? DateTime.MinValue,
- Started: wl.Started?.Value ?? DateTime.MinValue,
- TimeSpentSeconds: wl.TimeSpentSeconds ?? -1,
- Activity: wl.Attributes?[WorklogTypeAttributeKey].Value,
- Comment: wl.Comment ?? string.Empty
- ))
- .ToArray();
-
- return result;
- }
-
- public async Task AddWorkLog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
- {
- await AddWorkLogPeriod(issueKey, day, day, timeSpentSeconds, activity, comment);
- }
-
- public async Task AddWorkLogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? tempoWorklogType, string? comment, bool includeNonWorkingDays = false)
- {
- string userKey = UserInfo.Key ?? throw new ArgumentNullException($"{nameof(UserInfo)}.{nameof(UserInfo.Key)}");
-
- var request = new api.rest.request.TempoAddWorklogByIssueKey()
- {
- IssueKey = issueKey,
- Worker = userKey,
- Started = new api.rest.common.TempoDate(dayFrom),
- EndDate = new api.rest.common.TempoDate(dayTo),
- TimeSpentSeconds = timeSpentSeconds,
- BillableSeconds = timeSpentSeconds,
- IncludeNonWorkingDays = includeNonWorkingDays,
- Comment = comment,
- Attributes = new Dictionary()
- {
- [WorklogTypeAttributeKey] = new api.rest.common.TempoWorklogAttribute()
- {
- WorkAttributeId = 1,
- Key = WorklogTypeAttributeKey,
- Name = @"Worklog Type",
- Type = api.rest.common.TempoWorklogAttributeTypeIdentifier.StaticList,
- Value = tempoWorklogType
- }
- }
- };
-
- HttpResponseMessage response = await _httpClient.PostAsJsonAsync($"{_flavourOptions.PluginBaseUri}/worklogs", request);
- await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
- }
-
- public async Task DeleteWorkLog(long issueId, long worklogId, bool notifyUsers = false)
- {
- UriBuilder uriBuilder = new UriBuilder()
- {
- Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/worklogs")
- .Add(worklogId.ToString())
- };
-
- HttpResponseMessage response = await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
- await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
- }
-
- public async Task UpdateWorkLog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
- {
- await UpdateWorklogPeriod(issueKey, worklogId, day, day, timeSpentSeconds, comment, activity);
- }
-
- private async Task UpdateWorklogPeriod(string issueKey, long worklogId, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? comment, string? activity, bool includeNonWorkingDays = false)
- {
- UriBuilder uriBuilder = new UriBuilder()
- {
- Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/worklogs")
- .Add(worklogId.ToString())
- };
- var request = new api.rest.request.TempoUpdateWorklog()
- {
- Started = new api.rest.common.TempoDate(dayFrom),
- EndDate = new api.rest.common.TempoDate(dayTo),
- TimeSpentSeconds = timeSpentSeconds,
- BillableSeconds = timeSpentSeconds,
- IncludeNonWorkingDays = includeNonWorkingDays,
- Comment = comment,
- Attributes = new Dictionary()
- {
- [WorklogTypeAttributeKey] = new api.rest.common.TempoWorklogAttribute()
- {
- WorkAttributeId = 1,
- Key = WorklogTypeAttributeKey,
- Name = @"Worklog Type",
- Type = api.rest.common.TempoWorklogAttributeTypeIdentifier.StaticList,
- Value = activity
- }
- }
- };
-
- HttpResponseMessage response = await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
- await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
- }
-}
+namespace jwl.jira;
+using System.Net.Http.Json;
+using jwl.infra;
+using jwl.jira.api.rest.common;
+using jwl.jira.Flavours;
+using NoP77svk.Linq;
+
+// https://www.tempo.io/server-api-documentation/timesheets
+public class JiraWithTempoPluginApi
+ : IJiraClient
+{
+ private const string WorklogTypeAttributeKey = @"_WorklogType_";
+
+ private readonly HttpClient _httpClient;
+ private readonly FlavourTempoTimesheetsOptions _flavourOptions;
+ private readonly VanillaJiraClient _vanillaJiraApi;
+
+ public string UserName { get; }
+ public api.rest.common.JiraUserInfo UserInfo => _vanillaJiraApi.UserInfo;
+
+ public JiraWithTempoPluginApi(HttpClient httpClient, string userName, FlavourTempoTimesheetsOptions? flavourOptions)
+ {
+ _httpClient = httpClient;
+ UserName = userName;
+ _flavourOptions = flavourOptions ?? new FlavourTempoTimesheetsOptions();
+ _vanillaJiraApi = new VanillaJiraClient(httpClient, userName, null);
+ }
+
+ public async Task GetWorklogAttributeDefinitions()
+ {
+ return await _httpClient.GetAsJsonAsync($"{_flavourOptions.PluginCoreUri}/work-attribute");
+ }
+
+ public async Task GetAvailableActivities(string issueKey)
+ {
+ api.rest.response.TempoWorklogAttributeDefinition[] attrEnumDefs = await GetWorklogAttributeDefinitions();
+
+ WorkLogType[] result = attrEnumDefs
+ .Where(attrDef => attrDef.Key?.Equals(WorklogTypeAttributeKey) ?? false)
+ .Where(attrDef => attrDef.Type != null
+ && attrDef.Type?.Value == TempoWorklogAttributeTypeIdentifier.StaticList
+ )
+ .SelectMany(attrDef => attrDef.StaticListValues)
+ .Where(staticListItem => !string.IsNullOrEmpty(staticListItem.Name) && !string.IsNullOrEmpty(staticListItem.Value))
+ .Select(staticListItem => new WorkLogType(
+ Key: staticListItem.Name ?? string.Empty,
+ Value: staticListItem.Value ?? string.Empty,
+ Sequence: staticListItem.Sequence ?? -1
+ ))
+ .ToArray();
+
+ return result;
+ }
+
+ public async Task> GetAvailableActivities(IEnumerable issueKeys)
+ {
+ WorkLogType[] activities = await GetAvailableActivities(string.Empty);
+
+ Dictionary result = issueKeys
+ .Select(issueKey => new ValueTuple(issueKey, activities))
+ .ToDictionary(
+ keySelector: x => x.Item1,
+ elementSelector: x => x.Item2
+ );
+
+ return result;
+ }
+
+ public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, string issueKey)
+ {
+ return await GetIssueWorkLogs(from, to, new string[] { issueKey });
+ }
+
+ public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, IEnumerable? issueKeys)
+ {
+ string userKey = UserInfo.Key ?? throw new ArgumentNullException($"{nameof(UserInfo)}.{nameof(UserInfo.Key)}");
+
+ var request = new api.rest.request.TempoFindWorklogs(from, to)
+ {
+ IssueKey = issueKeys?.ToArray(),
+ UserKey = new string[] { userKey }
+ };
+ var response = await _httpClient.PostAsJsonAsync($"{_flavourOptions.PluginBaseUri}/worklogs/search", request);
+ var tempoWorkLogs = await HttpClientJsonExt.DeserializeJsonStreamAsync(await response.Content.ReadAsStreamAsync());
+
+ var result = tempoWorkLogs
+ .Select(wl => new WorkLog(
+ Id: wl.Id ?? -1,
+ IssueId: wl.IssueId ?? -1,
+ AuthorName: wl.WorkerKey == userKey ? UserName : null,
+ AuthorKey: wl.WorkerKey,
+ Created: wl.Created?.Value ?? DateTime.MinValue,
+ Started: wl.Started?.Value ?? DateTime.MinValue,
+ TimeSpentSeconds: wl.TimeSpentSeconds ?? -1,
+ Activity: wl.Attributes?[WorklogTypeAttributeKey].Value,
+ Comment: wl.Comment ?? string.Empty
+ ))
+ .ToArray();
+
+ return result;
+ }
+
+ public async Task AddWorkLog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+ {
+ await AddWorkLogPeriod(issueKey, day, day, timeSpentSeconds, activity, comment);
+ }
+
+ public async Task AddWorkLogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? tempoWorklogType, string? comment, bool includeNonWorkingDays = false)
+ {
+ string userKey = UserInfo.Key ?? throw new ArgumentNullException($"{nameof(UserInfo)}.{nameof(UserInfo.Key)}");
+
+ var request = new api.rest.request.TempoAddWorklogByIssueKey()
+ {
+ IssueKey = issueKey,
+ Worker = userKey,
+ Started = new api.rest.common.TempoDate(dayFrom),
+ EndDate = new api.rest.common.TempoDate(dayTo),
+ TimeSpentSeconds = timeSpentSeconds,
+ BillableSeconds = timeSpentSeconds,
+ IncludeNonWorkingDays = includeNonWorkingDays,
+ Comment = comment,
+ Attributes = new Dictionary()
+ {
+ [WorklogTypeAttributeKey] = new api.rest.common.TempoWorklogAttribute()
+ {
+ WorkAttributeId = 1,
+ Key = WorklogTypeAttributeKey,
+ Name = @"Worklog Type",
+ Type = api.rest.common.TempoWorklogAttributeTypeIdentifier.StaticList,
+ Value = tempoWorklogType
+ }
+ }
+ };
+
+ HttpResponseMessage response = await _httpClient.PostAsJsonAsync($"{_flavourOptions.PluginBaseUri}/worklogs", request);
+ await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
+ }
+
+ public async Task DeleteWorkLog(long issueId, long worklogId, bool notifyUsers = false)
+ {
+ UriBuilder uriBuilder = new UriBuilder()
+ {
+ Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/worklogs")
+ .Add(worklogId.ToString())
+ };
+
+ HttpResponseMessage response = await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
+ await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
+ }
+
+ public async Task UpdateWorkLog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+ {
+ await UpdateWorklogPeriod(issueKey, worklogId, day, day, timeSpentSeconds, comment, activity);
+ }
+
+ private async Task UpdateWorklogPeriod(string issueKey, long worklogId, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? comment, string? activity, bool includeNonWorkingDays = false)
+ {
+ UriBuilder uriBuilder = new UriBuilder()
+ {
+ Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/worklogs")
+ .Add(worklogId.ToString())
+ };
+ var request = new api.rest.request.TempoUpdateWorklog()
+ {
+ Started = new api.rest.common.TempoDate(dayFrom),
+ EndDate = new api.rest.common.TempoDate(dayTo),
+ TimeSpentSeconds = timeSpentSeconds,
+ BillableSeconds = timeSpentSeconds,
+ IncludeNonWorkingDays = includeNonWorkingDays,
+ Comment = comment,
+ Attributes = new Dictionary()
+ {
+ [WorklogTypeAttributeKey] = new api.rest.common.TempoWorklogAttribute()
+ {
+ WorkAttributeId = 1,
+ Key = WorklogTypeAttributeKey,
+ Name = @"Worklog Type",
+ Type = api.rest.common.TempoWorklogAttributeTypeIdentifier.StaticList,
+ Value = activity
+ }
+ }
+ };
+
+ HttpResponseMessage response = await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+ await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
+ }
+}
diff --git a/jwl.jira/Flavours/ServerApiFactory.cs b/jwl.jira/Flavours/ServerApiFactory.cs
index 8b009f0..5baeb46 100644
--- a/jwl.jira/Flavours/ServerApiFactory.cs
+++ b/jwl.jira/Flavours/ServerApiFactory.cs
@@ -1,38 +1,38 @@
-namespace jwl.jira.Flavours;
-
-public static class ServerApiFactory
-{
- public static IJiraClient CreateApi(HttpClient httpClient, string userName, JiraServerFlavour serverFlavour, IFlavourOptions? flavourOptions)
- {
- try
- {
- return serverFlavour switch
- {
- JiraServerFlavour.Vanilla => new VanillaJiraClient(httpClient, userName, (FlavourVanillaJiraOptions?)flavourOptions),
- JiraServerFlavour.TempoTimeSheets => new JiraWithTempoPluginApi(httpClient, userName, (FlavourTempoTimesheetsOptions?)flavourOptions),
- JiraServerFlavour.ICTime => new JiraWithICTimePluginApi(httpClient, userName, (FlavourICTimeOptions?)flavourOptions),
- _ => throw new NotImplementedException($"Jira server flavour {nameof(serverFlavour)} not yet implemented")
- };
- }
- catch (InvalidCastException ex)
- {
- throw new ArgumentOutOfRangeException($"Don't know how to instantiate client API for flavour {serverFlavour}", ex);
- }
- }
-
- public static JiraServerFlavour? DecodeServerClass(string? serverFlavour)
- {
- JiraServerFlavour? result;
-
- if (string.IsNullOrEmpty(serverFlavour))
- result = null;
- else if (int.TryParse(serverFlavour, out int serverFlavourIntId))
- result = (JiraServerFlavour)serverFlavourIntId;
- else if (Enum.TryParse(serverFlavour, true, out JiraServerFlavour serverFlavourEnumId))
- result = serverFlavourEnumId;
- else
- throw new ArgumentOutOfRangeException(nameof(serverFlavour), serverFlavour, "Invalid server flavour configured");
-
- return result;
- }
-}
+namespace jwl.jira.Flavours;
+
+public static class ServerApiFactory
+{
+ public static IJiraClient CreateApi(HttpClient httpClient, string userName, JiraServerFlavour serverFlavour, IFlavourOptions? flavourOptions)
+ {
+ try
+ {
+ return serverFlavour switch
+ {
+ JiraServerFlavour.Vanilla => new VanillaJiraClient(httpClient, userName, (FlavourVanillaJiraOptions?)flavourOptions),
+ JiraServerFlavour.TempoTimeSheets => new JiraWithTempoPluginApi(httpClient, userName, (FlavourTempoTimesheetsOptions?)flavourOptions),
+ JiraServerFlavour.ICTime => new JiraWithICTimePluginApi(httpClient, userName, (FlavourICTimeOptions?)flavourOptions),
+ _ => throw new NotImplementedException($"Jira server flavour {nameof(serverFlavour)} not yet implemented")
+ };
+ }
+ catch (InvalidCastException ex)
+ {
+ throw new ArgumentOutOfRangeException($"Don't know how to instantiate client API for flavour {serverFlavour}", ex);
+ }
+ }
+
+ public static JiraServerFlavour? DecodeServerClass(string? serverFlavour)
+ {
+ JiraServerFlavour? result;
+
+ if (string.IsNullOrEmpty(serverFlavour))
+ result = null;
+ else if (int.TryParse(serverFlavour, out int serverFlavourIntId))
+ result = (JiraServerFlavour)serverFlavourIntId;
+ else if (Enum.TryParse(serverFlavour, true, out JiraServerFlavour serverFlavourEnumId))
+ result = serverFlavourEnumId;
+ else
+ throw new ArgumentOutOfRangeException(nameof(serverFlavour), serverFlavour, "Invalid server flavour configured");
+
+ return result;
+ }
+}
diff --git a/jwl.jira/Flavours/VanillaJiraClient.cs b/jwl.jira/Flavours/VanillaJiraClient.cs
index 344fe56..124c20e 100644
--- a/jwl.jira/Flavours/VanillaJiraClient.cs
+++ b/jwl.jira/Flavours/VanillaJiraClient.cs
@@ -1,209 +1,209 @@
-namespace jwl.jira;
-
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Net.Http.Json;
-using System.Text;
-using jwl.infra;
-using jwl.jira.api.rest.response;
-using jwl.jira.Flavours;
-
-public class VanillaJiraClient
- : IJiraClient
-{
- public string UserName { get; }
- public api.rest.common.JiraUserInfo UserInfo => _lazyUserInfo.Value;
-
- private readonly HttpClient _httpClient;
- private readonly Lazy _lazyUserInfo;
- private readonly FlavourVanillaJiraOptions _flavourOptions;
-
- public VanillaJiraClient(HttpClient httpClient, string userName, FlavourVanillaJiraOptions? flavourOptions)
- {
- _httpClient = httpClient;
- UserName = userName;
- _lazyUserInfo = new Lazy(() => GetUserInfo().Result);
- _flavourOptions = flavourOptions ?? new FlavourVanillaJiraOptions();
- }
-
- public static async Task CheckHttpResponseForErrorMessages(HttpResponseMessage responseMessage)
- {
- using Stream responseContentStream = await responseMessage.Content.ReadAsStreamAsync();
-
- if (responseContentStream.Length > 0)
- {
- JiraRestResponse responseContent = await HttpClientJsonExt.DeserializeJsonStreamAsync(responseContentStream);
- if (responseContent?.ErrorMessages is not null && responseContent.ErrorMessages.Any())
- throw new InvalidOperationException(string.Join(Environment.NewLine, responseContent.ErrorMessages));
- }
- }
-
- #pragma warning disable CS1998
- public async Task GetAvailableActivities(string issueKey)
- {
- return Array.Empty();
- }
- #pragma warning restore
-
- public async Task> GetAvailableActivities(IEnumerable issueKeys)
- {
- WorkLogType[] activities = await GetAvailableActivities(string.Empty);
-
- Dictionary result = issueKeys
- .Select(issueKey => new ValueTuple(issueKey, activities))
- .ToDictionary(
- keySelector: x => x.Item1,
- elementSelector: x => x.Item2
- );
-
- return result;
- }
-
- public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, string issueKey)
- {
- UriBuilder uriBuilder = new UriBuilder()
- {
- Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
- .Add(issueKey)
- .Add(@"worklog")
- };
-
- var response = await _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery);
-
- (DateTime minDt, DateTime supDt) = DateOnlyUtils.DateOnlyRangeToDateTimeRange(from, to);
-
- var result = response.Worklogs
- .Where(worklog => worklog.Author.Name == UserName)
- .Where(worklog => worklog.Started.Value >= minDt && worklog.Started.Value < supDt)
- .Select(wl => new WorkLog(
- Id: wl.Id.Value,
- IssueId: wl.IssueId.Value,
- AuthorName: wl.Author.Name,
- AuthorKey: wl.Author.Key,
- Created: wl.Created.Value,
- Started: wl.Started.Value,
- TimeSpentSeconds: wl.TimeSpentSeconds,
- Activity: null,
- Comment: wl.Comment
- ))
- .ToArray();
-
- return result;
- }
-
- public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, IEnumerable? issueKeys)
- {
- if (issueKeys is null)
- return Array.Empty();
-
- Task[] responseTasks = issueKeys
- .Distinct()
- .Select(issueKey => GetIssueWorkLogs(from, to, issueKey))
- .ToArray();
-
- await Task.WhenAll(responseTasks);
-
- var result = responseTasks
- .SelectMany(task => task.Result)
- .ToArray();
-
- return result;
- }
-
- public async Task AddWorkLog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
- {
- UriBuilder uriBuilder = new UriBuilder()
- {
- Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
- .Add(issueKey)
- .Add(@"worklog")
- };
-
- StringBuilder commentBuilder = new StringBuilder();
- if (activity != null)
- commentBuilder.Append($"({activity}){Environment.NewLine}");
- commentBuilder.Append(comment);
-
- var request = new api.rest.request.JiraAddWorklogByIssueKey(
- Started: day
- .ToDateTime(TimeOnly.MinValue)
- .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
- .Replace(":", string.Empty)
- .Replace(';', ':'),
- TimeSpentSeconds: timeSpentSeconds,
- Comment: commentBuilder.ToString()
- );
-
- HttpResponseMessage response = await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
- await CheckHttpResponseForErrorMessages(response);
- }
-
- public async Task AddWorkLogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
- {
- DateOnly[] daysInPeriod = Enumerable.Range(0, dayFrom.NumberOfDaysTo(dayTo))
- .Select(i => dayFrom.AddDays(i))
- .Where(day => includeNonWorkingDays || day.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
- .ToArray();
-
- if (!daysInPeriod.Any())
- return;
-
- int timeSpentSecondsPerSingleDay = timeSpentSeconds / daysInPeriod.Length;
-
- Task[] addWorklogTasks = daysInPeriod
- .Select(day => AddWorkLog(issueKey, day, timeSpentSecondsPerSingleDay, activity, comment))
- .ToArray();
-
- await Task.WhenAll(addWorklogTasks);
- }
-
- public async Task DeleteWorkLog(long issueId, long worklogId, bool notifyUsers = false)
- {
- UriBuilder uriBuilder = new UriBuilder()
- {
- Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
- .Add(issueId.ToString())
- .Add(@"worklog")
- .Add(worklogId.ToString()),
- Query = new UriQueryBuilder()
- .Add(@"notifyUsers", notifyUsers.ToString().ToLower())
- };
-
- HttpResponseMessage response = await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
- await CheckHttpResponseForErrorMessages(response);
- }
-
- public async Task UpdateWorkLog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
- {
- UriBuilder uriBuilder = new UriBuilder()
- {
- Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
- .Add(issueKey)
- .Add(@"worklog")
- .Add(worklogId.ToString())
- };
- var request = new api.rest.request.JiraAddWorklogByIssueKey(
- Started: day
- .ToDateTime(TimeOnly.MinValue)
- .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
- .Replace(":", string.Empty)
- .Replace(';', ':'),
- TimeSpentSeconds: timeSpentSeconds,
- Comment: comment
- );
-
- HttpResponseMessage response = await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
- await CheckHttpResponseForErrorMessages(response);
- }
-
- private async Task GetUserInfo()
- {
- UriBuilder uriBuilder = new UriBuilder()
- {
- Path = $"{_flavourOptions.PluginBaseUri}/user",
- Query = new UriQueryBuilder()
- .Add(@"username", UserName)
- };
- return await _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery);
- }
-}
+namespace jwl.jira;
+
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text;
+using jwl.infra;
+using jwl.jira.api.rest.response;
+using jwl.jira.Flavours;
+
+public class VanillaJiraClient
+ : IJiraClient
+{
+ public string UserName { get; }
+ public api.rest.common.JiraUserInfo UserInfo => _lazyUserInfo.Value;
+
+ private readonly HttpClient _httpClient;
+ private readonly Lazy _lazyUserInfo;
+ private readonly FlavourVanillaJiraOptions _flavourOptions;
+
+ public VanillaJiraClient(HttpClient httpClient, string userName, FlavourVanillaJiraOptions? flavourOptions)
+ {
+ _httpClient = httpClient;
+ UserName = userName;
+ _lazyUserInfo = new Lazy(() => GetUserInfo().Result);
+ _flavourOptions = flavourOptions ?? new FlavourVanillaJiraOptions();
+ }
+
+ public static async Task CheckHttpResponseForErrorMessages(HttpResponseMessage responseMessage)
+ {
+ using Stream responseContentStream = await responseMessage.Content.ReadAsStreamAsync();
+
+ if (responseContentStream.Length > 0)
+ {
+ JiraRestResponse responseContent = await HttpClientJsonExt.DeserializeJsonStreamAsync(responseContentStream);
+ if (responseContent?.ErrorMessages is not null && responseContent.ErrorMessages.Any())
+ throw new InvalidOperationException(string.Join(Environment.NewLine, responseContent.ErrorMessages));
+ }
+ }
+
+ #pragma warning disable CS1998
+ public async Task GetAvailableActivities(string issueKey)
+ {
+ return Array.Empty();
+ }
+ #pragma warning restore
+
+ public async Task> GetAvailableActivities(IEnumerable issueKeys)
+ {
+ WorkLogType[] activities = await GetAvailableActivities(string.Empty);
+
+ Dictionary result = issueKeys
+ .Select(issueKey => new ValueTuple(issueKey, activities))
+ .ToDictionary(
+ keySelector: x => x.Item1,
+ elementSelector: x => x.Item2
+ );
+
+ return result;
+ }
+
+ public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, string issueKey)
+ {
+ UriBuilder uriBuilder = new UriBuilder()
+ {
+ Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
+ .Add(issueKey)
+ .Add(@"worklog")
+ };
+
+ var response = await _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery);
+
+ (DateTime minDt, DateTime supDt) = DateOnlyUtils.DateOnlyRangeToDateTimeRange(from, to);
+
+ var result = response.Worklogs
+ .Where(worklog => worklog.Author.Name == UserName)
+ .Where(worklog => worklog.Started.Value >= minDt && worklog.Started.Value < supDt)
+ .Select(wl => new WorkLog(
+ Id: wl.Id.Value,
+ IssueId: wl.IssueId.Value,
+ AuthorName: wl.Author.Name,
+ AuthorKey: wl.Author.Key,
+ Created: wl.Created.Value,
+ Started: wl.Started.Value,
+ TimeSpentSeconds: wl.TimeSpentSeconds,
+ Activity: null,
+ Comment: wl.Comment
+ ))
+ .ToArray();
+
+ return result;
+ }
+
+ public async Task GetIssueWorkLogs(DateOnly from, DateOnly to, IEnumerable? issueKeys)
+ {
+ if (issueKeys is null)
+ return Array.Empty();
+
+ Task[] responseTasks = issueKeys
+ .Distinct()
+ .Select(issueKey => GetIssueWorkLogs(from, to, issueKey))
+ .ToArray();
+
+ await Task.WhenAll(responseTasks);
+
+ var result = responseTasks
+ .SelectMany(task => task.Result)
+ .ToArray();
+
+ return result;
+ }
+
+ public async Task AddWorkLog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+ {
+ UriBuilder uriBuilder = new UriBuilder()
+ {
+ Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
+ .Add(issueKey)
+ .Add(@"worklog")
+ };
+
+ StringBuilder commentBuilder = new StringBuilder();
+ if (activity != null)
+ commentBuilder.Append($"({activity}){Environment.NewLine}");
+ commentBuilder.Append(comment);
+
+ var request = new api.rest.request.JiraAddWorklogByIssueKey(
+ Started: day
+ .ToDateTime(TimeOnly.MinValue)
+ .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
+ .Replace(":", string.Empty)
+ .Replace(';', ':'),
+ TimeSpentSeconds: timeSpentSeconds,
+ Comment: commentBuilder.ToString()
+ );
+
+ HttpResponseMessage response = await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+ await CheckHttpResponseForErrorMessages(response);
+ }
+
+ public async Task AddWorkLogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
+ {
+ DateOnly[] daysInPeriod = Enumerable.Range(0, dayFrom.NumberOfDaysTo(dayTo))
+ .Select(i => dayFrom.AddDays(i))
+ .Where(day => includeNonWorkingDays || day.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
+ .ToArray();
+
+ if (!daysInPeriod.Any())
+ return;
+
+ int timeSpentSecondsPerSingleDay = timeSpentSeconds / daysInPeriod.Length;
+
+ Task[] addWorklogTasks = daysInPeriod
+ .Select(day => AddWorkLog(issueKey, day, timeSpentSecondsPerSingleDay, activity, comment))
+ .ToArray();
+
+ await Task.WhenAll(addWorklogTasks);
+ }
+
+ public async Task DeleteWorkLog(long issueId, long worklogId, bool notifyUsers = false)
+ {
+ UriBuilder uriBuilder = new UriBuilder()
+ {
+ Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
+ .Add(issueId.ToString())
+ .Add(@"worklog")
+ .Add(worklogId.ToString()),
+ Query = new UriQueryBuilder()
+ .Add(@"notifyUsers", notifyUsers.ToString().ToLower())
+ };
+
+ HttpResponseMessage response = await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
+ await CheckHttpResponseForErrorMessages(response);
+ }
+
+ public async Task UpdateWorkLog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+ {
+ UriBuilder uriBuilder = new UriBuilder()
+ {
+ Path = new UriPathBuilder($"{_flavourOptions.PluginBaseUri}/issue")
+ .Add(issueKey)
+ .Add(@"worklog")
+ .Add(worklogId.ToString())
+ };
+ var request = new api.rest.request.JiraAddWorklogByIssueKey(
+ Started: day
+ .ToDateTime(TimeOnly.MinValue)
+ .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
+ .Replace(":", string.Empty)
+ .Replace(';', ':'),
+ TimeSpentSeconds: timeSpentSeconds,
+ Comment: comment
+ );
+
+ HttpResponseMessage response = await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+ await CheckHttpResponseForErrorMessages(response);
+ }
+
+ private async Task GetUserInfo()
+ {
+ UriBuilder uriBuilder = new UriBuilder()
+ {
+ Path = $"{_flavourOptions.PluginBaseUri}/user",
+ Query = new UriQueryBuilder()
+ .Add(@"username", UserName)
+ };
+ return await _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery);
+ }
+}
diff --git a/jwl.jira/IFlavourOptions.cs b/jwl.jira/IFlavourOptions.cs
index de51b2d..4eeea94 100644
--- a/jwl.jira/IFlavourOptions.cs
+++ b/jwl.jira/IFlavourOptions.cs
@@ -1,6 +1,6 @@
-namespace jwl.jira;
-
-public interface IFlavourOptions
-{
- string PluginBaseUri { get; init; }
-}
+namespace jwl.jira;
+
+public interface IFlavourOptions
+{
+ string PluginBaseUri { get; init; }
+}
diff --git a/jwl.jira/ServerConfig.cs b/jwl.jira/ServerConfig.cs
index 6a2e945..b65d419 100644
--- a/jwl.jira/ServerConfig.cs
+++ b/jwl.jira/ServerConfig.cs
@@ -1,16 +1,16 @@
-namespace jwl.jira;
-using System.Text.Json.Serialization;
-using jwl.jira.Flavours;
-
-public class ServerConfig
-{
- public string? BaseUrl { get; init; }
- public string? Flavour { get; init; }
- public JiraServerFlavour FlavourId => ServerApiFactory.DecodeServerClass(Flavour) ?? JiraServerFlavour.Vanilla;
-
- [JsonIgnore]
- public IFlavourOptions? FlavourOptions { get; set; }
- public bool? UseProxy { get; init; }
- public int? MaxConnectionsPerServer { get; init; }
- public bool? SkipSslCertificateCheck { get; init; }
-}
+namespace jwl.jira;
+using System.Text.Json.Serialization;
+using jwl.jira.Flavours;
+
+public class ServerConfig
+{
+ public string? BaseUrl { get; init; }
+ public string? Flavour { get; init; }
+ public JiraServerFlavour FlavourId => ServerApiFactory.DecodeServerClass(Flavour) ?? JiraServerFlavour.Vanilla;
+
+ [JsonIgnore]
+ public IFlavourOptions? FlavourOptions { get; set; }
+ public bool? UseProxy { get; init; }
+ public int? MaxConnectionsPerServer { get; init; }
+ public bool? SkipSslCertificateCheck { get; init; }
+}
diff --git a/jwl.jira/WorkLog.cs b/jwl.jira/WorkLog.cs
index e2a8c3b..33e07dc 100644
--- a/jwl.jira/WorkLog.cs
+++ b/jwl.jira/WorkLog.cs
@@ -1,6 +1,6 @@
-#pragma warning disable SA1313
-namespace jwl.jira;
-
-public record WorkLog(long Id, long IssueId, string? AuthorName, string? AuthorKey, DateTime Created, DateTime Started, int TimeSpentSeconds, string? Activity, string Comment)
-{
-}
+#pragma warning disable SA1313
+namespace jwl.jira;
+
+public record WorkLog(long Id, long IssueId, string? AuthorName, string? AuthorKey, DateTime Created, DateTime Started, int TimeSpentSeconds, string? Activity, string Comment)
+{
+}
diff --git a/jwl.jira/WorkLogType.cs b/jwl.jira/WorkLogType.cs
index c101987..a37e990 100644
--- a/jwl.jira/WorkLogType.cs
+++ b/jwl.jira/WorkLogType.cs
@@ -1,6 +1,6 @@
-#pragma warning disable SA1313
-namespace jwl.jira;
-
-public record WorkLogType(string Key, string Value, int Sequence)
-{
-}
+#pragma warning disable SA1313
+namespace jwl.jira;
+
+public record WorkLogType(string Key, string Value, int Sequence)
+{
+}
diff --git a/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs b/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs
index 035be2c..795e10e 100644
--- a/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs
+++ b/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs
@@ -1,6 +1,6 @@
-#pragma warning disable SA1313
-namespace jwl.jira.api.rest.request;
-
-public record JiraAddWorklogByIssueKey(string Started, int TimeSpentSeconds, string? Comment)
-{
-}
+#pragma warning disable SA1313
+namespace jwl.jira.api.rest.request;
+
+public record JiraAddWorklogByIssueKey(string Started, int TimeSpentSeconds, string? Comment)
+{
+}
diff --git a/jwl.jira/api/rest/response/ICTimeActivityDefinition.cs b/jwl.jira/api/rest/response/ICTimeActivityDefinition.cs
index d72979e..dd037f5 100644
--- a/jwl.jira/api/rest/response/ICTimeActivityDefinition.cs
+++ b/jwl.jira/api/rest/response/ICTimeActivityDefinition.cs
@@ -1,14 +1,14 @@
-namespace jwl.jira.api.rest.response;
-
-// 2do!
-public class ICTimeActivityDefinition
-{
- public ICTimeActivityDefinition(int id, string name)
- {
- Id = id;
- Name = name;
- }
-
- public int Id { get; set; }
- public string Name { get; set; }
-}
+namespace jwl.jira.api.rest.response;
+
+// 2do!
+public class ICTimeActivityDefinition
+{
+ public ICTimeActivityDefinition(int id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+
+ public int Id { get; set; }
+ public string Name { get; set; }
+}
diff --git a/jwl.wadl/WadlParameter.cs b/jwl.wadl/WadlParameter.cs
index addef5a..eb276e4 100644
--- a/jwl.wadl/WadlParameter.cs
+++ b/jwl.wadl/WadlParameter.cs
@@ -1,30 +1,30 @@
-namespace jwl.wadl
-{
- using System.Linq;
- using System.Xml.Serialization;
-
- [XmlRoot("param", Namespace = WadlApplication.XmlNamespace)]
- public class WadlParameter
- {
- public const string QueryStyle = "query";
- public const string TemplateStyle = "template";
-
- public const string BooleanType = "xs:boolean";
- public const string DoubleType = "xs:double";
- public const string IntType = "xs:int";
- public const string LongType = "xs:long";
- public const string StringType = "xs:string";
-
- [XmlAttribute("name")]
- public string? Name { get; set; }
-
- [XmlAttribute("style")]
- public string? Style { get; set; }
-
- [XmlAttribute("type")]
- public string? Type { get; set; }
-
- [XmlAttribute("default")]
- public string? Default { get; set; }
- }
-}
+namespace jwl.wadl
+{
+ using System.Linq;
+ using System.Xml.Serialization;
+
+ [XmlRoot("param", Namespace = WadlApplication.XmlNamespace)]
+ public class WadlParameter
+ {
+ public const string QueryStyle = "query";
+ public const string TemplateStyle = "template";
+
+ public const string BooleanType = "xs:boolean";
+ public const string DoubleType = "xs:double";
+ public const string IntType = "xs:int";
+ public const string LongType = "xs:long";
+ public const string StringType = "xs:string";
+
+ [XmlAttribute("name")]
+ public string? Name { get; set; }
+
+ [XmlAttribute("style")]
+ public string? Style { get; set; }
+
+ [XmlAttribute("type")]
+ public string? Type { get; set; }
+
+ [XmlAttribute("default")]
+ public string? Default { get; set; }
+ }
+}