use usage_rs::{Args, RunAsyncWith};
use yt::YtError;
use yt::api::work_items::{ListFilter, WorkItem};
use yt::output::{Format, Table};
use super::{day, work_item_json};
use crate::cli::Ctx;
#[derive(Args)]
#[usage(effect = "read")]
pub(crate) struct List {
#[usage(arg, name = "QUERY")]
words: Vec<String>,
#[usage(long, value_name = "QUERY")]
query: Option<String>,
#[usage(short, long, value_name = "PROJECT")]
project: Option<String>,
#[usage(long, value_name = "DATE")]
from: Option<String>,
#[usage(long, value_name = "DATE")]
to: Option<String>,
#[usage(long, value_name = "LOGIN", default = "me")]
author: String,
#[usage(short = 'n', long, default = "50")]
limit: usize,
#[usage(long, conflicts("--limit"))]
all: bool,
}
impl RunAsyncWith<&Ctx> for List {
type Output = yt::Result<()>;
async fn run_async_with(self, ctx: &Ctx) -> Self::Output {
crate::cli::check_limit(self.limit)?;
let user_query = match (self.words.is_empty(), &self.query) {
(false, Some(_)) => {
return Err(YtError::Usage {
message: "give the query as words or with --query, not both".into(),
}
.into());
}
(false, None) => Some(self.words.join(" ")),
(true, query) => query.clone(),
};
let start = self
.from
.as_deref()
.map(|value| day("--from", value).map(yt::time::date))
.transpose()?;
let end = self
.to
.as_deref()
.map(|value| day("--to", value).map(yt::time::date))
.transpose()?;
let client = ctx.client().await?;
let project =
yt::config::profile::project(self.project.as_deref(), ctx.repo()?, &ctx.profile()?);
let query = yt::api::issues::build_query(user_query.as_deref(), project.as_deref(), false)?;
let author = match self.author.as_str() {
"all" => None,
"me" => Some(yt::api::users::me(client).await?.login),
login => Some(login.to_string()),
};
let filter = ListFilter {
query: Some(query).filter(|query| !query.is_empty()),
start,
end,
author,
};
let mut items = yt::api::work_items::list(client, &filter).await?;
if !self.all && items.len() > self.limit {
let total: i64 = items.iter().map(|item| item.minutes()).sum();
ctx.note(&format!(
"Showing {} of {} work items ({} in all); --all lists every one.",
self.limit,
items.len(),
yt::time::hours_minutes(total)
));
items.truncate(self.limit);
}
if items.is_empty() {
ctx.note("No work items found.");
}
match ctx.output.format() {
Format::Json => ctx
.output
.json(&items.iter().map(work_item_json).collect::<Vec<_>>()),
Format::Ndjson => items
.iter()
.try_for_each(|item| ctx.output.ndjson(&work_item_json(item))),
Format::Table => {
let mut table = Table::new([
"ITEM", "DATE", "ISSUE", "DURATION", "TYPE", "AUTHOR", "TEXT",
]);
for item in &items {
table.row([
item.id.clone(),
yt::time::date(item.date),
item.issue
.as_ref()
.map(|issue| issue.id_readable.clone())
.unwrap_or_default(),
item.presentation(),
item.work_type
.as_ref()
.map(|work_type| work_type.name.clone())
.unwrap_or_default(),
item.author
.as_ref()
.map(|author| author.login.clone())
.unwrap_or_default(),
item.text
.as_deref()
.and_then(|text| text.lines().next())
.unwrap_or("")
.to_string(),
]);
}
ctx.output.table(&table)?;
if ctx.output.is_terminal() && !items.is_empty() {
ctx.output.write(&total(&items))?;
}
Ok(())
}
}
}
}
fn total(items: &[WorkItem]) -> String {
let minutes: i64 = items.iter().map(WorkItem::minutes).sum();
format!("Total {}", yt::time::hours_minutes(minutes))
}
#[cfg(test)]
mod tests {
use yt::api::work_items::WorkItem;
use super::total;
fn item(minutes: i64) -> WorkItem {
serde_json::from_value(serde_json::json!({"id": "270-1", "duration": {"minutes": minutes, "presentation": ""}}))
.unwrap()
}
#[test]
fn the_total_adds_the_rows_shown() {
assert_eq!(total(&[item(90), item(45)]), "Total 2h 15m");
}
}