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