Skip to content

Commit

Permalink
Add support for setting workspace using the config (#80)
Browse files Browse the repository at this point in the history
* πŸ—„οΈ Add workspaces to `get_entities`

* πŸ“₯ Use workspace specified in config

* 🎬 Use `workspace_id` from default time-entry if it's present

* 🩹 Limit `get_entities` calls

* We were running into 429s quite frequently, probably need to refactor this

Closes #79
  • Loading branch information
shantanuraj authored Nov 4, 2024
1 parent fb1e804 commit 3e9816b
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 11 deletions.
27 changes: 26 additions & 1 deletion src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::models::Entities;
use crate::models::Project;
use crate::models::Task;
use crate::models::TimeEntry;
use crate::models::Workspace;
use async_trait::async_trait;
use base64::{engine::general_purpose, Engine as _};
use error::ApiError;
Expand All @@ -21,6 +22,7 @@ use super::models::NetworkClient;
use super::models::NetworkProject;
use super::models::NetworkTask;
use super::models::NetworkTimeEntry;
use super::models::NetworkWorkspace;

#[cfg_attr(test, automock)]
#[async_trait]
Expand Down Expand Up @@ -58,6 +60,11 @@ impl V9ApiClient {
self.get::<Vec<NetworkTask>>(url).await
}

async fn get_workspaces(&self) -> ResultWithDefaultError<Vec<NetworkWorkspace>> {
let url = format!("{}/me/workspaces", self.base_url);
self.get::<Vec<NetworkWorkspace>>(url).await
}

pub fn from_credentials(
credentials: credentials::Credentials,
proxy: Option<String>,
Expand Down Expand Up @@ -142,11 +149,18 @@ impl ApiClient for V9ApiClient {
}

async fn get_entities(&self) -> ResultWithDefaultError<Entities> {
let (network_time_entries, network_projects, network_tasks, network_clients) = tokio::join!(
let (
network_time_entries,
network_projects,
network_tasks,
network_clients,
network_workspaces,
) = tokio::join!(
self.get_time_entries(),
self.get_projects(),
self.get_tasks(),
self.get_clients(),
self.get_workspaces(),
);

let clients: HashMap<i64, crate::models::Client> = network_clients
Expand Down Expand Up @@ -220,11 +234,22 @@ impl ApiClient for V9ApiClient {
})
.collect();

let workspaces = network_workspaces
.unwrap_or_default()
.iter()
.map(|w| Workspace {
id: w.id,
name: w.name.clone(),
admin: w.admin,
})
.collect();

Ok(Entities {
time_entries,
projects,
tasks,
clients,
workspaces,
})
}
}
7 changes: 7 additions & 0 deletions src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ pub struct NetworkTask {
pub project_id: i64,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NetworkWorkspace {
pub id: i64,
pub name: String,
pub admin: bool,
}

impl From<TimeEntry> for NetworkTimeEntry {
fn from(value: TimeEntry) -> Self {
NetworkTimeEntry {
Expand Down
24 changes: 15 additions & 9 deletions src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ impl StartCommand {
let track_config = config::parser::get_config_from_file(config_path)?;
let default_time_entry = track_config.get_default_entry(entities.clone())?;

let workspace_id = if default_time_entry.workspace_id != -1 {
default_time_entry.workspace_id
} else {
workspace_id
};

let project = project_name
.and_then(|name| {
entities
Expand All @@ -130,7 +136,7 @@ impl StartCommand {
interactively_create_time_entry(
default_time_entry,
workspace_id,
entities,
entities.clone(),
picker,
description,
project,
Expand All @@ -146,15 +152,15 @@ impl StartCommand {
}
};

let started_entry_id = api_client.create_time_entry(time_entry_to_create).await?;
let entities = api_client.get_entities().await?;
let started_entry = entities
.time_entries
.iter()
.find(|te| te.id == started_entry_id)
.unwrap();
let started_entry_id = api_client
.create_time_entry(time_entry_to_create.clone())
.await;
if started_entry_id.is_err() {
println!("{}", "Failed to start time entry".red());
return Err(started_entry_id.err().unwrap());
}

println!("{}\n{}", "Time entry started".green(), started_entry);
println!("{}\n{}", "Time entry started".green(), time_entry_to_create);

Ok(())
}
Expand Down
13 changes: 12 additions & 1 deletion src/config/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,8 +546,19 @@ impl TrackConfig {
.find(|t| t.name == name && t.project.id == project_id.unwrap())
});

let workspace_id = config.workspace.as_ref().map_or(
// Default to -1 if workspace is not set
Ok(-1),
|name| {
entities.workspace_id_for_name(name).ok_or_else(|| {
Box::new(ConfigError::WorkspaceNotFound(name.clone()))
as Box<dyn std::error::Error + Send>
})
},
)?;

let time_entry = TimeEntry {
// TODO: Add support for workspace
workspace_id,
description: config.description.clone().unwrap_or_default(),
billable: config.billable,
tags: config.tags.clone().unwrap_or_default(),
Expand Down
1 change: 1 addition & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub const CONFIG_FILE_NOT_FOUND_ERROR: &str = "No config file found";
pub const CONFIG_PARSE_ERROR: &str = "Failed to parse config file";
pub const CONFIG_UNRECOGNIZED_MACRO_ERROR: &str = "Unrecognized macro in config file";
pub const CONFIG_SHELL_MACRO_RESOLUTION_ERROR: &str = "Failed to resolve shell macro";
pub const CONFIG_INVALID_WORKSPACE_ERROR: &str = "Workspace not found";
pub const NO_PROJECT: &str = "No Project";
pub const NO_DESCRIPTION: &str = "(no description)";
pub const DIRECTORY_NOT_FOUND_ERROR: &str = "Directory not found";
Expand Down
10 changes: 10 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub enum ConfigError {
FileNotFound,
UnrecognizedMarco(String),
ShellResolution(String, String),
WorkspaceNotFound(String),
}

impl Display for ConfigError {
Expand Down Expand Up @@ -135,6 +136,15 @@ impl Display for ConfigError {
command.yellow().bold(),
)
}
ConfigError::WorkspaceNotFound(workspace) => {
format!(
"{}: {}\n{}\n{}",
constants::CONFIG_INVALID_WORKSPACE_ERROR.red().bold(),
workspace.red().bold(),
"Check your configuration file".yellow().bold(),
"toggl config --edit".yellow().bold(),
)
}
};
writeln!(f, "{}", summary)
}
Expand Down
15 changes: 15 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@ pub struct Entities {
pub projects: HashMap<i64, Project>,
pub tasks: HashMap<i64, Task>,
pub clients: HashMap<i64, Client>,
pub workspaces: Vec<Workspace>,
}

impl Entities {
pub fn running_time_entry(&self) -> Option<TimeEntry> {
self.time_entries.iter().find(|te| te.is_running()).cloned()
}

pub fn workspace_id_for_name(&self, name: &str) -> Option<i64> {
self.workspaces
.iter()
.find(|w| w.name == name)
.map(|w| w.id)
}
}

#[derive(Serialize, Deserialize, Clone, Debug)]
Expand Down Expand Up @@ -63,6 +71,13 @@ pub struct Project {
pub billable: Option<bool>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Workspace {
pub id: i64,
pub name: String,
pub admin: bool,
}

lazy_static! {
pub static ref HAS_TRUECOLOR_SUPPORT: bool = if let Ok(truecolor) = env::var("COLORTERM") {
truecolor == "truecolor" || truecolor == "24bit"
Expand Down

0 comments on commit 3e9816b

Please sign in to comment.