use crate::actions::issue::display_issue;
use crate::actions::GeneralArgs;
use crate::render::option::option_display;
use crate::render::spinner::spin_until_ready;
use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
use crate::types::api::state_type::StateType;
use crate::types::context::BergContext;
use crate::types::git::OwnerRepo;
use anyhow::Context;
use forgejo_api::structs::{
EditIssueOption, Issue, IssueLabelsOption, IssueListIssuesQuery, IssueListLabelsQuery,
};
use strum::{Display, VariantArray};
use crate::actions::text_manipulation::{edit_prompt_for, input_prompt_for, select_prompt_for};
use clap::Parser;
#[derive(Parser, Debug)]
pub struct EditIssueArgs {}
#[derive(Display, PartialEq, Eq, VariantArray)]
enum EditableFields {
Assignees,
Description,
State,
Title,
Labels,
}
impl EditIssueArgs {
pub async fn run(self, general_args: GeneralArgs) -> anyhow::Result<()> {
let _ = general_args;
let ctx = BergContext::new(self).await?;
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let issue = select_issue(&ctx).await?;
let issue_id = issue.number.context("Selected issue doesn't have an ID")?;
let (options, label_options) = create_options(&ctx, &issue).await?;
let updated_issue = ctx
.client
.issue_edit_issue(owner.as_str(), repo.as_str(), issue_id, options)
.await?;
tracing::debug!("{updated_issue:?}");
let updated_labels = ctx
.client
.issue_replace_labels(owner.as_str(), repo.as_str(), issue_id, label_options)
.await?;
tracing::debug!("{updated_labels:?}");
Ok(())
}
}
async fn select_issue(ctx: &BergContext<EditIssueArgs>) -> anyhow::Result<Issue> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let issues_list = spin_until_ready(ctx.client.issue_list_issues(
owner.as_str(),
repo.as_str(),
IssueListIssuesQuery::default(),
))
.await?;
if issues_list.is_empty() {
anyhow::bail!("No issues found in this repository");
}
fuzzy_select_with_key(&issues_list, select_prompt_for("issue"), display_issue).cloned()
}
async fn create_options(
ctx: &BergContext<EditIssueArgs>,
issue: &Issue,
) -> anyhow::Result<(EditIssueOption, IssueLabelsOption)> {
let selected_update_fields = multi_fuzzy_select_with_key(
EditableFields::VARIANTS,
select_prompt_for("options"),
|_| false,
|f| f.to_string(),
)?;
let mut options = EditIssueOption {
assignee: None,
assignees: None,
body: None,
due_date: None,
milestone: None,
r#ref: None,
state: None,
title: None,
unset_due_date: None,
updated_at: None,
};
let mut label_options = IssueLabelsOption {
labels: None,
updated_at: None,
};
if selected_update_fields.contains(&&EditableFields::Assignees) {
let current_assignees = issue
.assignees
.as_ref()
.map(|users| users.iter().filter_map(|user| user.id).collect::<Vec<_>>());
options
.assignees
.replace(issue_assignees(ctx, current_assignees).await?);
}
if selected_update_fields.contains(&&EditableFields::Description) {
options
.body
.replace(issue_body(ctx, issue.body.clone()).await?);
}
if selected_update_fields.contains(&&EditableFields::State) {
options
.state
.replace(issue_state(ctx, issue.state.clone()).await?);
}
if selected_update_fields.contains(&&EditableFields::Title) {
options
.title
.replace(issue_title(ctx, issue.title.clone()).await?);
}
if selected_update_fields.contains(&&EditableFields::Labels) {
let current_labels = issue.labels.as_ref().map(|labels| {
labels
.iter()
.filter_map(|label| label.id)
.collect::<Vec<_>>()
});
label_options
.labels
.replace(issue_labels(ctx, current_labels).await?);
}
Ok((options, label_options))
}
async fn issue_assignees(
ctx: &BergContext<EditIssueArgs>,
current_assignees: Option<Vec<u64>>,
) -> anyhow::Result<Vec<String>> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let current_assignees = current_assignees.unwrap_or_default();
let all_assignees = ctx
.client
.repo_get_assignees(owner.as_str(), repo.as_str())
.await?;
let selected_assignees = multi_fuzzy_select_with_key(
&all_assignees,
select_prompt_for("assignees"),
|u| u.id.is_some_and(|id| current_assignees.contains(&id)),
|u| option_display(&u.login),
)?;
Ok(selected_assignees
.into_iter()
.filter_map(|u| u.login.as_ref().cloned())
.collect::<Vec<_>>())
}
async fn issue_body(
_ctx: &BergContext<EditIssueArgs>,
current_body: Option<String>,
) -> anyhow::Result<String> {
inquire::Editor::new(edit_prompt_for("a description").as_str())
.with_predefined_text(
current_body
.as_deref()
.unwrap_or("Enter an issue description"),
)
.prompt()
.map_err(anyhow::Error::from)
}
async fn issue_state(
_ctx: &BergContext<EditIssueArgs>,
_current_state: Option<String>,
) -> anyhow::Result<String> {
let selected_state =
fuzzy_select_with_key(StateType::VARIANTS, select_prompt_for("states"), |f| {
f.to_string()
})?;
Ok(selected_state.to_string())
}
async fn issue_title(
_ctx: &BergContext<EditIssueArgs>,
current_title: Option<String>,
) -> anyhow::Result<String> {
inquire::Text::new(input_prompt_for("Choose a new issue title").as_str())
.with_default(current_title.as_deref().unwrap_or("Enter an issue title"))
.prompt()
.map_err(anyhow::Error::from)
}
async fn issue_labels(
ctx: &BergContext<EditIssueArgs>,
current_labels: Option<Vec<u64>>,
) -> anyhow::Result<Vec<u64>> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let current_labels = current_labels.unwrap_or_default();
let all_labels = ctx
.client
.issue_list_labels(
owner.as_str(),
repo.as_str(),
IssueListLabelsQuery::default(),
)
.await?;
let selected_labels = multi_fuzzy_select_with_key(
&all_labels,
select_prompt_for("labels"),
|l| l.id.is_some_and(|id| current_labels.contains(&id)),
|l| option_display(&l.name),
)?;
let label_ids = selected_labels
.iter()
.filter_map(|l| l.id)
.collect::<Vec<_>>();
Ok(label_ids)
}