1use std::sync::Arc;
2
3use console::style;
4
5use dialoguer::theme::ColorfulTheme;
6use dialoguer::Confirm;
7use dialoguer::Editor;
8use dialoguer::FuzzySelect;
9use dialoguer::Input;
10
11use crate::cmds::merge_request::MergeRequestBodyArgs;
12use crate::cmds::project::Member;
13use crate::config::ConfigProperties;
14use crate::error;
15use crate::Result;
16
17#[derive(Builder)]
18pub struct MergeRequestUserInput {
19 pub title: String,
20 pub description: String,
21 pub assignee: Member,
22 #[builder(default)]
23 pub reviewer: Member,
24}
25
26impl MergeRequestUserInput {
27 pub fn builder() -> MergeRequestUserInputBuilder {
28 MergeRequestUserInputBuilder::default()
29 }
30
31 pub fn new(title: &str, description: &str, user_id: i64, username: &str) -> Self {
32 MergeRequestUserInput {
33 title: title.to_string(),
34 description: description.to_string(),
35 assignee: Member::builder()
36 .id(user_id)
37 .username(username.to_string())
38 .build()
39 .unwrap(),
40 reviewer: Member::default(),
41 }
42 }
43}
44
45struct MemberSelector {
46 members: Vec<Member>,
47}
48
49impl MemberSelector {
50 pub fn new(members: Vec<Member>) -> Self {
51 Self { members }
52 }
53
54 pub fn prepare_assignee_list(
59 &self,
60 cli_assignee: Option<&Member>,
61 config_assignee: Option<Member>,
62 ) -> Vec<Member> {
63 let mut selection_list = self.members.clone();
65
66 match (cli_assignee, config_assignee) {
67 (Some(cli), _) => {
68 selection_list.insert(0, cli.clone());
70 selection_list.insert(1, Member::default()); }
72 (None, Some(config)) => {
73 selection_list.insert(0, config);
75 selection_list.insert(1, Member::default()); }
77 (None, None) => {
78 selection_list.insert(0, Member::default());
80 }
81 }
82
83 selection_list
84 }
85
86 pub fn prepare_reviewer_list(
88 &self,
89 default_cli_reviewer: Option<&Member>,
90 assigned_member: &Member,
91 ) -> Vec<Member> {
92 let mut selection_list = if default_cli_reviewer.is_some() {
93 vec![default_cli_reviewer.unwrap().clone(), Member::default()]
94 } else {
95 vec![Member::default()]
96 };
97 selection_list.extend(
98 self.members
99 .iter()
100 .filter(|m| m != &assigned_member)
101 .cloned(),
102 );
103 selection_list
104 }
105}
106
107pub fn prompt_user_merge_request_info(
109 default_title: &str,
110 default_description: &str,
111 default_cli_assignee: Option<&Member>,
112 default_cli_reviewer: Option<&Member>,
113 config: &Arc<dyn ConfigProperties>,
114) -> Result<MergeRequestUserInput> {
115 let (title, description) = prompt_user_title_description(default_title, default_description);
116
117 let selector = MemberSelector::new(config.merge_request_members());
119
120 let assignee_list =
122 selector.prepare_assignee_list(default_cli_assignee, config.preferred_assignee_username());
123
124 let assignee_index = gather_member(&assignee_list, "Assignee:");
126 let assigned_member = assignee_list[assignee_index].clone();
127
128 let reviewer_list = selector.prepare_reviewer_list(default_cli_reviewer, &assigned_member);
130 let reviewer_index = gather_member(&reviewer_list, "Reviewer:");
131
132 Ok(MergeRequestUserInput::builder()
133 .title(title)
134 .description(description)
135 .assignee(assigned_member)
136 .reviewer(reviewer_list[reviewer_index].clone())
137 .build()
138 .unwrap())
139}
140
141fn gather_member(members: &[Member], prompt: &str) -> usize {
142 let usernames = members
143 .iter()
144 .map(|member| member.username.as_str())
145 .collect::<Vec<&str>>();
146
147 let assignee_selection_id = FuzzySelect::with_theme(&ColorfulTheme::default())
148 .with_prompt(prompt)
149 .default(0)
150 .items(&usernames)
151 .interact()
152 .unwrap();
153
154 if assignee_selection_id != 0 {
155 assignee_selection_id
156 } else {
157 0
159 }
160}
161
162pub fn prompt_user_title_description(
163 default_title: &str,
164 default_description: &str,
165) -> (String, String) {
166 let title: String = Input::with_theme(&ColorfulTheme::default())
167 .with_prompt("Title: ")
168 .default(default_title.to_string())
169 .interact_text()
170 .unwrap();
171
172 let description = get_description(default_description);
173 (title, description)
174}
175
176fn get_description(default_description: &str) -> String {
177 show_input("Description: ", default_description, true, Style::Bold);
178 let mut description = default_description.to_string();
179 let prompt = "Edit description";
180 while !confirm(prompt, false) {
181 description = if let Some(entry_msg) = Editor::new().edit(&description).unwrap() {
182 entry_msg
183 } else {
184 "".to_string()
185 };
186 show_input("Description: ", &description, true, Style::Bold);
187 }
188 description
189}
190
191pub enum Style {
192 Bold,
193 Light,
194}
195
196pub fn show_input(prompt: &str, data: &str, new_line: bool, font_style: Style) {
197 let mut prompt_style = style(prompt);
198 if let Style::Bold = font_style {
199 prompt_style = prompt_style.bold()
200 }
201 if new_line {
202 println!("{prompt_style}");
203 println!("\n{data}\n");
204 } else {
205 print!("{prompt_style}: ");
206 println!("{data}")
207 }
208}
209
210fn confirm(prompt: &str, default_answer: bool) -> bool {
211 if Confirm::with_theme(&ColorfulTheme::default())
212 .with_prompt(prompt)
213 .default(default_answer)
214 .interact()
215 .unwrap()
216 {
217 return default_answer;
218 }
219 !default_answer
220}
221
222pub fn show_summary_merge_request(
223 commit_str: &str,
224 args: &MergeRequestBodyArgs,
225 accept: bool,
226) -> Result<()> {
227 show_outgoing_changes_summary(commit_str);
228 show_input("Target branch", &args.target_branch, false, Style::Bold);
229 show_input("Assignee", &args.assignee.username, false, Style::Bold);
230 show_input("Reviewer", &args.reviewer.username, false, Style::Bold);
231 show_input("Title", &args.title, false, Style::Bold);
232 if !args.description.is_empty() {
233 show_input("Description:", &args.description, true, Style::Bold);
234 } else {
235 show_input("Description", "None", false, Style::Bold);
236 }
237 println!();
238 if accept || confirm("Confirm summary", true) {
239 Ok(())
240 } else {
241 Err(error::gen("User cancelled"))
242 }
243}
244
245pub fn show_outgoing_changes_summary(commit_str: &str) {
246 show_input(
247 "\nSummary of outgoing changes:",
248 commit_str,
249 true,
250 Style::Bold,
251 );
252}
253
254pub fn prompt_args() -> String {
255 Input::with_theme(&ColorfulTheme::default())
256 .with_prompt("args: ")
257 .allow_empty(true)
258 .interact_text()
259 .unwrap()
260}
261
262pub fn fuzzy_select(amps: Vec<String>) -> Result<String> {
263 let selection = dialoguer::FuzzySelect::with_theme(&ColorfulTheme::default())
264 .with_prompt("amp:")
265 .default(0)
266 .items(&s)
267 .interact()
268 .unwrap();
269 Ok(amps[selection].to_string())
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 use crate::cmds::project::Member;
277
278 fn create_test_member(id: i64, username: &str) -> Member {
279 Member::builder()
280 .id(id)
281 .username(username.to_string())
282 .build()
283 .unwrap()
284 }
285
286 fn create_test_members() -> Vec<Member> {
287 vec![
288 create_test_member(1, "alice"),
289 create_test_member(2, "bob"),
290 create_test_member(3, "charlie"),
291 ]
292 }
293
294 #[test]
295 fn test_prepare_assignee_list_with_cli_assignee() {
296 let members = create_test_members();
297 let original_members = members.clone();
298 let selector = MemberSelector::new(members);
299 let cli_assignee = create_test_member(4, "david");
300
301 let result = selector.prepare_assignee_list(
302 Some(&cli_assignee),
303 Some(create_test_member(5, "eve")), );
305
306 assert_eq!(result[0], cli_assignee); assert_eq!(result[1], Member::default()); assert_eq!(&result[2..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 2); }
312
313 #[test]
314 fn test_prepare_assignee_list_with_config_assignee() {
315 let members = create_test_members();
316 let original_members = members.clone();
317 let selector = MemberSelector::new(members);
318 let config_assignee = create_test_member(4, "eve");
319
320 let result = selector.prepare_assignee_list(None, Some(config_assignee.clone()));
321
322 assert_eq!(result[0], config_assignee); assert_eq!(result[1], Member::default()); assert_eq!(&result[2..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 2); }
328
329 #[test]
330 fn test_prepare_assignee_list_with_no_defaults() {
331 let members = create_test_members();
332 let original_members = members.clone();
333 let selector = MemberSelector::new(members);
334
335 let result = selector.prepare_assignee_list(None, None);
336
337 assert_eq!(result[0], Member::default()); assert_eq!(&result[1..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 1); }
342
343 #[test]
344 fn test_prepare_assignee_list_with_existing_member() {
345 let members = create_test_members();
346 let original_members = members.clone();
347 let selector = MemberSelector::new(members);
348
349 let cli_assignee = &original_members[0];
351 let result = selector.prepare_assignee_list(Some(cli_assignee), None);
352
353 assert_eq!(result[0], cli_assignee.clone()); assert_eq!(result[1], Member::default()); assert_eq!(&result[2..], &original_members[..]); assert_eq!(result.len(), original_members.len() + 2); }
359
360 #[test]
361 fn test_prepare_reviewer_list_with_cli_reviewer_provided() {
362 let members = create_test_members();
363 let selector = MemberSelector::new(members);
364 let assignee = create_test_member(1, "alice");
365 let reviewer = create_test_member(2, "charlie");
366
367 let result = selector.prepare_reviewer_list(Some(&reviewer), &assignee);
368
369 assert_eq!(result[0], reviewer);
370 assert!(!result.contains(&assignee));
371 assert_eq!(result.len(), 4);
373 }
374
375 #[test]
376 fn test_prepare_reviewer_list_no_cli_reviewer_provided() {
377 let members = create_test_members();
378 let selector = MemberSelector::new(members);
379 let assignee = create_test_member(1, "alice");
380
381 let result = selector.prepare_reviewer_list(None::<&Member>, &assignee);
382
383 assert_eq!(result[0], Member::default());
384 assert!(!result.contains(&assignee));
385 assert_eq!(result.len(), 3); }
387}