wekan-cli 0.11.0

Agent-first, human-friendly command-line client for Wekan
Documentation
use super::formatting::{escape_terminal_controls, render_named_value, two_column_table};
use crate::command_result::{
    BoardCountSuccess, BoardCreateSuccess, BoardDeleteSuccess, BoardDetail, BoardListScope,
    BoardListSuccess, BoardRenameSuccess,
};

pub(super) fn render_label_added(data: &crate::command_result::BoardLabelAdded) -> String {
    format!(
        "Added or reused label {} on board {}.",
        escape_terminal_controls(&data.label_id),
        escape_terminal_controls(&data.board_id)
    )
}

pub(super) fn render_board_list(data: &BoardListSuccess) -> String {
    if data.boards.is_empty() {
        return match data.scope {
            BoardListScope::Active => "No active boards found.".to_owned(),
            BoardListScope::Public => "No public boards found.".to_owned(),
        };
    }
    let rows = data
        .boards
        .iter()
        .map(|board| {
            (
                escape_terminal_controls(&board.board_id),
                escape_terminal_controls(&board.title),
            )
        })
        .collect::<Vec<_>>();
    two_column_table("ID", "TITLE", &rows)
}

pub(super) fn render_board_count(data: &BoardCountSuccess) -> String {
    format!(
        "Private boards: {}\nPublic boards: {}",
        data.private, data.public
    )
}

pub(super) fn render_board_created(data: &BoardCreateSuccess) -> String {
    format!(
        "Created board {}.\nDefault swimlane ID: {}",
        escape_terminal_controls(&data.board_id),
        escape_terminal_controls(&data.default_swimlane_id)
    )
}

pub(super) fn render_board_renamed(data: &BoardRenameSuccess) -> String {
    format!(
        "Renamed board {} to {}.",
        escape_terminal_controls(&data.board_id),
        escape_terminal_controls(&data.title)
    )
}

pub(super) fn render_board_deleted(data: &BoardDeleteSuccess) -> String {
    format!(
        "Deleted board {}.",
        escape_terminal_controls(&data.board_id)
    )
}

pub(super) fn render_board_detail(data: &BoardDetail) -> String {
    let value = serde_json::to_value(data).expect("board documents are always serializable");
    let serde_json::Value::Object(mut fields) = value else {
        unreachable!("board documents serialize as objects")
    };
    fields.remove("board_id");
    fields.remove("title");

    let mut groups = [
        ("Core", Vec::new()),
        ("Sharing", Vec::new()),
        ("Appearance", Vec::new()),
        ("Dates and defaults", Vec::new()),
        ("Settings", Vec::new()),
    ];
    for (name, value) in fields {
        if value.is_null() {
            continue;
        }
        let group = board_field_group(&name);
        groups[group].1.push((name, value));
    }

    let mut lines = vec![
        format!("Board ID: {}", escape_terminal_controls(&data.board_id)),
        format!("Title: {}", escape_terminal_controls(&data.title)),
    ];
    for (heading, values) in groups {
        if values.is_empty() {
            continue;
        }
        lines.push(format!("{heading}:"));
        for (name, value) in values {
            render_named_value(&mut lines, 2, &name, &value);
        }
    }
    lines.join("\n")
}

pub(super) fn board_field_group(name: &str) -> usize {
    if matches!(
        name,
        "members" | "watchers" | "labels" | "orgs" | "teams" | "domains" | "import_usernames"
    ) {
        1
    } else if matches!(
        name,
        "color" | "custom_theme_colors" | "background_image_url" | "background_image_id"
    ) {
        2
    } else if name.contains("_at")
        || name.ends_with("_board_id")
        || name.ends_with("_list_id")
        || matches!(name, "spent_time" | "is_overtime")
    {
        3
    } else if name.starts_with("allows_")
        || name.starts_with("card_aging")
        || matches!(
            name,
            "show_dependencies" | "restrict_comment_editing" | "auto_width" | "present_parent_task"
        )
    {
        4
    } else {
        0
    }
}