codeberg_cli/actions/issue/
edit.rs1use crate::actions::GeneralArgs;
2use crate::actions::issue::display_issue;
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, multi_fuzzy_select_with_key};
7use crate::types::context::BergContext;
8use crate::types::git::OwnerRepo;
9use crate::types::output::OutputMode;
10use anyhow::Context;
11use forgejo_api::structs::{
12 EditIssueOption, Issue, IssueLabelsOption, IssueListIssuesQuery, IssueListLabelsQuery,
13 StateType,
14};
15use strum::{Display, VariantArray};
16
17use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
18
19use clap::Parser;
20
21#[derive(Parser, Debug)]
23pub struct EditIssueArgs {}
24
25#[derive(Display, PartialEq, Eq, VariantArray)]
26enum EditableFields {
27 Assignees,
28 Description,
29 State,
30 Title,
31 Labels,
32}
33
34impl EditIssueArgs {
35 pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
36 let ctx = BergContext::new(self, general_args).await?;
37 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
38 let issue = select_issue(&ctx).await?;
39 let issue_id = issue.number.context("Selected issue doesn't have an ID")?;
40
41 let (options, label_options) = create_options(&ctx, &issue).await?;
42
43 let updated_issue = ctx
44 .client
45 .issue_edit_issue(owner.as_str(), repo.as_str(), issue_id as u64, options)
46 .await?;
47
48 tracing::debug!("{updated_issue:?}");
49
50 let (_, updated_labels) = ctx
51 .client
52 .issue_replace_labels(
53 owner.as_str(),
54 repo.as_str(),
55 issue_id as u64,
56 label_options,
57 )
58 .await?;
59
60 match general_args.output_mode {
61 OutputMode::Pretty => {
62 tracing::debug!("{updated_labels:?}");
63 }
64 OutputMode::Json => updated_labels.print_json()?,
65 }
66
67 Ok(())
68 }
69}
70
71async fn select_issue(ctx: &BergContext<EditIssueArgs>) -> anyhow::Result<Issue> {
72 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
73 let (_, issues_list) = spin_until_ready(ctx.client.issue_list_issues(
74 owner.as_str(),
75 repo.as_str(),
76 IssueListIssuesQuery::default(),
77 ))
78 .await?;
79
80 if issues_list.is_empty() {
81 anyhow::bail!("No issues found in this repository");
82 }
83
84 fuzzy_select_with_key(&issues_list, select_prompt_for("issue"), display_issue).cloned()
85}
86
87async fn create_options(
88 ctx: &BergContext<EditIssueArgs>,
89 issue: &Issue,
90) -> anyhow::Result<(EditIssueOption, IssueLabelsOption)> {
91 let selected_update_fields = multi_fuzzy_select_with_key(
92 EditableFields::VARIANTS,
93 select_prompt_for("options"),
94 |_| false,
95 |f| f.to_string(),
96 )?;
97
98 let mut options = EditIssueOption {
99 assignee: None,
100 assignees: None,
101 body: None,
102 due_date: None,
103 milestone: None,
104 r#ref: None,
105 state: None,
106 title: None,
107 unset_due_date: None,
108 updated_at: None,
109 };
110
111 let mut label_options = IssueLabelsOption {
112 labels: None,
113 updated_at: None,
114 };
115
116 if selected_update_fields.contains(&&EditableFields::Assignees) {
117 let current_assignees = issue
118 .assignees
119 .as_ref()
120 .map(|users| users.iter().filter_map(|user| user.id).collect::<Vec<_>>());
121 options
122 .assignees
123 .replace(issue_assignees(ctx, current_assignees).await?);
124 }
125 if selected_update_fields.contains(&&EditableFields::Description) {
126 options
127 .body
128 .replace(issue_body(ctx, issue.body.clone()).await?);
129 }
130 if selected_update_fields.contains(&&EditableFields::State) {
131 options.state.replace(issue_state(ctx, issue.state).await?);
132 }
133 if selected_update_fields.contains(&&EditableFields::Title) {
134 options
135 .title
136 .replace(issue_title(ctx, issue.title.clone()).await?);
137 }
138 if selected_update_fields.contains(&&EditableFields::Labels) {
139 let current_labels = issue.labels.as_ref().map(|labels| {
140 labels
141 .iter()
142 .filter_map(|label| label.id)
143 .collect::<Vec<_>>()
144 });
145 label_options
146 .labels
147 .replace(issue_labels(ctx, current_labels).await?);
148 }
149
150 Ok((options, label_options))
151}
152
153async fn issue_assignees(
154 ctx: &BergContext<EditIssueArgs>,
155 current_assignees: Option<Vec<i64>>,
156) -> anyhow::Result<Vec<String>> {
157 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
158 let current_assignees = current_assignees.unwrap_or_default();
159 let (_, all_assignees) = ctx
160 .client
161 .repo_get_assignees(owner.as_str(), repo.as_str())
162 .await?;
163 let selected_assignees = multi_fuzzy_select_with_key(
164 &all_assignees,
165 select_prompt_for("assignees"),
166 |u| u.id.is_some_and(|id| current_assignees.contains(&id)),
167 |u| option_display(&u.login),
168 )?;
169
170 Ok(selected_assignees
171 .into_iter()
172 .filter_map(|u| u.login.as_ref().cloned())
173 .collect::<Vec<_>>())
174}
175
176async fn issue_body(
177 ctx: &BergContext<EditIssueArgs>,
178 current_body: Option<String>,
179) -> anyhow::Result<String> {
180 ctx.editor_for(
181 "a description",
182 current_body
183 .as_deref()
184 .unwrap_or("Enter an issue description"),
185 )
186}
187
188async fn issue_state(
189 _ctx: &BergContext<EditIssueArgs>,
190 _current_state: Option<StateType>,
191) -> anyhow::Result<String> {
192 let selected_state = fuzzy_select_with_key(
193 &[StateType::Open, StateType::Closed],
194 select_prompt_for("states"),
195 |f| format!("{f:?}"),
196 )?;
197 Ok(format!("{selected_state:?}"))
198}
199
200async fn issue_title(
201 _ctx: &BergContext<EditIssueArgs>,
202 current_title: Option<String>,
203) -> anyhow::Result<String> {
204 inquire::Text::new(input_prompt_for("Choose a new issue title").as_str())
205 .with_default(current_title.as_deref().unwrap_or("Enter an issue title"))
206 .prompt()
207 .map_err(anyhow::Error::from)
208}
209
210async fn issue_labels(
211 ctx: &BergContext<EditIssueArgs>,
212 current_labels: Option<Vec<i64>>,
213) -> anyhow::Result<Vec<serde_json::Value>> {
214 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
215 let current_labels = current_labels.unwrap_or_default();
216 let (_, all_labels) = ctx
217 .client
218 .issue_list_labels(
219 owner.as_str(),
220 repo.as_str(),
221 IssueListLabelsQuery::default(),
222 )
223 .await?;
224
225 let selected_labels = multi_fuzzy_select_with_key(
226 &all_labels,
227 select_prompt_for("labels"),
228 |l| l.id.is_some_and(|id| current_labels.contains(&id)),
229 |l| option_display(&l.name),
230 )?;
231
232 let label_ids = selected_labels
233 .iter()
234 .filter_map(|l| l.id)
235 .map(|x| serde_json::json!(x))
236 .collect::<Vec<_>>();
237
238 Ok(label_ids)
239}