xbp 10.40.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
//! Interactive Linear issues hub.

use super::bulk::run_bulk_interactive;
use super::comments::{list_comments, run_comment};
use super::issues::{
    get_issue, list_issues, print_issue_detail, print_issue_table, run_assign, run_create,
    run_labels, run_priority, run_status, ListIssuesFilter,
};
use super::require_interactive;
use crate::cli::commands::{
    LinearAssignCmd, LinearCommentCmd, LinearCreateCmd, LinearLabelsCmd, LinearPriorityCmd,
    LinearStatusCmd,
};
use crate::cli::ui::Loader;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, FuzzySelect, Input, Select};

pub async fn run_hub(api_key: &str) -> Result<(), String> {
    require_interactive("Linear interactive hub")?;
    loop {
        println!();
        println!("{}", "Linear issues".bright_cyan().bold());
        let choices = [
            "List issues",
            "Bulk edit (multi-select)",
            "Create issue",
            "Open issue by ID",
            "Scan TODOs (xbp issues)",
            "Exit",
        ];
        let idx = Select::with_theme(&ColorfulTheme::default())
            .with_prompt("What do you want to do?")
            .items(&choices)
            .default(0)
            .interact()
            .map_err(|e| e.to_string())?;

        match idx {
            0 => list_and_act(api_key).await?,
            1 => run_bulk_interactive(api_key).await?,
            2 => {
                run_create(
                    api_key,
                    LinearCreateCmd {
                        title: None,
                        team: None,
                        description: None,
                        priority: None,
                        state: None,
                        assignee: None,
                        labels: Vec::new(),
                        interactive_body: true,
                    },
                )
                .await?;
            }
            3 => {
                let id: String = Input::with_theme(&ColorfulTheme::default())
                    .with_prompt("Issue ID (e.g. XLX-29)")
                    .interact_text()
                    .map_err(|e| e.to_string())?;
                if !id.trim().is_empty() {
                    open_issue_actions(api_key, id.trim()).await?;
                }
            }
            4 => {
                println!(
                    "{}",
                    "Run `xbp issues scan` then `xbp issues sync --to linear`.".dimmed()
                );
                let _ = crate::commands::todos::run_todos_scan_default().await;
            }
            _ => break,
        }
    }
    Ok(())
}

async fn list_and_act(api_key: &str) -> Result<(), String> {
    let loader = Loader::start("Fetching Linear issues");
    let issues = match list_issues(
        api_key,
        ListIssuesFilter {
            limit: 40,
            ..Default::default()
        },
    )
    .await
    {
        Ok(v) => {
            loader.success();
            v
        }
        Err(e) => {
            loader.fail(&e);
            return Err(e);
        }
    };
    if issues.is_empty() {
        println!("{}", "No issues found.".dimmed());
        return Ok(());
    }
    print_issue_table(&issues);
    let labels: Vec<String> = issues
        .iter()
        .map(|i| format!("{}{}", i.identifier, i.title))
        .collect();
    let mut items = labels;
    items.push("(back)".to_string());
    let idx = FuzzySelect::with_theme(&ColorfulTheme::default())
        .with_prompt("Open issue")
        .items(&items)
        .default(0)
        .interact()
        .map_err(|e| e.to_string())?;
    if idx >= issues.len() {
        return Ok(());
    }
    open_issue_actions(api_key, &issues[idx].identifier).await
}

async fn open_issue_actions(api_key: &str, id: &str) -> Result<(), String> {
    loop {
        let issue = get_issue(api_key, id).await?;
        print_issue_detail(&issue);
        let actions = [
            "Change status",
            "Change priority",
            "Edit labels",
            "Assign",
            "View comments",
            "Add comment",
            "Back",
        ];
        let idx = Select::with_theme(&ColorfulTheme::default())
            .with_prompt(format!("Actions for {}", issue.identifier))
            .items(&actions)
            .default(0)
            .interact()
            .map_err(|e| e.to_string())?;
        match idx {
            0 => {
                run_status(
                    api_key,
                    LinearStatusCmd {
                        id: issue.identifier.clone(),
                        state: None,
                    },
                )
                .await?;
            }
            1 => {
                run_priority(
                    api_key,
                    LinearPriorityCmd {
                        id: issue.identifier.clone(),
                        priority: None,
                    },
                )
                .await?;
            }
            2 => {
                run_labels(
                    api_key,
                    LinearLabelsCmd {
                        id: issue.identifier.clone(),
                        add: Vec::new(),
                        remove: Vec::new(),
                        set: Vec::new(),
                        clear: false,
                    },
                )
                .await?;
            }
            3 => {
                run_assign(
                    api_key,
                    LinearAssignCmd {
                        id: issue.identifier.clone(),
                        assignee: None,
                    },
                )
                .await?;
            }
            4 => {
                let comments = list_comments(api_key, &issue.identifier).await?;
                if comments.is_empty() {
                    println!("{}", "No comments.".dimmed());
                } else {
                    for c in comments {
                        println!(
                            "{} {}{}",
                            c.user_name.as_deref().unwrap_or("?").bright_cyan(),
                            c.created_at.as_deref().unwrap_or("").dimmed(),
                            c.body.replace('\n', " ")
                        );
                    }
                }
            }
            5 => {
                run_comment(
                    api_key,
                    LinearCommentCmd {
                        id: issue.identifier.clone(),
                        body: None,
                    },
                )
                .await?;
            }
            _ => break,
        }
    }
    Ok(())
}