Add issue label and milestone filters
This commit is contained in:
@@ -61,18 +61,21 @@ pub async fn load_issues(
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
status: &str,
|
||||
labels: &[String],
|
||||
milestone: &str,
|
||||
) -> Result<Vec<models::Issue>, String> {
|
||||
let client =
|
||||
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
||||
let labels = (!labels.is_empty()).then(|| labels.join(","));
|
||||
apis::issue_api::issue_list_issues(
|
||||
&client.configuration(),
|
||||
owner,
|
||||
repository,
|
||||
Some(status),
|
||||
None,
|
||||
labels.as_deref(),
|
||||
None,
|
||||
Some("issues"),
|
||||
None,
|
||||
(!milestone.is_empty()).then_some(milestone),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -85,6 +88,34 @@ pub async fn load_issues(
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub async fn load_labels(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
) -> Result<Vec<models::Label>, String> {
|
||||
let client =
|
||||
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
||||
let configuration = client.configuration();
|
||||
let mut labels = Vec::new();
|
||||
for page in 1.. {
|
||||
let batch = apis::issue_api::issue_list_labels(
|
||||
&configuration,
|
||||
owner,
|
||||
repository,
|
||||
Some(page),
|
||||
Some(100),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let done = batch.len() < 100;
|
||||
labels.extend(batch);
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(labels)
|
||||
}
|
||||
|
||||
pub async fn load_milestones(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use gotcha_gitea::models;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,6 +50,8 @@ pub struct Preferences {
|
||||
pub last_server: Option<usize>,
|
||||
#[serde(default = "open_status")]
|
||||
pub issue_status: String,
|
||||
#[serde(default)]
|
||||
pub issue_filters: BTreeMap<String, IssueFilter>,
|
||||
#[serde(default = "open_status")]
|
||||
pub pull_status: String,
|
||||
#[serde(default)]
|
||||
@@ -63,12 +65,21 @@ impl Default for Preferences {
|
||||
favorites: BTreeSet::new(),
|
||||
last_server: None,
|
||||
issue_status: open_status(),
|
||||
issue_filters: BTreeMap::new(),
|
||||
pull_status: open_status(),
|
||||
appearance: AppearanceMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct IssueFilter {
|
||||
#[serde(default)]
|
||||
pub milestone: String,
|
||||
#[serde(default)]
|
||||
pub labels: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RepositoryData {
|
||||
pub owner: String,
|
||||
|
||||
@@ -211,12 +211,87 @@ impl GotchaCore {
|
||||
repository: String,
|
||||
) -> Result<Vec<IssueRow>, GotchaError> {
|
||||
let server = self.server()?;
|
||||
let status = self.state.lock().unwrap().preferences.issue_status.clone();
|
||||
let (status, filter) = {
|
||||
let state = self.state.lock().unwrap();
|
||||
(
|
||||
state.preferences.issue_status.clone(),
|
||||
state
|
||||
.preferences
|
||||
.issue_filters
|
||||
.get(&repository_key(&server.url, &owner, &repository))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
};
|
||||
let labels: Vec<_> = filter.labels.into_iter().collect();
|
||||
Ok(issue_rows(
|
||||
&load_issues(&server, &owner, &repository, &status).await?,
|
||||
&load_issues(
|
||||
&server,
|
||||
&owner,
|
||||
&repository,
|
||||
&status,
|
||||
&labels,
|
||||
&filter.milestone,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn issue_filters(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
) -> Result<IssueFilterOptions, GotchaError> {
|
||||
let server = self.server()?;
|
||||
let filter = self
|
||||
.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.preferences
|
||||
.issue_filters
|
||||
.get(&repository_key(&server.url, &owner, &repository))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let (labels, milestones) = tokio::try_join!(
|
||||
load_labels(&server, &owner, &repository),
|
||||
load_milestones(&server, &owner, &repository)
|
||||
)?;
|
||||
Ok(issue_filter_options(&labels, &milestones, &filter))
|
||||
}
|
||||
|
||||
pub fn set_issue_filters(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
milestone: String,
|
||||
labels: Vec<String>,
|
||||
) -> Result<(), GotchaError> {
|
||||
let owner = owner.trim();
|
||||
let repository = repository.trim();
|
||||
if owner.is_empty() || repository.is_empty() {
|
||||
return Err("Select a repository first.".into());
|
||||
}
|
||||
let filter = IssueFilter {
|
||||
milestone,
|
||||
labels: labels
|
||||
.into_iter()
|
||||
.filter(|label| !label.is_empty())
|
||||
.collect(),
|
||||
};
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let server = state
|
||||
.active_server
|
||||
.and_then(|index| state.preferences.servers.get(index))
|
||||
.ok_or("Select a server first.")?;
|
||||
let key = repository_key(&server.url, owner, repository);
|
||||
if filter == IssueFilter::default() {
|
||||
state.preferences.issue_filters.remove(&key);
|
||||
} else {
|
||||
state.preferences.issue_filters.insert(key, filter);
|
||||
}
|
||||
save_preferences(&state.preferences).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn issue(
|
||||
&self,
|
||||
owner: String,
|
||||
|
||||
@@ -3,7 +3,8 @@ use gotcha_gitea::models;
|
||||
use crate::{
|
||||
activity,
|
||||
domain::{
|
||||
HistoryCommit, HomeData, IssueDetails, MilestoneDetails, PullDetails, RepositoryData,
|
||||
HistoryCommit, HomeData, IssueDetails, IssueFilter, MilestoneDetails, PullDetails,
|
||||
RepositoryData,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -66,6 +67,14 @@ pub struct IssuePage {
|
||||
pub comments: Vec<CommentRow>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct IssueFilterOptions {
|
||||
pub milestones: Vec<String>,
|
||||
pub labels: Vec<String>,
|
||||
pub selected_milestone: String,
|
||||
pub selected_labels: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct MilestoneRow {
|
||||
pub id: i64,
|
||||
@@ -219,6 +228,31 @@ pub fn issue_rows(issues: &[models::Issue]) -> Vec<IssueRow> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn issue_filter_options(
|
||||
labels: &[models::Label],
|
||||
milestones: &[models::Milestone],
|
||||
filter: &IssueFilter,
|
||||
) -> IssueFilterOptions {
|
||||
let mut labels: Vec<_> = labels
|
||||
.iter()
|
||||
.filter_map(|label| label.name.clone())
|
||||
.collect();
|
||||
labels.sort_by_key(|label| label.to_lowercase());
|
||||
labels.dedup();
|
||||
let mut milestones: Vec<_> = milestones
|
||||
.iter()
|
||||
.filter_map(|milestone| milestone.title.clone())
|
||||
.collect();
|
||||
milestones.sort_by_key(|milestone| milestone.to_lowercase());
|
||||
milestones.dedup();
|
||||
IssueFilterOptions {
|
||||
milestones,
|
||||
labels,
|
||||
selected_milestone: filter.milestone.clone(),
|
||||
selected_labels: filter.labels.iter().cloned().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn milestone_rows(milestones: &[models::Milestone]) -> Vec<MilestoneRow> {
|
||||
let mut milestones = milestones.to_vec();
|
||||
milestones.sort_by_key(|milestone| {
|
||||
@@ -879,4 +913,40 @@ mod tests {
|
||||
|
||||
assert_eq!(page.state, "closed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_sorted_issue_filter_options() {
|
||||
let filter = IssueFilter {
|
||||
milestone: "v2".into(),
|
||||
labels: ["bug".into(), "critical".into()].into(),
|
||||
};
|
||||
let options = issue_filter_options(
|
||||
&[
|
||||
models::Label {
|
||||
name: Some("critical".into()),
|
||||
..Default::default()
|
||||
},
|
||||
models::Label {
|
||||
name: Some("bug".into()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
&[
|
||||
models::Milestone {
|
||||
title: Some("v2".into()),
|
||||
..Default::default()
|
||||
},
|
||||
models::Milestone {
|
||||
title: Some("v1".into()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
&filter,
|
||||
);
|
||||
|
||||
assert_eq!(options.labels, ["bug", "critical"]);
|
||||
assert_eq!(options.milestones, ["v1", "v2"]);
|
||||
assert_eq!(options.selected_labels, ["bug", "critical"]);
|
||||
assert_eq!(options.selected_milestone, "v2");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ use security_framework::passwords::{get_generic_password, set_generic_password};
|
||||
use crate::domain::{Preferences, Server, open_status};
|
||||
|
||||
pub fn favorite_key(server: &str, owner: &str, repository: &str) -> String {
|
||||
repository_key(server, owner, repository)
|
||||
}
|
||||
|
||||
pub fn repository_key(server: &str, owner: &str, repository: &str) -> String {
|
||||
format!("{server}|{owner}/{repository}")
|
||||
}
|
||||
|
||||
@@ -95,6 +99,19 @@ mod tests {
|
||||
favorite_key("https://gitea.example.com", "octo", "demo"),
|
||||
"https://gitea.example.com|octo/demo"
|
||||
);
|
||||
let preferences: Preferences = serde_json::from_str(
|
||||
r#"{"issue_filters":{"server|octo/demo":{"milestone":"v1","labels":["bug"]}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
preferences.issue_filters["server|octo/demo"].milestone,
|
||||
"v1"
|
||||
);
|
||||
assert!(
|
||||
preferences.issue_filters["server|octo/demo"]
|
||||
.labels
|
||||
.contains("bug")
|
||||
);
|
||||
assert_eq!(Preferences::default().pull_status, "open");
|
||||
assert_eq!(
|
||||
Preferences::default().appearance,
|
||||
|
||||
Reference in New Issue
Block a user