radicle_cli/terminal/
issue.rs1use std::io;
2
3use radicle_term::table::TableOptions;
4use radicle_term::{Table, VStack};
5
6use radicle::cob;
7use radicle::cob::issue;
8use radicle::cob::issue::CloseReason;
9use radicle::Profile;
10
11use crate::terminal as term;
12use crate::terminal::format::Author;
13use crate::terminal::Element;
14
15pub const OPEN_MSG: &str = r#"
16<!--
17Please enter an issue title and description.
18
19The first line is the issue title. The issue description
20follows, and must be separated by a blank line, just
21like a commit message. Markdown is supported in the title
22and description.
23-->
24"#;
25
26#[derive(Default, Debug, PartialEq, Eq)]
28pub enum Format {
29 #[default]
30 Full,
31 Header,
32}
33
34pub fn get_title_description(
35 title: Option<String>,
36 description: Option<String>,
37) -> io::Result<Option<(String, String)>> {
38 term::patch::Message::edit_title_description(title, description, OPEN_MSG)
39}
40
41pub fn show(
42 issue: &issue::Issue,
43 id: &cob::ObjectId,
44 format: Format,
45 profile: &Profile,
46) -> anyhow::Result<()> {
47 let labels: Vec<String> = issue.labels().cloned().map(|t| t.into()).collect();
48 let assignees: Vec<String> = issue
49 .assignees()
50 .map(|a| term::format::did(a).to_string())
51 .collect();
52 let author = issue.author();
53 let did = author.id();
54 let author = Author::new(did, profile);
55
56 let mut attrs = Table::<2, term::Line>::new(TableOptions {
57 spacing: 2,
58 ..TableOptions::default()
59 });
60
61 attrs.push([
62 term::format::tertiary("Title".to_owned()).into(),
63 term::format::bold(issue.title().to_owned()).into(),
64 ]);
65
66 attrs.push([
67 term::format::tertiary("Issue".to_owned()).into(),
68 term::format::bold(id.to_string()).into(),
69 ]);
70
71 attrs.push([
72 term::format::tertiary("Author".to_owned()).into(),
73 author.line(),
74 ]);
75
76 if !labels.is_empty() {
77 attrs.push([
78 term::format::tertiary("Labels".to_owned()).into(),
79 term::format::secondary(labels.join(", ")).into(),
80 ]);
81 }
82
83 if !assignees.is_empty() {
84 attrs.push([
85 term::format::tertiary("Assignees".to_owned()).into(),
86 term::format::dim(assignees.join(", ")).into(),
87 ]);
88 }
89
90 attrs.push([
91 term::format::tertiary("Status".to_owned()).into(),
92 match issue.state() {
93 issue::State::Open => term::format::positive("open".to_owned()).into(),
94 issue::State::Closed {
95 reason: CloseReason::Solved,
96 } => term::Line::spaced([
97 term::format::negative("closed").into(),
98 term::format::negative("(solved)").italic().dim().into(),
99 ]),
100 issue::State::Closed {
101 reason: CloseReason::Other,
102 } => term::Line::spaced([term::format::negative("closed").into()]),
103 },
104 ]);
105
106 let description = issue.description();
107 let mut widget = VStack::default()
108 .border(Some(term::colors::FAINT))
109 .child(attrs)
110 .children(if !description.is_empty() {
111 vec![
112 term::Label::blank().boxed(),
113 term::textarea(description.trim()).wrap(60).boxed(),
114 ]
115 } else {
116 vec![]
117 });
118
119 if format == Format::Full {
120 for (id, comment) in issue.replies() {
121 let hstack = term::comment::header(id, comment, profile);
122
123 widget = widget.divider();
124 widget.push(hstack);
125 widget.push(term::textarea(comment.body()).wrap(60));
126 }
127 }
128 widget.print();
129
130 Ok(())
131}