Skip to content

Commit

Permalink
Add support for client projects
Browse files Browse the repository at this point in the history
- Refactor the whole ClockifyContext
- Make API calls for projects more robust
  • Loading branch information
eXpl0it3r committed May 5, 2023
1 parent 95e82c1 commit c076413
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 104 deletions.
192 changes: 88 additions & 104 deletions Clockify/ClockifyContext.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Clockify.Net;
using Clockify.Net.Models.Projects;
using Clockify.Net.Models.Tasks;
using Clockify.Net.Models.TimeEntries;
using Clockify.Net.Models.Users;
using Clockify.Net.Models.Workspaces;

namespace Clockify;

public class ClockifyContext
{
private readonly Logger _logger;

private string _apiKey = string.Empty;

private ClockifyClient _clockifyClient;
private CurrentUserDto _currentUser = new();

private string _apiKey = string.Empty;
private string _clientName = string.Empty;
private string _projectName = string.Empty;
private Dictionary<string, List<ProjectDtoImpl>> _projects = new();
private string _serverUrl = string.Empty;
private string _taskName = string.Empty;
private string _timerName = string.Empty;
private string _workspaceName = string.Empty;
private List<WorkspaceDto> _workspaces = new();

public ClockifyContext(Logger logger)
{
Expand All @@ -39,25 +36,30 @@ public bool IsValid()

public async Task ToggleTimerAsync()
{
if (_clockifyClient == null
|| _workspaces.All(w => w.Name != _workspaceName)
|| !string.IsNullOrEmpty(_projectName) && (!_projects.ContainsKey(_workspaceName) || _projects[_workspaceName].All(p => p.Name != _projectName)))
// TODO Validation for project
if (_clockifyClient is null || string.IsNullOrWhiteSpace(_workspaceName))
{
_logger.LogWarn($"Invalid settings for toggle {_workspaceName}, {_projectName}, {_timerName}");
return;
}

var runningTimer = await GetRunningTimerAsync();

await StopRunningTimerAsync();

if (runningTimer != null)
{
await StopRunningTimerAsync();
return;
}

var workspaces = await _clockifyClient.GetWorkspacesAsync();
if (!workspaces.IsSuccessful || workspaces.Data is null)
{
_logger.LogWarn("Unable to retrieve available workspaces");
return;
}

await StopRunningTimerAsync();

var workspace = _workspaces.Single(w => w.Name == _workspaceName);
var workspace = workspaces.Data.Single(w => w.Name == _workspaceName);
var timeEntryRequest = new TimeEntryRequest
{
UserId = _currentUser.Id,
Expand All @@ -68,13 +70,19 @@ public async Task ToggleTimerAsync()

if (!string.IsNullOrEmpty(_projectName))
{
var project = _projects[_workspaceName].Single(p => p.Name == _projectName);
var project = await FindMatchingProjectAsync(workspace.Id);

if (project is null)
{
return;
}

timeEntryRequest.ProjectId = project.Id;

if (!string.IsNullOrEmpty(_taskName))
{
var taskId = await FindOrCreateTaskAsync(workspace, project, _taskName);
if (taskId != null)
var taskId = await FindOrCreateTaskAsync(workspace.Id, project.Id, _taskName);
if (taskId is not null)
{
timeEntryRequest.TaskId = taskId;
}
Expand All @@ -85,47 +93,25 @@ public async Task ToggleTimerAsync()
_logger.LogInfo($"Toggle Timer {_workspaceName}, {_projectName}, {_taskName}, {_timerName}");
}

public async Task StopRunningTimerAsync()
public async Task<TimeEntryDtoImpl> GetRunningTimerAsync()
{
if (_clockifyClient == null || _workspaces.All(w => w.Name != _workspaceName))
{
return;
}

var workspace = _workspaces.Single(w => w.Name == _workspaceName);
var runningTimer = await GetRunningTimerAsync();
if (runningTimer == null)
// TODO Validation for project
if (_clockifyClient is null || string.IsNullOrWhiteSpace(_workspaceName))
{
return;
_logger.LogWarn($"Invalid settings for running timer {_workspaceName}");
return null;
}

var timerUpdate = new UpdateTimeEntryRequest
{
Billable = runningTimer.Billable,
Start = runningTimer.TimeInterval.Start,
End = DateTimeOffset.UtcNow,
ProjectId = runningTimer.ProjectId,
TaskId = runningTimer.TaskId,
Description = runningTimer.Description
};

await _clockifyClient.UpdateTimeEntryAsync(workspace.Id, runningTimer.Id, timerUpdate);
_logger.LogInfo($"Timer Stopped {_workspaceName}, {runningTimer.ProjectId}, {runningTimer.TaskId}, {runningTimer.Description}");
}

public async Task<TimeEntryDtoImpl> GetRunningTimerAsync()
{
if (_clockifyClient == null
|| _workspaces.All(w => w.Name != _workspaceName)
|| !string.IsNullOrEmpty(_projectName) && (!_projects.ContainsKey(_workspaceName) || _projects[_workspaceName].All(p => p.Name != _projectName)))
var workspaces = await _clockifyClient.GetWorkspacesAsync();
if (!workspaces.IsSuccessful || workspaces.Data is null)
{
_logger.LogWarn($"Invalid settings for running timer {_workspaceName}");
_logger.LogWarn("Unable to retrieve available workspaces");
return null;
}

var workspace = _workspaces.Single(w => w.Name == _workspaceName);
var workspace = workspaces.Data.Single(w => w.Name == _workspaceName);
var timeEntries = await _clockifyClient.FindAllTimeEntriesForUserAsync(workspace.Id, _currentUser.Id, inProgress: true);
if (!timeEntries.IsSuccessful || timeEntries.Data == null)
if (!timeEntries.IsSuccessful || timeEntries.Data is null)
{
return null;
}
Expand All @@ -137,26 +123,32 @@ public async Task<TimeEntryDtoImpl> GetRunningTimerAsync()
: timeEntries.Data.FirstOrDefault(t => t.Description == _timerName);
}

var project = _projects[_workspaceName].Single(p => p.Name == _projectName);
var project = await FindMatchingProjectAsync(workspace.Id);

if (project is null)
{
return null;
}

return string.IsNullOrEmpty(_timerName)
? timeEntries.Data.FirstOrDefault(t => t.ProjectId == project.Id)
: timeEntries.Data.FirstOrDefault(t => t.ProjectId == project.Id && t.Description == _timerName);
}

public async Task<bool> UpdateSettings(PluginSettings settings)
public async Task UpdateSettings(PluginSettings settings)
{
if (_clockifyClient == null || settings.ApiKey != _apiKey || settings.ServerUrl != _serverUrl)
{
if (!Uri.IsWellFormedUriString(settings.ServerUrl, UriKind.Absolute))
{
_logger.LogWarn("Server URL is invalid");
return false;
return;
}

if (settings.ApiKey.Length != 48)
{
_logger.LogWarn("Invalid API key format");
return false;
return;
}

_serverUrl = settings.ServerUrl;
Expand All @@ -169,93 +161,85 @@ public async Task<bool> UpdateSettings(PluginSettings settings)
_projectName = settings.ProjectName;
_taskName = settings.TaskName;
_timerName = settings.TimerName;
_clientName = settings.ClientName;

if (await TestConnectionAsync())
{
_logger.LogInfo("API key successfully set");

await UpdateWorkspacesAsync();
foreach (var workspace in _workspaces)
{
await UpdateProjectsAsync(workspace.Name);
}

return true;
_logger.LogInfo("Connection to Clockify successfully established");
return;
}

_logger.LogWarn("Invalid API key");
_logger.LogWarn("Invalid server URL or API key");
_clockifyClient = null;
_currentUser = new CurrentUserDto();
_workspaces = new List<WorkspaceDto>();
_projects = new Dictionary<string, List<ProjectDtoImpl>>();
return false;
}

public async Task<List<WorkspaceDto>> GetWorkspacesAsync()
private async Task StopRunningTimerAsync()
{
if (!_workspaces.Any())
if (_clockifyClient == null || string.IsNullOrWhiteSpace(_workspaceName))
{
await UpdateWorkspacesAsync();
return;
}

return _workspaces;
}

public async Task<List<ProjectDtoImpl>> GetProjectsAsync(string workspaceName)
{
if (!_projects.ContainsKey(workspaceName))
var workspaces = await _clockifyClient.GetWorkspacesAsync();
if (!workspaces.IsSuccessful || workspaces.Data is null)
{
await UpdateProjectsAsync(workspaceName);
_logger.LogWarn("Unable to retrieve available workspaces");
return;
}

return _projects[workspaceName];
}

private async Task UpdateWorkspacesAsync()
{
if (_clockifyClient == null || _workspaces.Any())
var workspace = workspaces.Data.Single(w => w.Name == _workspaceName);
var runningTimer = await GetRunningTimerAsync();
if (runningTimer == null)
{
return;
}

var workspaceResponse = await _clockifyClient.GetWorkspacesAsync();
if (workspaceResponse.IsSuccessful)
var timerUpdate = new UpdateTimeEntryRequest
{
_workspaces = workspaceResponse.Data;
}
Billable = runningTimer.Billable,
Start = runningTimer.TimeInterval.Start,
End = DateTimeOffset.UtcNow,
ProjectId = runningTimer.ProjectId,
TaskId = runningTimer.TaskId,
Description = runningTimer.Description
};

await _clockifyClient.UpdateTimeEntryAsync(workspace.Id, runningTimer.Id, timerUpdate);
_logger.LogInfo($"Timer Stopped {_workspaceName}, {runningTimer.ProjectId}, {runningTimer.TaskId}, {runningTimer.Description}");
}

private async Task UpdateProjectsAsync(string workspaceName)
private async Task<ProjectDtoImpl> FindMatchingProjectAsync(string workspaceId)
{
if (_clockifyClient == null || _projects.ContainsKey(workspaceName))
var projects = await _clockifyClient.FindAllProjectsOnWorkspaceAsync(workspaceId, false, _projectName, pageSize: 5000);
if (!projects.IsSuccessful || projects.Data is null)
{
return;
_logger.LogWarn($"Unable to retrieve project {_projectName} on workspace {_workspaceName}");
return null;
}

if (_workspaces.All(w => w.Name != workspaceName))
{
await UpdateWorkspacesAsync();
}
var project = projects.Data
.Where(p => p.Name == _projectName && (string.IsNullOrWhiteSpace(_clientName) || p.ClientName == _clientName))
.ToList();

var workspace = _workspaces.SingleOrDefault(w => w.Name == workspaceName);
if (workspace == null)
if (project.Count > 1)
{
return;
_logger.LogWarn($"Multiple projects with the name {_projectName} on workspace {_workspaceName}, consider setting a client name");
return null;
}

var projectResponse = await _clockifyClient.FindAllProjectsOnWorkspaceAsync(workspace.Id, pageSize: 5000);
if (!projectResponse.IsSuccessful)
if (!project.Any())
{
return;
_logger.LogWarn($"Unable to find project {_projectName} on workspace {_workspaceName} for client {_clientName}");
return null;
}

_projects[workspace.Name] = projectResponse.Data;
return project.Single();
}

private async Task<string> FindOrCreateTaskAsync(WorkspaceDto workspace, ProjectDtoImpl project, string taskName)
private async Task<string> FindOrCreateTaskAsync(string workspaceId, string projectId, string taskName)
{
var taskResponse =
await _clockifyClient.FindAllTasksAsync(workspace.Id, project.Id, name: taskName, pageSize: 5000);
var taskResponse = await _clockifyClient.FindAllTasksAsync(workspaceId, projectId, name: taskName, pageSize: 5000);

if (!taskResponse.IsSuccessful || taskResponse.Data == null)
{
Expand All @@ -272,7 +256,7 @@ private async Task<string> FindOrCreateTaskAsync(WorkspaceDto workspace, Project
Name = taskName
};

var creationResponse = await _clockifyClient.CreateTaskAsync(workspace.Id, project.Id, taskRequest);
var creationResponse = await _clockifyClient.CreateTaskAsync(workspaceId, projectId, taskRequest);

if (!creationResponse.IsSuccessful || creationResponse.Data == null)
{
Expand All @@ -290,7 +274,7 @@ private async Task<bool> TestConnectionAsync()
}

var user = await _clockifyClient.GetCurrentUserAsync();
if (!user.IsSuccessful)
if (!user.IsSuccessful || user.Data is null)
{
return false;
}
Expand Down
3 changes: 3 additions & 0 deletions Clockify/PluginSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public class PluginSettings
[JsonProperty(PropertyName = "timerName")]
public string TimerName { get; set; } = string.Empty;

[JsonProperty(PropertyName = "clientName")]
public string ClientName { get; set; } = string.Empty;

[JsonProperty(PropertyName = "titleFormat")]
public string TitleFormat { get; set; } = string.Empty;

Expand Down
4 changes: 4 additions & 0 deletions PropertyInspector/PluginActionPI.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
<div type="group" class="sdpi-item" id="advanced">
<div class="sdpi-item-label">Advanced</div>
<div class="sdpi-item-group" id="messagegroup_items">
<div class="sdpi-item">
<div class="sdpi-item-label">Client Name</div>
<input class="sdpi-item-value sdProperty" id="clientName" value="" placeholder="Enter the name of the Client" oninput="setSettings()">
</div>
<div class="sdpi-item">
<div class="sdpi-item-label">Title Format</div>
<div class="sdpi-item-value textarea">
Expand Down

0 comments on commit c076413

Please sign in to comment.