Skip to content

Commit ae5e1ec

Browse files
committed
Fix client name and tags handling
1 parent 8761aa9 commit ae5e1ec

File tree

3 files changed

+135
-88
lines changed

3 files changed

+135
-88
lines changed

Clockify/ClockifyService.cs

Lines changed: 109 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Net.Http;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using ClockifyClient;
78
using ClockifyClient.Models;
@@ -13,6 +14,8 @@ public class ClockifyService(Logger logger)
1314
{
1415
private const int MaxPageSize = 5000;
1516

17+
private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1, 1);
18+
1619
private PluginSettings _settings = new();
1720

1821
private ClockifyApiClient _clockifyClient;
@@ -21,7 +24,6 @@ public class ClockifyService(Logger logger)
2124
private ProjectDtoV1 _project = new();
2225
private List<string> _tags = [];
2326
private TaskDtoV1 _task = new();
24-
private ClientWithCurrencyDtoV1 _client = new();
2527

2628
public bool IsValid => _clockifyClient is not null
2729
&& !string.IsNullOrWhiteSpace(_settings.WorkspaceName)
@@ -30,116 +32,140 @@ public class ClockifyService(Logger logger)
3032
public async Task<bool> ToggleTimerAsync()
3133
{
3234
logger.LogInfo("Toggling timer...");
33-
34-
if (!IsValid)
35+
36+
await _cacheLock.WaitAsync();
37+
try
3538
{
36-
logger.LogError($"Toggling trimer failed, invalid settings: {_settings}");
37-
return false;
38-
}
39+
if (!IsValid)
40+
{
41+
logger.LogError($"Toggling trimer failed, invalid settings: {_settings}");
42+
return false;
43+
}
3944

40-
var runningTimer = await StopRunningTimerAsync();
45+
var runningTimer = await StopRunningTimerAsync();
4146

42-
if (runningTimer is not null)
43-
{
44-
logger.LogInfo("Toggling trimer successful, timer has been stopped");
45-
return true;
46-
}
47+
if (runningTimer is not null)
48+
{
49+
logger.LogInfo("Toggling trimer successful, timer has been stopped");
50+
return true;
51+
}
4752

48-
try
49-
{
50-
var timeEntryRequest = await CreateTimeEntryRequestAsync();
51-
await _clockifyClient.V1.Workspaces[_workspace.Id].TimeEntries.PostAsync(timeEntryRequest);
52-
53-
logger.LogInfo("Toggling trimer successful, timer has been started");
54-
return true;
53+
try
54+
{
55+
var timeEntryRequest = await CreateTimeEntryRequestAsync();
56+
await _clockifyClient.V1.Workspaces[_workspace.Id].TimeEntries.PostAsync(timeEntryRequest);
57+
58+
logger.LogInfo("Toggling trimer successful, timer has been started");
59+
return true;
60+
}
61+
catch (Exception exception) when (exception is ApiException or HttpRequestException)
62+
{
63+
logger.LogError($"Toggling trimer failed, TimeEntry creation failed: {exception.Message}");
64+
return false;
65+
}
5566
}
56-
catch (Exception exception) when (exception is ApiException or HttpRequestException)
67+
finally
5768
{
58-
logger.LogError($"Toggling trimer failed, TimeEntry creation failed: {exception.Message}");
59-
return false;
69+
_cacheLock.Release();
6070
}
6171
}
6272

6373
public async Task<TimeEntryWithRatesDtoV1> GetRunningTimerAsync()
6474
{
6575
logger.LogInfo("Fetching running timer...");
6676

67-
if (!IsValid)
68-
{
69-
logger.LogError($"Fetching running timer failed, invalid settings: {_settings}");
70-
return null;
71-
}
72-
77+
await _cacheLock.WaitAsync();
7378
try
7479
{
75-
var timeEntries = await _clockifyClient.V1.Workspaces[_workspace.Id].User[_currentUser.Id].TimeEntries
76-
.GetAsync(p => p.QueryParameters.InProgress = true);
77-
78-
if (string.IsNullOrEmpty(_settings.ProjectName))
80+
if (!IsValid)
81+
{
82+
logger.LogError($"Fetching running timer failed, invalid settings: {_settings}");
83+
return null;
84+
}
85+
86+
try
7987
{
80-
return timeEntries?.FirstOrDefault(t => string.IsNullOrEmpty(_settings.TimerName) || t.Description == _settings.TimerName);
88+
var timeEntries = await _clockifyClient.V1.Workspaces[_workspace.Id].User[_currentUser.Id].TimeEntries
89+
.GetAsync(p => p.QueryParameters.InProgress = true);
90+
91+
if (string.IsNullOrEmpty(_settings.ProjectName))
92+
{
93+
return timeEntries?.FirstOrDefault(t => string.IsNullOrEmpty(_settings.TimerName) || t.Description == _settings.TimerName);
94+
}
95+
96+
if (_project is null)
97+
{
98+
logger.LogError($"Fetching running timer failed, no project in workspace matching {_settings.ProjectName}");
99+
return null;
100+
}
101+
102+
return timeEntries?.FirstOrDefault(t => t.ProjectId == _project.Id
103+
&& (string.IsNullOrEmpty(_settings.TimerName) || t.Description == _settings.TimerName)
104+
&& (string.IsNullOrEmpty(_settings.TaskName) || string.IsNullOrEmpty(_task?.Id) || t.TaskId == _task.Id)
105+
&& ((t.TagIds is null && _tags is null) || (t.TagIds is not null && _tags is not null && t.TagIds.OrderBy(s => s, StringComparer.InvariantCulture)
106+
.SequenceEqual(_tags.OrderBy(s => s, StringComparer.InvariantCulture))))
107+
&& t.Billable == _settings.Billable);
81108
}
82-
83-
if (_project is null)
109+
catch (Exception exception) when (exception is ApiException or HttpRequestException)
84110
{
85-
logger.LogError($"Fetching running timer failed, no project in workspace matching {_settings.ProjectName}");
111+
logger.LogError($"Fetching running timer failed, TimeEntry request failed: {exception.Message}");
86112
return null;
87113
}
88-
89-
return timeEntries?.FirstOrDefault(t => t.ProjectId == _project.Id
90-
&& (string.IsNullOrEmpty(_settings.TimerName) || t.Description == _settings.TimerName)
91-
&& (string.IsNullOrEmpty(_settings.TaskName) || string.IsNullOrEmpty(_task?.Id) || t.TaskId == _task.Id)
92-
&& ((t.TagIds is null && _tags is null) || t.TagIds is not null && _tags is not null && t.TagIds.OrderBy(s => s, StringComparer.InvariantCulture)
93-
.SequenceEqual(_tags.OrderBy(s => s, StringComparer.InvariantCulture)))
94-
&& t.Billable == _settings.Billable);
95114
}
96-
catch (Exception exception) when (exception is ApiException or HttpRequestException)
115+
finally
97116
{
98-
logger.LogError($"Fetching running timer failed, TimeEntry request failed: {exception.Message}");
99-
return null;
117+
_cacheLock.Release();
100118
}
101119
}
102120

103121
public async Task UpdateSettingsAsync(PluginSettings settings)
104122
{
105123
logger.LogInfo("Updating settings...");
106124

107-
SettingsValidator.MigrateServerUrl(settings);
108-
109-
var cacheInvalidationRequired = SettingsValidator.HasChanged(_settings, settings);
110-
111-
// Do we need to recreate the client?
112-
if (!IsValid || _settings.ApiKey != settings.ApiKey || _settings.ServerUrl != settings.ServerUrl)
125+
await _cacheLock.WaitAsync();
126+
try
113127
{
114-
logger.LogInfo("Updating settings, recreate Clockify client");
115-
116-
var validation = SettingsValidator.Validate(settings);
128+
SettingsValidator.MigrateServerUrl(settings);
129+
var cacheInvalidationRequired = SettingsValidator.HasChanged(_settings, settings);
117130

118-
if (!validation.IsValid)
131+
132+
// Do we need to recreate the client?
133+
if (!IsValid || _settings.ApiKey != settings.ApiKey || _settings.ServerUrl != settings.ServerUrl)
119134
{
120-
logger.LogError($"Updating settings failed, settings validation failed: {validation.Error}");
121-
return;
135+
logger.LogInfo("Updating settings, recreate Clockify client");
136+
137+
var validation = SettingsValidator.Validate(settings);
138+
139+
if (!validation.IsValid)
140+
{
141+
logger.LogError($"Updating settings failed, settings validation failed: {validation.Error}");
142+
return;
143+
}
144+
145+
_clockifyClient = ClockifyApiClientFactory.Create(settings.ApiKey, settings.ServerUrl);
146+
147+
if (!await TestConnectionAsync())
148+
{
149+
logger.LogError("Updating settings failed, invalid server URL or API key");
150+
_clockifyClient = null;
151+
_currentUser = new UserDtoV1();
152+
return;
153+
}
154+
155+
logger.LogInfo("Updating settings successful, connection to Clockify established");
156+
cacheInvalidationRequired = true;
122157
}
123158

124-
_clockifyClient = ClockifyApiClientFactory.Create(settings.ApiKey, settings.ServerUrl);
159+
_settings = new PluginSettings(settings);
125160

126-
if (!await TestConnectionAsync())
161+
if (cacheInvalidationRequired)
127162
{
128-
logger.LogError("Updating settings failed, invalid server URL or API key");
129-
_clockifyClient = null;
130-
_currentUser = new UserDtoV1();
131-
return;
163+
await ReloadCacheAsync();
132164
}
133-
134-
logger.LogInfo("Updating settings successful, connection to Clockify established");
135-
cacheInvalidationRequired = true;
136165
}
137-
138-
_settings = settings;
139-
140-
if (cacheInvalidationRequired)
166+
finally
141167
{
142-
await ReloadCacheAsync();
168+
_cacheLock.Release();
143169
}
144170
}
145171

@@ -180,22 +206,21 @@ private async Task ReloadCacheAsync()
180206
_project = null;
181207
_tags = [];
182208
_task = null;
183-
_client = null;
184209

185210
try
186211
{
187212
var workspaces = await _clockifyClient.V1.Workspaces.GetAsync();
188213
_workspace = workspaces?.SingleOrDefault(w => w.Name == _settings.WorkspaceName);
189214

190-
if (_workspace != null)
215+
if (_workspace is not null)
191216
{
192-
_project = await FindMatchingProjectAsync(_workspace.Id, _settings.ProjectName);
217+
var client = await FindMatchingClientAsync(_workspace.Id, _settings.ClientName);
218+
_project = await FindMatchingProjectAsync(_workspace.Id, _settings.ProjectName, client?.Id);
193219
_tags = await FindMatchingTagsAsync(_workspace.Id, _settings.Tags);
194220

195-
if (_project != null)
221+
if (_project is not null)
196222
{
197223
_task = await FindMatchingTaskAsync(_workspace.Id, _project.Id, _settings.TaskName);
198-
_client = await FindMatchingClientAsync(_workspace.Id, _settings.ClientName);
199224
}
200225
}
201226

@@ -216,7 +241,7 @@ private async Task<TimeEntryWithRatesDtoV1> StopRunningTimerAsync()
216241
}
217242

218243
var runningTimer = await GetRunningTimerAsync();
219-
if (runningTimer == null)
244+
if (runningTimer is null)
220245
{
221246
// No running timer
222247
return null;
@@ -246,13 +271,15 @@ private async Task<TimeEntryWithRatesDtoV1> StopRunningTimerAsync()
246271
return runningTimer;
247272
}
248273

249-
private async Task<ProjectDtoV1> FindMatchingProjectAsync(string workspaceId, string projectName)
274+
private async Task<ProjectDtoV1> FindMatchingProjectAsync(string workspaceId, string projectName, string clientId = null)
250275
{
251276
if (string.IsNullOrEmpty(projectName))
252277
{
253278
return null;
254279
}
255280

281+
logger.LogInfo("Finding matching project...");
282+
256283
try
257284
{
258285
var projects = await _clockifyClient.V1.Workspaces[workspaceId].Projects
@@ -262,9 +289,9 @@ private async Task<ProjectDtoV1> FindMatchingProjectAsync(string workspaceId, st
262289
q.QueryParameters.StrictNameSearch = true;
263290
q.QueryParameters.PageSize = MaxPageSize;
264291

265-
if (_client is not null)
292+
if (clientId is not null)
266293
{
267-
q.QueryParameters.Clients = [_client.Id];
294+
q.QueryParameters.Clients = [clientId];
268295
}
269296
});
270297

@@ -395,7 +422,7 @@ private async Task<List<string>> FindMatchingTagsAsync(string workspaceId, strin
395422
var tagsOnWorkspace = await _clockifyClient.V1.Workspaces[workspaceId].Tags
396423
.GetAsync(q => q.QueryParameters.PageSize = MaxPageSize);
397424

398-
return tagsOnWorkspace == null ? [] : tagsOnWorkspace.Where(t => tagList.Contains(t.Name)).Select(t => t.Id).ToList();
425+
return tagsOnWorkspace is null ? [] : tagsOnWorkspace.Where(t => tagList.Contains(t.Name)).Select(t => t.Id).ToList();
399426
}
400427
catch (Exception exception) when (exception is ApiException or HttpRequestException)
401428
{

Clockify/PluginSettings.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ namespace Clockify;
44

55
public class PluginSettings
66
{
7+
public PluginSettings()
8+
{}
9+
10+
public PluginSettings(PluginSettings settings)
11+
{
12+
ApiKey = settings.ApiKey;
13+
WorkspaceName = settings.WorkspaceName;
14+
ProjectName = settings.ProjectName;
15+
TaskName = settings.TaskName;
16+
TimerName = settings.TimerName;
17+
Tags = settings.Tags;
18+
ClientName = settings.ClientName;
19+
Billable = settings.Billable;
20+
TitleFormat = settings.TitleFormat;
21+
RefreshRate = settings.RefreshRate;
22+
ServerUrl = settings.ServerUrl;
23+
}
24+
725
[JsonProperty(PropertyName = "apiKey")]
826
public string ApiKey { get; set; } = string.Empty;
927

Clockify/TextFormatter.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System;
2+
13
namespace Clockify;
24

35
public static class TextFormatter
@@ -7,12 +9,12 @@ public static string CreateTimerText(PluginSettings settings, string timerTime)
79
if (!string.IsNullOrEmpty(settings.TitleFormat))
810
{
911
return settings.TitleFormat
10-
.Replace("{workspaceName}", settings.WorkspaceName)
11-
.Replace("{projectName}", settings.ProjectName)
12-
.Replace("{taskName}", settings.TaskName)
13-
.Replace("{timerName}", settings.TimerName)
14-
.Replace("{clientName}", settings.ClientName)
15-
.Replace("{timer}", timerTime);
12+
.Replace("{workspaceName}", settings.WorkspaceName, StringComparison.InvariantCultureIgnoreCase)
13+
.Replace("{projectName}", settings.ProjectName, StringComparison.InvariantCultureIgnoreCase)
14+
.Replace("{taskName}", settings.TaskName, StringComparison.InvariantCultureIgnoreCase)
15+
.Replace("{timerName}", settings.TimerName, StringComparison.InvariantCultureIgnoreCase)
16+
.Replace("{clientName}", settings.ClientName, StringComparison.InvariantCultureIgnoreCase)
17+
.Replace("{timer}", timerTime, StringComparison.InvariantCultureIgnoreCase);
1618
}
1719

1820
string timerText;

0 commit comments

Comments
 (0)