use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::atlassian::client::{AgileBoardList, JiraSearchResult};
use crate::cli::atlassian::helpers::create_client;
#[derive(Parser)]
pub struct BoardCommand {
#[command(subcommand)]
pub command: BoardSubcommands,
}
#[derive(Subcommand)]
pub enum BoardSubcommands {
List(ListCommand),
Issues(IssuesCommand),
}
impl BoardCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
BoardSubcommands::List(cmd) => cmd.execute().await,
BoardSubcommands::Issues(cmd) => cmd.execute().await,
}
}
}
#[derive(Parser)]
pub struct ListCommand {
#[arg(long)]
pub project: Option<String>,
#[arg(long, value_name = "TYPE")]
pub r#type: Option<String>,
#[arg(long, default_value_t = 50)]
pub limit: u32,
}
impl ListCommand {
pub async fn execute(self) -> Result<()> {
let (client, _instance_url) = create_client()?;
let result = client
.get_boards(self.project.as_deref(), self.r#type.as_deref(), self.limit)
.await?;
print_boards(&result);
Ok(())
}
}
#[derive(Parser)]
pub struct IssuesCommand {
#[arg(long)]
pub board_id: u64,
#[arg(long)]
pub jql: Option<String>,
#[arg(long, default_value_t = 50)]
pub limit: u32,
}
impl IssuesCommand {
pub async fn execute(self) -> Result<()> {
let (client, _instance_url) = create_client()?;
let result = client
.get_board_issues(self.board_id, self.jql.as_deref(), self.limit)
.await?;
print_board_issues(&result);
Ok(())
}
}
fn print_boards(result: &AgileBoardList) {
if result.boards.is_empty() {
println!("No boards found.");
return;
}
let id_width = result
.boards
.iter()
.map(|b| b.id.to_string().len())
.max()
.unwrap_or(2)
.max(2);
let type_width = result
.boards
.iter()
.map(|b| b.board_type.len())
.max()
.unwrap_or(4)
.max(4);
let proj_width = result
.boards
.iter()
.filter_map(|b| b.project_key.as_ref().map(String::len))
.max()
.unwrap_or(7)
.max(7);
println!(
"{:<id_width$} {:<type_width$} {:<proj_width$} NAME",
"ID", "TYPE", "PROJECT"
);
let name_sep = "-".repeat(4);
println!(
"{:<id_width$} {:<type_width$} {:<proj_width$} {name_sep}",
"-".repeat(id_width),
"-".repeat(type_width),
"-".repeat(proj_width),
);
for board in &result.boards {
let proj = board.project_key.as_deref().unwrap_or("-");
println!(
"{:<id_width$} {:<type_width$} {:<proj_width$} {}",
board.id, board.board_type, proj, board.name
);
}
if result.total > result.boards.len() as u32 {
println!(
"\nShowing {} of {} boards.",
result.boards.len(),
result.total
);
}
}
fn print_board_issues(result: &JiraSearchResult) {
if result.issues.is_empty() {
println!("No issues found.");
return;
}
let key_width = result
.issues
.iter()
.map(|i| i.key.len())
.max()
.unwrap_or(3)
.max(3);
let status_width = result
.issues
.iter()
.filter_map(|i| i.status.as_ref().map(String::len))
.max()
.unwrap_or(6)
.max(6);
let assignee_width = result
.issues
.iter()
.filter_map(|i| i.assignee.as_ref().map(String::len))
.max()
.unwrap_or(8)
.max(8);
let summary_sep = "-".repeat(7);
println!(
"{:<key_width$} {:<status_width$} {:<assignee_width$} SUMMARY",
"KEY", "STATUS", "ASSIGNEE"
);
println!(
"{:<key_width$} {:<status_width$} {:<assignee_width$} {summary_sep}",
"-".repeat(key_width),
"-".repeat(status_width),
"-".repeat(assignee_width),
);
for issue in &result.issues {
let status = issue.status.as_deref().unwrap_or("-");
let assignee = issue.assignee.as_deref().unwrap_or("-");
println!(
"{:<key_width$} {:<status_width$} {:<assignee_width$} {}",
issue.key, status, assignee, issue.summary
);
}
if result.total > result.issues.len() as u32 {
println!(
"\nShowing {} of {} issues.",
result.issues.len(),
result.total
);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::atlassian::client::{AgileBoard, JiraIssue};
fn sample_board(id: u64, name: &str, board_type: &str, project: Option<&str>) -> AgileBoard {
AgileBoard {
id,
name: name.to_string(),
board_type: board_type.to_string(),
project_key: project.map(String::from),
}
}
fn sample_issue(
key: &str,
summary: &str,
status: Option<&str>,
assignee: Option<&str>,
) -> JiraIssue {
JiraIssue {
key: key.to_string(),
summary: summary.to_string(),
description_adf: None,
status: status.map(String::from),
issue_type: None,
assignee: assignee.map(String::from),
priority: None,
labels: vec![],
}
}
#[test]
fn print_boards_empty() {
let result = AgileBoardList {
boards: vec![],
total: 0,
};
print_boards(&result);
}
#[test]
fn print_boards_with_data() {
let result = AgileBoardList {
boards: vec![
sample_board(1, "PROJ Board", "scrum", Some("PROJ")),
sample_board(2, "Kanban", "kanban", None),
],
total: 2,
};
print_boards(&result);
}
#[test]
fn print_boards_with_pagination() {
let result = AgileBoardList {
boards: vec![sample_board(1, "Board", "scrum", Some("PROJ"))],
total: 100,
};
print_boards(&result);
}
#[test]
fn print_board_issues_empty() {
let result = JiraSearchResult {
issues: vec![],
total: 0,
};
print_board_issues(&result);
}
#[test]
fn print_board_issues_with_data() {
let result = JiraSearchResult {
issues: vec![
sample_issue("PROJ-1", "Fix login", Some("Open"), Some("Alice")),
sample_issue("PROJ-2", "Add feature", None, None),
],
total: 2,
};
print_board_issues(&result);
}
#[test]
fn print_board_issues_with_pagination() {
let result = JiraSearchResult {
issues: vec![sample_issue("PROJ-1", "Issue", Some("Open"), None)],
total: 50,
};
print_board_issues(&result);
}
#[test]
fn board_command_list_variant() {
let cmd = BoardCommand {
command: BoardSubcommands::List(ListCommand {
project: None,
r#type: None,
limit: 50,
}),
};
assert!(matches!(cmd.command, BoardSubcommands::List(_)));
}
#[test]
fn board_command_issues_variant() {
let cmd = BoardCommand {
command: BoardSubcommands::Issues(IssuesCommand {
board_id: 1,
jql: None,
limit: 50,
}),
};
assert!(matches!(cmd.command, BoardSubcommands::Issues(_)));
}
#[test]
fn list_command_with_filters() {
let cmd = ListCommand {
project: Some("PROJ".to_string()),
r#type: Some("scrum".to_string()),
limit: 25,
};
assert_eq!(cmd.project.as_deref(), Some("PROJ"));
assert_eq!(cmd.r#type.as_deref(), Some("scrum"));
}
#[test]
fn issues_command_with_jql() {
let cmd = IssuesCommand {
board_id: 42,
jql: Some("status = Open".to_string()),
limit: 10,
};
assert_eq!(cmd.board_id, 42);
assert_eq!(cmd.jql.as_deref(), Some("status = Open"));
}
}