From abc4f505eda256374c30629891087eb64c798608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <16204822+nop77svk@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:52:33 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=9B=A0=20refactoring=20client=20APIs?= =?UTF-8?q?=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ➕ IJiraClient.GetIssueWorklogs() overload for a single issueKey * 🛠 refactor: IJiraClient.GetUserInfo() method removed in favour of (delayed) IJiraClient.UserInfo property * 🛠 fixes to the missing IJiraClient.GetUserInfo() method --- jwl.core/JwlCoreProcess.cs | 14 +- jwl.jira/IJiraClient.cs | 4 +- jwl.jira/JiraWithICTimePluginApi.cs | 12 +- jwl.jira/JiraWithTempoPluginApi.cs | 334 ++++++++++++++-------------- jwl.jira/VanillaJiraClient.cs | 70 +++--- 5 files changed, 221 insertions(+), 213 deletions(-) diff --git a/jwl.core/JwlCoreProcess.cs b/jwl.core/JwlCoreProcess.cs index 744f874..cc9cdb1 100644 --- a/jwl.core/JwlCoreProcess.cs +++ b/jwl.core/JwlCoreProcess.cs @@ -21,8 +21,6 @@ public class JwlCoreProcess : IDisposable private HttpClient _httpClient; private IJiraClient _jiraClient; - private jwl.jira.api.rest.common.JiraUserInfo? _userInfo; - public JwlCoreProcess(AppConfig config, ICoreProcessInteraction interaction) { _config = config; @@ -59,6 +57,7 @@ public JwlCoreProcess(AppConfig config, ICoreProcessInteraction interaction) */ } + #pragma warning disable CS1998 public async Task PreProcess() { Feedback?.OverallProcessStart(); @@ -76,11 +75,8 @@ public async Task PreProcess() throw new ArgumentNullException($"Jira credentials not supplied"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(@"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(jiraUserName + ":" + jiraUserPassword))); - - Feedback?.PreloadUserInfoStart(jiraUserName); - _userInfo = await _jiraClient.GetUserInfo(); - Feedback?.PreloadUserInfoEnd(); } + #pragma warning restore public async Task Process(IEnumerable inputFiles) { @@ -143,7 +139,7 @@ private async Task FillJiraWithWorklogs(InputWorkLog[] inputWorklogs, WorkLog[] { Feedback?.FillJiraWithWorklogsSetTarget(inputWorklogs.Length, worklogsForDeletion.Length); - if (_userInfo == null || _userInfo.Key == null) + if (_jiraClient.UserInfo?.Key is null) throw new ArgumentNullException(@"Unresolved Jira key for the logged-on user"); Task[] fillJiraWithWorklogsTasks = worklogsForDeletion @@ -228,9 +224,7 @@ private async Task RetrieveWorklogsForDeletion(InputWorkLog[] inputWo { WorkLog[] result; - if (_userInfo == null) - throw new ArgumentNullException(@"User info not preloaded from Jira server"); - if (string.IsNullOrEmpty(_userInfo.Key)) + if (string.IsNullOrEmpty(_jiraClient.UserInfo?.Key)) throw new ArgumentNullException(@"Empty user key preloaded from Jira server"); DateTime[] inputWorklogDays = inputWorklogs diff --git a/jwl.jira/IJiraClient.cs b/jwl.jira/IJiraClient.cs index 9fa6adb..526803e 100644 --- a/jwl.jira/IJiraClient.cs +++ b/jwl.jira/IJiraClient.cs @@ -7,10 +7,12 @@ public interface IJiraClient { - Task GetUserInfo(); + api.rest.common.JiraUserInfo UserInfo { get; } Task GetAvailableActivities(); + Task GetIssueWorklogs(DateOnly from, DateOnly to, string issueKey); + Task GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable? issueKeys); Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment); diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs index e55ccd1..dfd6c09 100644 --- a/jwl.jira/JiraWithICTimePluginApi.cs +++ b/jwl.jira/JiraWithICTimePluginApi.cs @@ -14,8 +14,10 @@ public class JiraWithICTimePluginApi : IJiraClient { public string UserName { get; } + public api.rest.common.JiraUserInfo UserInfo => _vanillaJiraApi.UserInfo; private readonly HttpClient _httpClient; + private readonly VanillaJiraClient _vanillaJiraApi; public JiraWithICTimePluginApi(HttpClient httpClient, string userName) { @@ -24,16 +26,14 @@ public JiraWithICTimePluginApi(HttpClient httpClient, string userName) _vanillaJiraApi = new VanillaJiraClient(httpClient, userName); } - private readonly VanillaJiraClient _vanillaJiraApi; - - public async Task GetUserInfo() + public async Task GetAvailableActivities() { - return await _vanillaJiraApi.GetUserInfo(); + return await _vanillaJiraApi.GetAvailableActivities(); } - public async Task GetAvailableActivities() + public async Task GetIssueWorklogs(DateOnly from, DateOnly to, string issueKey) { - return await _vanillaJiraApi.GetAvailableActivities(); + return await _vanillaJiraApi.GetIssueWorklogs(from, to, issueKey); } public async Task GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable? issueKeys) diff --git a/jwl.jira/JiraWithTempoPluginApi.cs b/jwl.jira/JiraWithTempoPluginApi.cs index b85fb0a..788df12 100644 --- a/jwl.jira/JiraWithTempoPluginApi.cs +++ b/jwl.jira/JiraWithTempoPluginApi.cs @@ -1,171 +1,169 @@ -namespace jwl.jira; -using System.Net.Http.Json; -using jwl.infra; -using jwl.jira.api.rest.common; - -// https://www.tempo.io/server-api-documentation/timesheets -public class JiraWithTempoPluginApi - : IJiraClient -{ - private const string WorklogTypeAttributeKey = @"_WorklogType_"; - - private readonly HttpClient _httpClient; - private readonly VanillaJiraClient _vanillaJiraServerApi; - - public string UserName { get; } - - public JiraWithTempoPluginApi(HttpClient httpClient, string userName) - { - _httpClient = httpClient; - _vanillaJiraServerApi = new VanillaJiraClient(httpClient, userName); - - UserName = userName; - } - - public async Task GetUserInfo() - { - return await _vanillaJiraServerApi.GetUserInfo(); - } - - public async Task GetWorklogAttributeDefinitions() - { - return await _httpClient.GetAsJsonAsync(@"rest/tempo-core/1/work-attribute"); - } - - public async Task GetAvailableActivities() - { - api.rest.response.TempoWorklogAttributeDefinition[] attrEnumDefs = await GetWorklogAttributeDefinitions(); - - var 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 GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable? issueKeys) - { - JiraUserInfo userInfo = await GetUserInfo(); - 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(@"rest/tempo-timesheets/4/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) - { - JiraUserInfo userInfo = await GetUserInfo(); - 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(@"rest/tempo-timesheets/4/worklogs", request); - await VanillaJiraClient.CheckHttpResponseForErrorMessages(response); - } - - public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false) - { - UriBuilder uriBuilder = new UriBuilder() - { - Path = new UriPathBuilder(@"rest/tempo-timesheets/4/worklogs") - .Add(worklogId.ToString()) - }; - +namespace jwl.jira; +using System.Net.Http.Json; +using jwl.infra; +using jwl.jira.api.rest.common; + +// https://www.tempo.io/server-api-documentation/timesheets +public class JiraWithTempoPluginApi + : IJiraClient +{ + private const string WorklogTypeAttributeKey = @"_WorklogType_"; + + private readonly HttpClient _httpClient; + private readonly VanillaJiraClient _vanillaJiraApi; + + public string UserName { get; } + public api.rest.common.JiraUserInfo UserInfo => _vanillaJiraApi.UserInfo; + + public JiraWithTempoPluginApi(HttpClient httpClient, string userName) + { + _httpClient = httpClient; + UserName = userName; + _vanillaJiraApi = new VanillaJiraClient(httpClient, userName); + } + + public async Task GetWorklogAttributeDefinitions() + { + return await _httpClient.GetAsJsonAsync(@"rest/tempo-core/1/work-attribute"); + } + + public async Task GetAvailableActivities() + { + api.rest.response.TempoWorklogAttributeDefinition[] attrEnumDefs = await GetWorklogAttributeDefinitions(); + + var 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 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(@"rest/tempo-timesheets/4/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(@"rest/tempo-timesheets/4/worklogs", request); + await VanillaJiraClient.CheckHttpResponseForErrorMessages(response); + } + + public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false) + { + UriBuilder uriBuilder = new UriBuilder() + { + Path = new UriPathBuilder(@"rest/tempo-timesheets/4/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(@"rest/tempo-timesheets/4/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 - } - } - }; - + 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(@"rest/tempo-timesheets/4/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/VanillaJiraClient.cs b/jwl.jira/VanillaJiraClient.cs index 1204540..170f1c4 100644 --- a/jwl.jira/VanillaJiraClient.cs +++ b/jwl.jira/VanillaJiraClient.cs @@ -13,13 +13,16 @@ public class VanillaJiraClient : IJiraClient { public string UserName { get; } + public api.rest.common.JiraUserInfo UserInfo => _lazyUserInfo.Value; private readonly HttpClient _httpClient; + private readonly Lazy _lazyUserInfo; public VanillaJiraClient(HttpClient httpClient, string userName) { _httpClient = httpClient; UserName = userName; + _lazyUserInfo = new Lazy(() => GetUserInfo().Result); } public static async Task CheckHttpResponseForErrorMessages(HttpResponseMessage responseMessage) @@ -34,17 +37,6 @@ public static async Task CheckHttpResponseForErrorMessages(HttpResponseMessage r } } - public async Task GetUserInfo() - { - UriBuilder uriBuilder = new UriBuilder() - { - Path = @"rest/api/2/user", - Query = new UriQueryBuilder() - .Add(@"username", UserName) - }; - return await _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery); - } - #pragma warning disable CS1998 public async Task GetAvailableActivities() { @@ -52,28 +44,20 @@ public async Task GetAvailableActivities() } #pragma warning restore - public async Task GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable? issueKeys) + public async Task GetIssueWorklogs(DateOnly from, DateOnly to, string issueKey) { - if (issueKeys is null) - return Array.Empty(); - - Task[] responseTasks = issueKeys - .Distinct() - .Select(issueKey => new UriBuilder() - { - Path = new UriPathBuilder(@"rest/api/2/issue") - .Add(issueKey) - .Add(@"worklog") - }) - .Select(uriBuilder => _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery)) - .ToArray(); + UriBuilder uriBuilder = new UriBuilder() + { + Path = new UriPathBuilder(@"rest/api/2/issue") + .Add(issueKey) + .Add(@"worklog") + }; - await Task.WhenAll(responseTasks); + var response = await _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery); (DateTime minDt, DateTime supDt) = DateOnlyUtils.DateOnlyRangeToDateTimeRange(from, to); - var result = responseTasks - .SelectMany(task => task.Result.Worklogs) + var result = response.Worklogs .Where(worklog => worklog.Author.Name == UserName) .Where(worklog => worklog.Started.Value >= minDt && worklog.Started.Value < supDt) .Select(wl => new WorkLog( @@ -92,6 +76,25 @@ public async Task GetIssueWorklogs(DateOnly from, DateOnly to, IEnume 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() @@ -177,4 +180,15 @@ public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, i HttpResponseMessage response = await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request); await CheckHttpResponseForErrorMessages(response); } + + private async Task GetUserInfo() + { + UriBuilder uriBuilder = new UriBuilder() + { + Path = @"rest/api/2/user", + Query = new UriQueryBuilder() + .Add(@"username", UserName) + }; + return await _httpClient.GetAsJsonAsync(uriBuilder.Uri.PathAndQuery); + } } From e514681fd9cf09ec9c033ba08f6e4848cfffe1f9 Mon Sep 17 00:00:00 2001 From: nop77svk Date: Thu, 1 Feb 2024 08:56:46 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=9B=A0=20fix:=20handling=20of=20cance?= =?UTF-8?q?lled=20multitask=20corrected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jwl.infra/MultiTask.cs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/jwl.infra/MultiTask.cs b/jwl.infra/MultiTask.cs index 2d7bb70..1ca7b10 100644 --- a/jwl.infra/MultiTask.cs +++ b/jwl.infra/MultiTask.cs @@ -67,19 +67,22 @@ public async Task WhenAll(IEnumerable tasks, CancellationToken? cancellati } } - if (errors.All(ex => ex is TaskCanceledException)) + if (errors.Any()) { - State = ProgressState.Cancelled; - ProcessFeedback?.Invoke(this); + if (errors.All(ex => ex is TaskCanceledException)) + { + State = ProgressState.Cancelled; + ProcessFeedback?.Invoke(this); - throw new TaskCanceledException($"All {errors.Count} tasks have been cancelled", new AggregateException(errors)); - } - else if (errors.Any()) - { - State = ProgressState.Error; - ProcessFeedback?.Invoke(this); + throw new TaskCanceledException($"All {errors.Count} tasks have been cancelled", new AggregateException(errors)); + } + else + { + State = ProgressState.Error; + ProcessFeedback?.Invoke(this); - throw new AggregateException(errors); + throw new AggregateException(errors); + } } else { From 424ba3fc04e9bec296691d4e4cb001f09dbd0ad3 Mon Sep 17 00:00:00 2001 From: nop77svk Date: Thu, 1 Feb 2024 09:10:46 +0100 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=9B=A0=20README=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6f17442..ba4299c 100644 --- a/README.md +++ b/README.md @@ -21,25 +21,27 @@ Meet the scripted Jira worklogging! Give it your worklogs in a CSV file (and you ## 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 +- "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) +- 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) - - TempoWorklogType (string) - Tempo Timesheets worklog type; values are checked against the available values from Jira server on each execution. + - 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. From a8f2ea8f18b6b434bf96d12e4b1bbc59ac5d9868 Mon Sep 17 00:00:00 2001 From: nop77svk Date: Thu, 1 Feb 2024 10:32:44 +0100 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A7=B9=20multitask=20stuff=20naming?= =?UTF-8?q?=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jwl.core/JwlCoreProcess.cs | 4 ++-- jwl.infra/MultiTask.cs | 39 +++++++++++++++++++------------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/jwl.core/JwlCoreProcess.cs b/jwl.core/JwlCoreProcess.cs index cc9cdb1..b380815 100644 --- a/jwl.core/JwlCoreProcess.cs +++ b/jwl.core/JwlCoreProcess.cs @@ -164,7 +164,7 @@ private async Task FillJiraWithWorklogs(InputWorkLog[] inputWorklogs, WorkLog[] MultiTaskStats progress = new MultiTaskStats(fillJiraWithWorklogsTasks.Length); MultiTask multiTask = new MultiTask() { - TaskFeedback = t => Feedback?.FillJiraWithWorklogsProcess(progress.ApplyTaskStatus(t.Status)) + OnTaskAwaited = t => Feedback?.FillJiraWithWorklogsProcess(progress.ApplyTaskStatus(t.Status)) }; await multiTask.WhenAll(fillJiraWithWorklogsTasks); @@ -181,7 +181,7 @@ private async Task ReadInputFiles(IEnumerable fileNames) MultiTaskStats progressStats = new MultiTaskStats(readerTasks.Length); MultiTask multiTask = new MultiTask() { - TaskFeedback = t => Feedback?.ReadCsvInputProcess(progressStats.ApplyTaskStatus(t.Status)) + OnTaskAwaited = t => Feedback?.ReadCsvInputProcess(progressStats.ApplyTaskStatus(t.Status)) }; if (readerTasks.Any()) diff --git a/jwl.infra/MultiTask.cs b/jwl.infra/MultiTask.cs index 1ca7b10..b180c39 100644 --- a/jwl.infra/MultiTask.cs +++ b/jwl.infra/MultiTask.cs @@ -6,8 +6,8 @@ public enum ProgressState { Unknown, Starting, - BeforeTaskWait, - AfterTaskWait, + BeforeTaskAwait, + AfterTaskAwait, Finished, Error, Cancelled @@ -15,8 +15,8 @@ public enum ProgressState public ProgressState State { get; private set; } = ProgressState.Unknown; - public Action? ProcessFeedback { get; init; } - public Action? TaskFeedback { get; init; } + public Action? OnStateChange { get; init; } + public Action? OnTaskAwaited { get; init; } public MultiTask() { @@ -25,35 +25,34 @@ public MultiTask() public async Task WhenAll(IEnumerable tasks, CancellationToken? cancellationToken = null) { State = ProgressState.Starting; - ProcessFeedback?.Invoke(this); + OnStateChange?.Invoke(this); HashSet tasksToExecute = tasks.ToHashSet(); List errors = new List(); while (tasksToExecute.Any()) { - State = ProgressState.BeforeTaskWait; - ProcessFeedback?.Invoke(this); + State = ProgressState.BeforeTaskAwait; + OnStateChange?.Invoke(this); - Task? taskFinished = null; try { cancellationToken?.ThrowIfCancellationRequested(); - taskFinished = await Task.WhenAny(tasksToExecute); + Task finishedTask = await Task.WhenAny(tasksToExecute); - State = ProgressState.AfterTaskWait; - ProcessFeedback?.Invoke(this); - TaskFeedback?.Invoke(taskFinished); + State = ProgressState.AfterTaskAwait; + OnStateChange?.Invoke(this); + OnTaskAwaited?.Invoke(finishedTask); - if (taskFinished.Status is TaskStatus.Faulted or TaskStatus.Canceled) + if (finishedTask.Status is TaskStatus.Faulted or TaskStatus.Canceled) { - tasksToExecute.Remove(taskFinished); - throw taskFinished.Exception ?? new Exception($"Task ended in {taskFinished.Status} status without exception details"); + tasksToExecute.Remove(finishedTask); + throw finishedTask.Exception ?? new Exception($"Task ended in {finishedTask.Status} status without exception details"); } - else if (taskFinished.Status == TaskStatus.RanToCompletion) + else if (finishedTask.Status == TaskStatus.RanToCompletion) { - if (!tasksToExecute.Remove(taskFinished)) + if (!tasksToExecute.Remove(finishedTask)) throw new InvalidOperationException("Task reported as finished... again!"); } } @@ -72,14 +71,14 @@ public async Task WhenAll(IEnumerable tasks, CancellationToken? cancellati if (errors.All(ex => ex is TaskCanceledException)) { State = ProgressState.Cancelled; - ProcessFeedback?.Invoke(this); + OnStateChange?.Invoke(this); throw new TaskCanceledException($"All {errors.Count} tasks have been cancelled", new AggregateException(errors)); } else { State = ProgressState.Error; - ProcessFeedback?.Invoke(this); + OnStateChange?.Invoke(this); throw new AggregateException(errors); } @@ -87,7 +86,7 @@ public async Task WhenAll(IEnumerable tasks, CancellationToken? cancellati else { State = ProgressState.Finished; - ProcessFeedback?.Invoke(this); + OnStateChange?.Invoke(this); } } }