codeberg_cli/actions/issue/
view.rs1use crate::render::comment::render_comment;
2use crate::render::datetime::render_datetime_and_info;
3use crate::render::json::JsonToStdout;
4use crate::render::option::option_display;
5use crate::render::spinner::spin_until_ready;
6use crate::render::ui::fuzzy_select_with_key;
7use crate::types::api::state_type::ViewStateType;
8use crate::types::context::BergContext;
9use crate::types::output::OutputMode;
10use crate::{actions::GlobalArgs, types::git::OwnerRepo};
11use anyhow::Context;
12use forgejo_api::structs::{
13 Issue, IssueGetCommentsQuery, IssueListIssuesQuery, IssueListIssuesQueryState,
14};
15
16use crate::actions::text_manipulation::select_prompt_for;
17
18use super::display_issue;
19
20use clap::Parser;
21
22#[derive(Parser, Debug)]
24pub struct ViewIssueArgs {
25 pub index: Option<i64>,
27
28 #[arg(short, long, value_enum, default_value_t = ViewStateType::All)]
30 pub state: ViewStateType,
31
32 #[arg(short, long)]
34 pub comments: bool,
35}
36
37impl ViewIssueArgs {
38 pub async fn run(self, global_args: GlobalArgs) -> anyhow::Result<()> {
39 let ctx = BergContext::new(self, global_args).await?;
40 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
41 let state = match ctx.args.state {
42 ViewStateType::Closed => IssueListIssuesQueryState::Closed,
43 ViewStateType::Open => IssueListIssuesQueryState::Open,
44 ViewStateType::All => IssueListIssuesQueryState::All,
45 };
46 let selected_issue = if let Some(index) = ctx.args.index {
47 ctx.client
48 .issue_get_issue(owner.as_str(), repo.as_str(), index)
49 .await?
50 } else {
51 if ctx.global_args.non_interactive {
52 anyhow::bail!("non-interactive mode enabled. You have to specify an issue ID");
53 };
54 let (_, issues_list) = spin_until_ready(
55 ctx.client
56 .issue_list_issues(
57 owner.as_str(),
58 repo.as_str(),
59 IssueListIssuesQuery {
60 state: Some(state),
61 ..Default::default()
62 },
63 )
64 .send(),
65 )
66 .await?;
67
68 fuzzy_select_with_key(&issues_list, select_prompt_for("issue"), display_issue)
69 .cloned()?
70 };
71
72 match ctx.global_args.output_mode {
73 OutputMode::Pretty => {
74 if ctx.args.comments {
75 spin_until_ready(present_issue_comments(&ctx, &selected_issue)).await?;
76 } else {
77 present_issue_overview(&ctx, &selected_issue);
78 }
79 }
80 OutputMode::Json => {
81 if ctx.args.comments {
82 let index = selected_issue
83 .number
84 .context("Selected issue has to have a valid issue number")?;
85 let (_, comments) = ctx
86 .client
87 .issue_get_comments(
88 owner.as_str(),
89 repo.as_str(),
90 index,
91 IssueGetCommentsQuery::default(),
92 )
93 .send()
94 .await?;
95 comments.print_json()?;
96 } else {
97 selected_issue.print_json()?;
98 }
99 }
100 }
101
102 Ok(())
103 }
104}
105
106fn present_issue_overview(ctx: &BergContext<ViewIssueArgs>, issue: &Issue) {
107 let days_passed_since_creation =
108 option_display(&issue.created_at.as_ref().map(render_datetime_and_info));
109
110 let mut table = ctx.make_table();
111
112 table
113 .set_header(vec![option_display(
114 &issue.id.as_ref().map(|id| format!("Issue #{}", id)),
115 )])
116 .add_row(vec![String::from("Title"), option_display(&issue.title)])
117 .add_row(vec![String::from("Created"), days_passed_since_creation])
118 .add_row(vec![
119 String::from("Labels"),
120 option_display(&issue.labels.as_ref().map(|labels| {
121 labels
122 .iter()
123 .map(|label| option_display(&label.name))
124 .collect::<Vec<_>>()
125 .join(", ")
126 })),
127 ])
128 .add_row(vec![
129 String::from("Description"),
130 option_display(&issue.body),
131 ]);
132
133 println!("{table}", table = table.show());
134}
135
136async fn present_issue_comments(
137 ctx: &BergContext<ViewIssueArgs>,
138 issue: &Issue,
139) -> anyhow::Result<()> {
140 let index = issue.number.context("Issue has no issue number")?;
141 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
142 let (header, comments) = {
143 let (_, comments_list) = ctx
144 .client
145 .issue_get_comments(
146 owner.as_str(),
147 repo.as_str(),
148 index,
149 IssueGetCommentsQuery::default(),
150 )
151 .send()
152 .await?;
153 let header = format!(
154 "Issue #{} {}",
155 index,
156 if comments_list.is_empty() {
157 "(no comments)"
158 } else {
159 "comments"
160 }
161 );
162 (header, comments_list)
163 };
164
165 let mut table = ctx.make_table();
166
167 table
168 .add_row(vec![header])
169 .add_rows(comments.into_iter().filter_map(|comment| {
170 let username = comment.user.as_ref()?.login.as_ref()?.as_str();
171 let creation_time = comment.created_at.as_ref()?;
172 let comment = comment.body.as_ref()?;
173 let comment = render_comment(&ctx.config, username, creation_time, comment);
174 Some(vec![comment])
175 }));
176
177 println!("{table}", table = table.show());
178
179 Ok(())
180}