codeberg_cli/actions/milestone/
edit.rs

1use crate::actions::GeneralArgs;
2use crate::render::json::JsonToStdout;
3use crate::render::spinner::spin_until_ready;
4use crate::render::ui::fuzzy_select_with_key;
5use crate::render::ui::multi_fuzzy_select_with_key;
6use crate::types::context::BergContext;
7use crate::types::git::OwnerRepo;
8use crate::types::output::OutputMode;
9use anyhow::Context;
10use forgejo_api::structs::EditMilestoneOption;
11use forgejo_api::structs::IssueGetMilestonesListQuery;
12use forgejo_api::structs::Milestone;
13use forgejo_api::structs::StateType;
14use strum::Display;
15use strum::VariantArray;
16
17use crate::actions::text_manipulation::input_prompt_for;
18use crate::actions::text_manipulation::select_prompt_for;
19
20use super::display_milestone;
21
22use clap::Parser;
23
24/// Edit selected issue
25#[derive(Parser, Debug)]
26pub struct EditMilestoneArgs {}
27
28#[derive(Display, PartialEq, Eq, VariantArray)]
29enum EditableFields {
30    Title,
31    State,
32    Description,
33}
34
35impl EditMilestoneArgs {
36    pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
37        let ctx = BergContext::new(self, general_args).await?;
38
39        let OwnerRepo { owner, repo } = ctx.owner_repo()?;
40        let milestone = select_milestone(&ctx).await?;
41        let milestone_id_or_name = milestone
42            .id
43            .as_ref()
44            .map(|id| id.to_string())
45            .or_else(|| milestone.title.clone())
46            .context("Selected milestone doesn't have an ID or Name")?;
47
48        let options = create_options(&ctx, &milestone).await?;
49
50        let updated_milestone = ctx
51            .client
52            .issue_edit_milestone(
53                owner.as_str(),
54                repo.as_str(),
55                milestone_id_or_name.as_str(),
56                options,
57            )
58            .await?;
59
60        match general_args.output_mode {
61            OutputMode::Pretty => {
62                tracing::debug!("{updated_milestone:?}");
63            }
64            OutputMode::Json => updated_milestone.print_json()?,
65        }
66
67        Ok(())
68    }
69}
70
71async fn select_milestone(ctx: &BergContext<EditMilestoneArgs>) -> anyhow::Result<Milestone> {
72    let OwnerRepo { owner, repo } = ctx.owner_repo()?;
73    let (_, milestones_list) = spin_until_ready(ctx.client.issue_get_milestones_list(
74        owner.as_str(),
75        repo.as_str(),
76        IssueGetMilestonesListQuery::default(),
77    ))
78    .await?;
79
80    if milestones_list.is_empty() {
81        anyhow::bail!("No milestones found in this repository");
82    }
83
84    fuzzy_select_with_key(
85        &milestones_list,
86        select_prompt_for("milestone"),
87        display_milestone,
88    )
89    .cloned()
90}
91
92async fn create_options(
93    ctx: &BergContext<EditMilestoneArgs>,
94    milestone: &Milestone,
95) -> anyhow::Result<EditMilestoneOption> {
96    let selected_update_fields = multi_fuzzy_select_with_key(
97        EditableFields::VARIANTS,
98        select_prompt_for("options"),
99        |_| false,
100        |f| f.to_string(),
101    )?;
102
103    let mut options = EditMilestoneOption {
104        description: None,
105        due_on: None,
106        state: None,
107        title: None,
108    };
109
110    if selected_update_fields.contains(&&EditableFields::Title) {
111        let current_title = milestone.title.as_ref().cloned();
112        options
113            .title
114            .replace(milestone_title(ctx, current_title).await?);
115    }
116    if selected_update_fields.contains(&&EditableFields::State) {
117        let current_state = milestone.state.as_ref().cloned();
118        options
119            .state
120            .replace(milestone_state(ctx, current_state).await?);
121    }
122    if selected_update_fields.contains(&&EditableFields::Description) {
123        let current_description = milestone.description.as_ref().cloned();
124        options
125            .description
126            .replace(milestone_description(ctx, current_description).await?);
127    }
128
129    Ok(options)
130}
131
132async fn milestone_title(
133    _ctx: &BergContext<EditMilestoneArgs>,
134    current_title: Option<String>,
135) -> anyhow::Result<String> {
136    inquire::Text::new(input_prompt_for("Choose a new milestone title").as_str())
137        .with_default(
138            current_title
139                .as_deref()
140                .unwrap_or("Enter a milestone title"),
141        )
142        .prompt()
143        .map_err(anyhow::Error::from)
144}
145
146async fn milestone_state(
147    _ctx: &BergContext<EditMilestoneArgs>,
148    _current_state: Option<StateType>,
149) -> anyhow::Result<String> {
150    let selected_state = fuzzy_select_with_key(
151        &[StateType::Open, StateType::Closed],
152        select_prompt_for("states"),
153        |f| format!("{f:?}"),
154    )
155    .cloned()?;
156    Ok(format!("{selected_state:?}"))
157}
158
159async fn milestone_description(
160    ctx: &BergContext<EditMilestoneArgs>,
161    current_description: Option<String>,
162) -> anyhow::Result<String> {
163    ctx.editor_for(
164        "a description",
165        current_description
166            .as_deref()
167            .unwrap_or("Enter a milestone description"),
168    )
169}