Skip to main content

ytcli/render/
bulk.rs

1//! The answer to a change that touched more than one issue.
2//!
3//! A bulk change is one request that ends in a count, so the tally rule applies
4//! to it directly: `changed N of M`. What it cannot say on its own is *which*
5//! ones, and that is only worth asking for when the counts do not already
6//! answer it — printing a line per issue that succeeded would spend the saving
7//! the command exists to make.
8
9use std::fmt::Write as _;
10
11use crate::api::{BulkChange, BulkOutcome};
12use crate::render::Context;
13use crate::render::style::Palette;
14use crate::render::table::{Column, render};
15
16/// The tally a change to several issues ends with, whichever way it was made.
17///
18/// One request or fifty, the caller is owed the same sentence: how many of the
19/// issues they named actually changed.
20#[must_use]
21pub fn changed(done: u64, total: u64, ctx: &Context) -> String {
22    let colour = if done == total {
23        Palette::label()
24    } else {
25        Palette::warn()
26    };
27    format!(
28        "{}\n",
29        ctx.painter()
30            .paint(&format!("changed {done} of {total}"), colour)
31    )
32}
33
34/// The tally, and the id that outlives the command.
35///
36/// The id is printed on every outcome rather than only on failure: it is the
37/// only handle on work Tracker is still doing, and a caller who did not keep it
38/// has no way back to the answer.
39#[must_use]
40pub fn change(change: &BulkChange, ctx: &Context) -> String {
41    let paint = ctx.painter();
42    let mut out = String::with_capacity(96);
43
44    let counted = match (change.done, change.total) {
45        (Some(done), Some(total)) => format!("changed {done} of {total}"),
46        // Before Tracker has counted the issues there is no tally to print, and
47        // inventing one from the keys we sent would be our number, not its.
48        _ => format!("{} — not counted yet", change.status.to_lowercase()),
49    };
50
51    let colour = if change.succeeded() {
52        Palette::label()
53    } else {
54        Palette::warn()
55    };
56    let _ = writeln!(
57        out,
58        "{}  {}",
59        paint.paint(&counted, colour),
60        paint.paint(&format!("bulkchange {}", change.id), Palette::key())
61    );
62
63    // Tracker's own sentence, kept whenever it is saying something other than
64    // "fine": it is in the organisation's language and is the only wording that
65    // will match what the web interface shows.
66    if !change.succeeded() && !change.status_text.is_empty() {
67        let _ = writeln!(out, "{}", paint.paint(&change.status_text, Palette::warn()));
68    }
69    out
70}
71
72/// One line per issue that did not change, and Tracker's reason for each.
73///
74/// Only the failures: the ones that worked are in the tally, and repeating them
75/// would make the output grow with the size of the change.
76#[must_use]
77pub fn failures(outcomes: &[BulkOutcome], ctx: &Context) -> String {
78    let rows: Vec<Vec<String>> = outcomes
79        .iter()
80        .filter(|outcome| outcome.status != "COMPLETE")
81        .map(|outcome| {
82            vec![
83                outcome.key.clone(),
84                outcome.status.to_lowercase(),
85                outcome.error.clone().unwrap_or_else(|| "-".to_owned()),
86            ]
87        })
88        .collect();
89
90    if rows.is_empty() {
91        return String::new();
92    }
93
94    render(
95        &[
96            Column::whole("KEY", 14, Palette::key()),
97            Column::whole("STATUS", 10, Palette::warn()),
98            Column::new("WHY", 48, anstyle::Style::new()),
99        ],
100        &rows,
101        ctx,
102    )
103}