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}