Add issue creation and editing

This commit is contained in:
Georg Bauer
2026-07-31 14:39:17 +02:00
parent 86200b4668
commit 228879da15
11 changed files with 1175 additions and 40 deletions

View File

@@ -3,8 +3,8 @@ use gotcha_gitea::models;
use crate::{
activity,
domain::{
HistoryCommit, HomeData, IssueDetails, IssueFilter, MilestoneDetails, PullDetails,
PullFilter, RepositoryData,
HistoryCommit, HomeData, IssueDetails, IssueEditorData, IssueFilter, MilestoneDetails,
PullDetails, PullFilter, RepositoryData, civil_from_days, days_from_civil, parse_api_date,
},
};
@@ -67,6 +67,29 @@ pub struct IssuePage {
pub comments: Vec<CommentRow>,
}
#[derive(Clone, uniffi::Record)]
pub struct IssueEditorLabel {
pub id: i64,
pub name: String,
pub selected: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct IssueEditorMilestone {
pub id: i64,
pub title: String,
pub selected: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct IssueEditorPage {
pub title: String,
pub body: String,
pub due_date: Option<i64>,
pub labels: Vec<IssueEditorLabel>,
pub milestones: Vec<IssueEditorMilestone>,
}
#[derive(Clone, uniffi::Record)]
pub struct IssueFilterOptions {
pub milestones: Vec<String>,
@@ -457,6 +480,70 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
}
}
pub fn issue_editor_page(data: IssueEditorData) -> IssueEditorPage {
let selected_labels: std::collections::BTreeSet<_> = data
.issue
.as_ref()
.and_then(|issue| issue.labels.as_deref())
.unwrap_or_default()
.iter()
.filter_map(|label| label.id)
.collect();
let selected_milestone = data
.issue
.as_ref()
.and_then(|issue| issue.milestone.as_ref())
.and_then(|milestone| milestone.id);
let mut labels: Vec<_> = data
.labels
.into_iter()
.filter_map(|label| {
let id = label.id?;
Some(IssueEditorLabel {
id,
name: label.name?,
selected: selected_labels.contains(&id),
})
})
.collect();
labels.sort_by_key(|label| label.name.to_lowercase());
let mut milestones: Vec<_> = data
.milestones
.into_iter()
.filter(|milestone| {
milestone.state.as_deref() != Some("closed") || milestone.id == selected_milestone
})
.filter_map(|milestone| {
let id = milestone.id?;
Some(IssueEditorMilestone {
id,
title: milestone.title?,
selected: Some(id) == selected_milestone,
})
})
.collect();
milestones.sort_by_key(|milestone| milestone.title.to_lowercase());
IssueEditorPage {
title: data
.issue
.as_ref()
.and_then(|issue| issue.title.clone())
.unwrap_or_default(),
body: data
.issue
.as_ref()
.and_then(|issue| issue.body.clone())
.unwrap_or_default(),
due_date: data
.issue
.as_ref()
.and_then(|issue| issue.due_date.as_deref())
.and_then(parse_api_date),
labels,
milestones,
}
}
pub fn pull_page(details: PullDetails) -> PullPage {
let pull = &details.pull;
let author = pull
@@ -840,31 +927,6 @@ fn heat_cells(data: &[models::UserHeatmapData]) -> (Vec<HeatCell>, i64) {
(cells, contribution_count)
}
fn civil_from_days(days: i64) -> (i64, i64, i64) {
let days = days + 719_468;
let era = days.div_euclid(146_097);
let day_of_era = days - era * 146_097;
let year_of_era =
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let mut year = year_of_era + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let month_prime = (5 * day_of_year + 2) / 153;
let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
let month = month_prime + if month_prime < 10 { 3 } else { -9 };
year += i64::from(month <= 2);
(year, month, day)
}
fn days_from_civil(mut year: i64, month: i64, day: i64) -> i64 {
year -= i64::from(month <= 2);
let era = year.div_euclid(400);
let year_of_era = year - era * 400;
let month_prime = month + if month > 2 { -3 } else { 9 };
let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era - 719_468
}
fn label_row(label: &models::Label) -> Option<LabelRow> {
let name = label.name.as_deref()?.trim();
let color = label.color.as_deref()?.trim().trim_start_matches('#');
@@ -1172,6 +1234,42 @@ mod tests {
assert_eq!(page.state, "closed");
}
#[test]
fn issue_editor_exposes_selected_metadata_and_due_date() {
let selected_label = models::Label {
id: Some(2),
name: Some("bug".into()),
..Default::default()
};
let page = issue_editor_page(IssueEditorData {
issue: Some(models::Issue {
title: Some("Title".into()),
body: Some("Body".into()),
due_date: Some("2024-02-29T18:00:00Z".into()),
labels: Some(vec![selected_label.clone()]),
milestone: Some(Box::new(models::Milestone {
id: Some(4),
title: Some("Current".into()),
state: Some("closed".into()),
..Default::default()
})),
..Default::default()
}),
labels: vec![selected_label],
milestones: vec![models::Milestone {
id: Some(4),
title: Some("Current".into()),
state: Some("closed".into()),
..Default::default()
}],
});
assert_eq!((page.title.as_str(), page.body.as_str()), ("Title", "Body"));
assert_eq!(page.due_date, Some(1_709_164_800));
assert!(page.labels[0].selected);
assert!(page.milestones[0].selected);
}
#[test]
fn builds_sorted_issue_filter_options() {
let filter = IssueFilter {