use anyhow::Result;
use clap::Subcommand;
use colored::Colorize;
use serde_json::json;
use tabled::{Table, Tabled};
use crate::api::LinearClient;
#[derive(Subcommand)]
pub enum DocumentCommands {
#[command(alias = "ls")]
List {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long)]
archived: bool,
},
Get {
id: String,
},
Create {
title: String,
#[arg(short, long)]
project: String,
#[arg(short, long)]
content: Option<String>,
#[arg(short, long)]
icon: Option<String>,
#[arg(long)]
color: Option<String>,
},
Update {
id: String,
#[arg(short, long)]
title: Option<String>,
#[arg(short, long)]
content: Option<String>,
#[arg(short, long)]
icon: Option<String>,
#[arg(long)]
color: Option<String>,
#[arg(short, long)]
project: Option<String>,
},
}
#[derive(Tabled)]
struct DocumentRow {
#[tabled(rename = "Title")]
title: String,
#[tabled(rename = "Project")]
project: String,
#[tabled(rename = "Updated")]
updated: String,
#[tabled(rename = "ID")]
id: String,
}
pub async fn handle(cmd: DocumentCommands) -> Result<()> {
match cmd {
DocumentCommands::List { project, archived } => list_documents(project, archived).await,
DocumentCommands::Get { id } => get_document(&id).await,
DocumentCommands::Create { title, project, content, icon, color } => {
create_document(&title, &project, content, icon, color).await
}
DocumentCommands::Update { id, title, content, icon, color, project } => {
update_document(&id, title, content, icon, color, project).await
}
}
}
async fn list_documents(project_id: Option<String>, include_archived: bool) -> Result<()> {
let client = LinearClient::new()?;
let query = r#"
query($includeArchived: Boolean) {
documents(first: 100, includeArchived: $includeArchived) {
nodes {
id
title
updatedAt
project { id name }
}
}
}
"#;
let variables = json!({ "includeArchived": include_archived });
let result = client.query(query, Some(variables)).await?;
let empty = vec![];
let documents = result["data"]["documents"]["nodes"]
.as_array()
.unwrap_or(&empty);
let filtered_docs: Vec<_> = if let Some(ref pid) = project_id {
documents
.iter()
.filter(|d| {
d["project"]["id"].as_str() == Some(pid.as_str())
|| d["project"]["name"].as_str().map(|n| n.to_lowercase()) == Some(pid.to_lowercase())
})
.collect()
} else {
documents.iter().collect()
};
if filtered_docs.is_empty() {
println!("No documents found.");
return Ok(());
}
let rows: Vec<DocumentRow> = filtered_docs
.iter()
.map(|d| {
let updated = d["updatedAt"]
.as_str()
.unwrap_or("")
.chars()
.take(10)
.collect::<String>();
DocumentRow {
title: d["title"].as_str().unwrap_or("").to_string(),
project: d["project"]["name"].as_str().unwrap_or("-").to_string(),
updated,
id: d["id"].as_str().unwrap_or("").to_string(),
}
})
.collect();
let table = Table::new(rows).to_string();
println!("{}", table);
println!("\n{} documents", filtered_docs.len());
Ok(())
}
async fn get_document(id: &str) -> Result<()> {
let client = LinearClient::new()?;
let query = r#"
query($id: String!) {
document(id: $id) {
id
title
content
icon
color
url
createdAt
updatedAt
creator { name email }
project { id name }
}
}
"#;
let result = client.query(query, Some(json!({ "id": id }))).await?;
let document = &result["data"]["document"];
if document.is_null() {
anyhow::bail!("Document not found: {}", id);
}
println!("{}", document["title"].as_str().unwrap_or("").bold());
println!("{}", "─".repeat(40));
if let Some(project_name) = document["project"]["name"].as_str() {
println!("Project: {}", project_name);
}
if let Some(creator_name) = document["creator"]["name"].as_str() {
println!("Creator: {}", creator_name);
}
if let Some(icon) = document["icon"].as_str() {
println!("Icon: {}", icon);
}
if let Some(color) = document["color"].as_str() {
println!("Color: {}", color);
}
println!("URL: {}", document["url"].as_str().unwrap_or("-"));
println!("ID: {}", document["id"].as_str().unwrap_or("-"));
if let Some(created) = document["createdAt"].as_str() {
println!("Created: {}", created.chars().take(10).collect::<String>());
}
if let Some(updated) = document["updatedAt"].as_str() {
println!("Updated: {}", updated.chars().take(10).collect::<String>());
}
if let Some(content) = document["content"].as_str() {
println!("\n{}", "Content".bold());
println!("{}", "─".repeat(40));
println!("{}", content);
}
Ok(())
}
async fn create_document(
title: &str,
project: &str,
content: Option<String>,
icon: Option<String>,
color: Option<String>,
) -> Result<()> {
let client = LinearClient::new()?;
let mut input = json!({
"title": title,
"projectId": project
});
if let Some(c) = content {
input["content"] = json!(c);
}
if let Some(i) = icon {
input["icon"] = json!(i);
}
if let Some(col) = color {
input["color"] = json!(col);
}
let mutation = r#"
mutation($input: DocumentCreateInput!) {
documentCreate(input: $input) {
success
document { id title url }
}
}
"#;
let result = client.mutate(mutation, Some(json!({ "input": input }))).await?;
if result["data"]["documentCreate"]["success"].as_bool() == Some(true) {
let document = &result["data"]["documentCreate"]["document"];
println!("{} Created document: {}", "+".green(), document["title"].as_str().unwrap_or(""));
println!(" ID: {}", document["id"].as_str().unwrap_or(""));
println!(" URL: {}", document["url"].as_str().unwrap_or(""));
} else {
anyhow::bail!("Failed to create document");
}
Ok(())
}
async fn update_document(
id: &str,
title: Option<String>,
content: Option<String>,
icon: Option<String>,
color: Option<String>,
project: Option<String>,
) -> Result<()> {
let client = LinearClient::new()?;
let mut input = json!({});
if let Some(t) = title {
input["title"] = json!(t);
}
if let Some(c) = content {
input["content"] = json!(c);
}
if let Some(i) = icon {
input["icon"] = json!(i);
}
if let Some(col) = color {
input["color"] = json!(col);
}
if let Some(p) = project {
input["projectId"] = json!(p);
}
if input.as_object().map(|o| o.is_empty()).unwrap_or(true) {
println!("No updates specified.");
return Ok(());
}
let mutation = r#"
mutation($id: String!, $input: DocumentUpdateInput!) {
documentUpdate(id: $id, input: $input) {
success
document { id title }
}
}
"#;
let result = client.mutate(mutation, Some(json!({ "id": id, "input": input }))).await?;
if result["data"]["documentUpdate"]["success"].as_bool() == Some(true) {
println!("{} Document updated", "+".green());
} else {
anyhow::bail!("Failed to update document");
}
Ok(())
}