use anyhow::Result;
use clap::Subcommand;
use serde_json::json;
use tabled::{Table, Tabled};
use crate::api::LinearClient;
use crate::output::{print_json, OutputOptions};
use crate::text::truncate;
use crate::DISPLAY_OPTIONS;
#[derive(Subcommand, Debug)]
pub enum TriageCommands {
List {
#[arg(short, long)]
team: Option<String>,
},
Claim {
id: String,
},
Snooze {
id: String,
#[arg(short, long, default_value = "1d")]
duration: String,
},
}
#[derive(Tabled)]
struct TriageRow {
#[tabled(rename = "ID")]
identifier: String,
#[tabled(rename = "Title")]
title: String,
#[tabled(rename = "Created")]
created: String,
#[tabled(rename = "Team")]
team: String,
}
pub async fn handle(cmd: TriageCommands, output: &OutputOptions) -> Result<()> {
match cmd {
TriageCommands::List { team } => list_triage(team, output).await,
TriageCommands::Claim { id } => claim_issue(&id, output).await,
TriageCommands::Snooze { id, duration } => snooze_issue(&id, &duration, output).await,
}
}
async fn list_triage(team: Option<String>, output: &OutputOptions) -> Result<()> {
let client = LinearClient::new()?;
let query = r#"
query($filter: IssueFilter) {
issues(first: 50, filter: $filter) {
nodes {
id
identifier
title
createdAt
team {
key
name
}
state {
name
type
}
}
}
}
"#;
let mut filter = json!({
"assignee": { "null": true },
"state": { "type": { "eq": "triage" } }
});
if let Some(ref t) = team {
filter["team"] = json!({ "key": { "eq": t } });
}
let result = client
.query(query, Some(json!({ "filter": filter })))
.await?;
let issues = &result["data"]["issues"]["nodes"];
if output.is_json() {
print_json(issues, output)?;
} else {
let display = DISPLAY_OPTIONS.get().cloned().unwrap_or_default();
let max_width = display.max_width(50);
let rows: Vec<TriageRow> = issues
.as_array()
.unwrap_or(&vec![])
.iter()
.map(|i| TriageRow {
identifier: i["identifier"].as_str().unwrap_or("-").to_string(),
title: truncate(i["title"].as_str().unwrap_or("-"), max_width),
created: i["createdAt"]
.as_str()
.unwrap_or("-")
.chars()
.take(10)
.collect(),
team: i["team"]["key"].as_str().unwrap_or("-").to_string(),
})
.collect();
if rows.is_empty() {
println!("No triage issues found - inbox zero!");
} else {
println!("{}", Table::new(rows));
}
}
Ok(())
}
async fn claim_issue(id: &str, output: &OutputOptions) -> Result<()> {
let client = LinearClient::new()?;
let me_query = r#"query { viewer { id } }"#;
let me_result = client.query(me_query, None).await?;
let my_id = me_result["data"]["viewer"]["id"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Could not get current user"))?;
let mutation = r#"
mutation($id: String!, $assigneeId: String!) {
issueUpdate(id: $id, input: { assigneeId: $assigneeId }) {
success
issue {
id
identifier
title
assignee { name }
}
}
}
"#;
let result = client
.mutate(
mutation,
Some(json!({ "id": id, "assigneeId": my_id })),
)
.await?;
if output.is_json() {
print_json(&result["data"]["issueUpdate"], output)?;
} else {
let issue = &result["data"]["issueUpdate"]["issue"];
println!(
"Claimed {} - {}",
issue["identifier"].as_str().unwrap_or(id),
issue["title"].as_str().unwrap_or("")
);
}
Ok(())
}
async fn snooze_issue(id: &str, duration: &str, output: &OutputOptions) -> Result<()> {
let client = LinearClient::new()?;
let days = match duration {
"1d" => 1,
"2d" => 2,
"3d" => 3,
"1w" => 7,
"2w" => 14,
_ => duration
.trim_end_matches('d')
.parse::<i64>()
.unwrap_or(1),
};
let snooze_until = chrono::Utc::now() + chrono::Duration::days(days);
let mutation = r#"
mutation($id: String!, $snoozedUntilAt: DateTime!) {
issueUpdate(id: $id, input: { snoozedUntilAt: $snoozedUntilAt }) {
success
issue {
id
identifier
snoozedUntilAt
}
}
}
"#;
let result = client
.mutate(
mutation,
Some(json!({ "id": id, "snoozedUntilAt": snooze_until.to_rfc3339() })),
)
.await?;
if output.is_json() {
print_json(&result["data"]["issueUpdate"], output)?;
} else {
println!("Snoozed {} for {}", id, duration);
}
Ok(())
}