codeberg_cli/actions/issue/
create.rs1use crate::actions::GeneralArgs;
2use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
3use crate::types::context::BergContext;
4use crate::types::git::OwnerRepo;
5use anyhow::Context;
6use forgejo_api::structs::{CreateIssueOption, IssueGetMilestonesListQuery, IssueListLabelsQuery};
7use strum::*;
8
9use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
10
11use clap::Parser;
12
13#[derive(Parser, Debug)]
15pub struct CreateIssueArgs {
16 #[arg(short, long)]
18 pub title: Option<String>,
19
20 #[arg(short, long, value_name = "LABEL,...", value_delimiter = ',')]
22 pub labels: Option<Vec<String>>,
23
24 #[arg(id = "description", short, long)]
26 pub body: Option<String>,
27
28 #[arg(short, long, value_name = "ASSIGNEE,...", value_delimiter = ',')]
30 pub assignees: Option<Vec<String>>,
31
32 #[arg(short, long)]
34 pub milestone: Option<String>,
35}
36
37#[derive(Display, PartialEq, Eq, VariantArray)]
38enum CreatableFields {
39 Labels,
40 Assignees,
41 Milestone,
42}
43
44impl CreateIssueArgs {
45 pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
46 let _ = general_args;
47 let ctx = BergContext::new(self, general_args).await?;
48 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
49 let options = create_options(&ctx).await?;
50 tracing::debug!("{options:?}");
51 let issue = ctx
52 .client
53 .issue_create_issue(owner.as_str(), repo.as_str(), options)
54 .await?;
55 tracing::debug!("{issue:?}");
56 Ok(())
57 }
58}
59
60async fn create_options(ctx: &BergContext<CreateIssueArgs>) -> anyhow::Result<CreateIssueOption> {
61 let title = match ctx.args.title.as_ref().cloned() {
62 Some(title) => title,
63 None => inquire::Text::new(input_prompt_for("Issue Title").as_str()).prompt()?,
64 };
65
66 let mut options = CreateIssueOption {
67 title,
68 assignee: None,
69 assignees: None,
70 body: None,
71 closed: None,
72 due_date: None,
73 labels: None,
74 milestone: None,
75 r#ref: None,
76 };
77
78 let optional_data = {
79 use CreatableFields::*;
80 [
81 (Labels, ctx.args.labels.is_none()),
82 (Assignees, ctx.args.assignees.is_none()),
83 (Milestone, ctx.args.milestone.is_none()),
84 ]
85 .into_iter()
86 .filter_map(|(name, missing)| missing.then_some(name))
87 .collect::<Vec<_>>()
88 };
89
90 let chosen_optionals = multi_fuzzy_select_with_key(
91 &optional_data,
92 "Choose optional properties",
93 |_| false,
94 |o| o.to_string(),
95 )?;
96
97 options.body.replace(issue_body(ctx)?);
98 {
99 use CreatableFields::*;
100 options.labels = issue_labels(ctx, chosen_optionals.contains(&&Labels)).await?;
101 options.assignees = issue_assignees(ctx, chosen_optionals.contains(&&Assignees)).await?;
102 options.milestone = issue_milestone(ctx, chosen_optionals.contains(&&Milestone)).await?;
103 }
104
105 Ok(options)
106}
107
108fn issue_body(ctx: &BergContext<CreateIssueArgs>) -> anyhow::Result<String> {
109 let body = match ctx.args.body.as_ref() {
110 Some(body) => body.clone(),
111 None => ctx.editor_for("a description", "Enter a issue description")?,
112 };
113 Ok(body)
114}
115
116async fn issue_labels(
117 ctx: &BergContext<CreateIssueArgs>,
118 interactive: bool,
119) -> anyhow::Result<Option<Vec<i64>>> {
120 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
121
122 let all_labels = ctx
123 .client
124 .issue_list_labels(
125 owner.as_str(),
126 repo.as_str(),
127 IssueListLabelsQuery::default(),
128 )
129 .await?;
130
131 let labels = match &ctx.args.labels {
132 Some(label_names) => {
133 let label_ids = all_labels
134 .iter()
135 .filter(|l| {
136 l.name
137 .as_ref()
138 .is_some_and(|name| label_names.contains(name))
139 })
140 .filter_map(|l| l.id)
141 .collect::<Vec<_>>();
142 Some(label_ids)
143 }
144 None => {
145 if !interactive {
146 return Ok(None);
147 }
148
149 let selected_labels = multi_fuzzy_select_with_key(
150 &all_labels,
151 select_prompt_for("labels"),
152 |_| false,
153 |l| {
154 l.name
155 .as_ref()
156 .cloned()
157 .unwrap_or_else(|| String::from("???"))
158 },
159 )?;
160
161 let label_ids = selected_labels
162 .iter()
163 .filter_map(|l| l.id)
164 .collect::<Vec<_>>();
165
166 Some(label_ids)
167 }
168 };
169 Ok(labels)
170}
171
172async fn issue_assignees(
173 ctx: &BergContext<CreateIssueArgs>,
174 interactive: bool,
175) -> anyhow::Result<Option<Vec<String>>> {
176 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
177
178 let all_assignees = ctx
179 .client
180 .repo_get_assignees(owner.as_str(), repo.as_str())
181 .await?;
182 let assignees = match &ctx.args.assignees {
183 Some(assignees_names) => Some(assignees_names.clone()),
184 None => {
185 tracing::debug!("Hey");
186 if !interactive {
187 return Ok(None);
188 }
189
190 let selected_assignees = multi_fuzzy_select_with_key(
191 &all_assignees,
192 select_prompt_for("assignees"),
193 |_| false,
194 |u| {
195 u.login
196 .as_ref()
197 .cloned()
198 .unwrap_or_else(|| String::from("???"))
199 },
200 )?;
201
202 Some(
203 selected_assignees
204 .into_iter()
205 .filter_map(|u| u.login.as_ref().cloned())
206 .collect::<Vec<_>>(),
207 )
208 }
209 };
210
211 Ok(assignees)
212}
213
214async fn issue_milestone(
215 ctx: &BergContext<CreateIssueArgs>,
216 interactive: bool,
217) -> anyhow::Result<Option<i64>> {
218 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
219
220 let all_milestones = ctx
221 .client
222 .issue_get_milestones_list(
223 owner.as_str(),
224 repo.as_str(),
225 IssueGetMilestonesListQuery::default(),
226 )
227 .await?;
228
229 let milestone = match &ctx.args.milestone {
230 Some(milestone_name) => Some(
231 all_milestones
232 .iter()
233 .find(|m| m.title.as_ref().is_some_and(|name| name == milestone_name))
234 .and_then(|milestone| milestone.id)
235 .context(format!(
236 "Milestone with name {milestone_name} wasn't found. Check the spelling"
237 ))?,
238 ),
239 None => {
240 if !interactive {
241 return Ok(None);
242 }
243 let selected_milestone =
244 fuzzy_select_with_key(&all_milestones, select_prompt_for("milestones"), |m| {
245 m.title
246 .as_ref()
247 .cloned()
248 .unwrap_or_else(|| String::from("???"))
249 })?;
250
251 Some(
252 selected_milestone
253 .id
254 .context("Selected milestone's id is missing")?,
255 )
256 }
257 };
258
259 Ok(milestone)
260}