Add pull request milestone features
This commit is contained in:
@@ -192,6 +192,11 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
|||||||
- [ ] The list defaults to the saved Open/Closed filter.
|
- [ ] The list defaults to the saved Open/Closed filter.
|
||||||
- [ ] Change the native filter menu between Open and Closed; the checkmark,
|
- [ ] Change the native filter menu between Open and Closed; the checkmark,
|
||||||
rows, and persisted selection update.
|
rows, and persisted selection update.
|
||||||
|
- [ ] Select a milestone and then All Milestones; the checkmark and pull-request
|
||||||
|
rows update, and the milestone selection persists independently per
|
||||||
|
server after relaunch.
|
||||||
|
- [ ] The pull-request filter icon uses a neutral color for Open + All
|
||||||
|
Milestones and the app accent color whenever either filter is non-default.
|
||||||
- [ ] Each row shows repository/number, title, author/update metadata, comment
|
- [ ] Each row shows repository/number, title, author/update metadata, comment
|
||||||
count, and draft/merged state where applicable.
|
count, and draft/merged state where applicable.
|
||||||
- [ ] Open a pull request and verify title, metadata, Markdown body, comments,
|
- [ ] Open a pull request and verify title, metadata, Markdown body, comments,
|
||||||
@@ -204,8 +209,8 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
|||||||
both open and closed milestones.
|
both open and closed milestones.
|
||||||
- [ ] Each milestone shows its title, description, state, due date, issue
|
- [ ] Each milestone shows its title, description, state, due date, issue
|
||||||
counts, and a green/amber closed/open progress bar.
|
counts, and a green/amber closed/open progress bar.
|
||||||
- [ ] Open a milestone and verify every assigned issue is listed; tapping an
|
- [ ] Open a milestone and verify every assigned issue and pull request is
|
||||||
issue opens its normal issue detail.
|
listed; tapping either opens its normal detail.
|
||||||
- [ ] Repository issue lists and issue details show the assigned milestone with
|
- [ ] Repository issue lists and issue details show the assigned milestone with
|
||||||
a flag icon instead of a text label.
|
a flag icon instead of a text label.
|
||||||
- [ ] Repositories without milestones show the native empty state.
|
- [ ] Repositories without milestones show the native empty state.
|
||||||
|
|||||||
@@ -159,29 +159,48 @@ pub async fn load_milestone(
|
|||||||
apis::issue_api::issue_get_milestone(&configuration, owner, repository, &id.to_string())
|
apis::issue_api::issue_get_milestone(&configuration, owner, repository, &id.to_string())
|
||||||
.await
|
.await
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
let issues = apis::issue_api::issue_list_issues(
|
let load = |kind| {
|
||||||
&configuration,
|
apis::issue_api::issue_list_issues(
|
||||||
owner,
|
&configuration,
|
||||||
repository,
|
owner,
|
||||||
Some("all"),
|
repository,
|
||||||
None,
|
Some("all"),
|
||||||
None,
|
None,
|
||||||
Some("issues"),
|
None,
|
||||||
milestone.title.as_deref(),
|
Some(kind),
|
||||||
None,
|
milestone.title.as_deref(),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some(1),
|
None,
|
||||||
Some(100),
|
Some(1),
|
||||||
)
|
Some(100),
|
||||||
.await
|
)
|
||||||
.map_err(|error| error.to_string())?;
|
};
|
||||||
Ok(MilestoneDetails { milestone, issues })
|
let (issues, pulls) = tokio::join!(load("issues"), load("pulls"));
|
||||||
|
let mut pulls = pulls.map_err(|error| error.to_string())?;
|
||||||
|
for pull in &mut pulls {
|
||||||
|
pull.repository.get_or_insert_with(|| {
|
||||||
|
Box::new(models::RepositoryMeta {
|
||||||
|
name: Some(repository.into()),
|
||||||
|
owner: Some(owner.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(MilestoneDetails {
|
||||||
|
milestone,
|
||||||
|
issues: issues.map_err(|error| error.to_string())?,
|
||||||
|
pulls,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_pulls(server: &Server, status: &str) -> Result<Vec<models::Issue>, String> {
|
pub async fn load_pulls(
|
||||||
|
server: &Server,
|
||||||
|
status: &str,
|
||||||
|
milestone: &str,
|
||||||
|
) -> Result<Vec<models::Issue>, String> {
|
||||||
let client =
|
let client =
|
||||||
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
||||||
let configuration = client.configuration();
|
let configuration = client.configuration();
|
||||||
@@ -197,7 +216,7 @@ pub async fn load_pulls(server: &Server, status: &str) -> Result<Vec<models::Iss
|
|||||||
&configuration,
|
&configuration,
|
||||||
Some(status),
|
Some(status),
|
||||||
None,
|
None,
|
||||||
None,
|
(!milestone.is_empty()).then_some(milestone),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
Some("pulls"),
|
Some("pulls"),
|
||||||
@@ -223,6 +242,20 @@ pub async fn load_pulls(server: &Server, status: &str) -> Result<Vec<models::Iss
|
|||||||
Ok(pulls)
|
Ok(pulls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn load_pull_milestones(server: &Server) -> Result<Vec<String>, String> {
|
||||||
|
let (open, closed) = tokio::try_join!(
|
||||||
|
load_pulls(server, "open", ""),
|
||||||
|
load_pulls(server, "closed", "")
|
||||||
|
)?;
|
||||||
|
Ok(open
|
||||||
|
.into_iter()
|
||||||
|
.chain(closed)
|
||||||
|
.filter_map(|pull| pull.milestone?.title)
|
||||||
|
.collect::<BTreeSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn load_branches(
|
pub async fn load_branches(
|
||||||
server: &Server,
|
server: &Server,
|
||||||
owner: &str,
|
owner: &str,
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ pub struct Preferences {
|
|||||||
#[serde(default = "open_status")]
|
#[serde(default = "open_status")]
|
||||||
pub pull_status: String,
|
pub pull_status: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub pull_filters: BTreeMap<String, PullFilter>,
|
||||||
|
#[serde(default)]
|
||||||
pub appearance: AppearanceMode,
|
pub appearance: AppearanceMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +69,7 @@ impl Default for Preferences {
|
|||||||
issue_status: open_status(),
|
issue_status: open_status(),
|
||||||
issue_filters: BTreeMap::new(),
|
issue_filters: BTreeMap::new(),
|
||||||
pull_status: open_status(),
|
pull_status: open_status(),
|
||||||
|
pull_filters: BTreeMap::new(),
|
||||||
appearance: AppearanceMode::default(),
|
appearance: AppearanceMode::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +89,18 @@ impl IssueFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct PullFilter {
|
||||||
|
#[serde(default)]
|
||||||
|
pub milestone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PullFilter {
|
||||||
|
pub fn is_active(&self, status: &str) -> bool {
|
||||||
|
status != "open" || self != &Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RepositoryData {
|
pub struct RepositoryData {
|
||||||
pub owner: String,
|
pub owner: String,
|
||||||
@@ -127,6 +142,7 @@ pub struct IssueDetails {
|
|||||||
pub struct MilestoneDetails {
|
pub struct MilestoneDetails {
|
||||||
pub milestone: models::Milestone,
|
pub milestone: models::Milestone,
|
||||||
pub issues: Vec<models::Issue>,
|
pub issues: Vec<models::Issue>,
|
||||||
|
pub pulls: Vec<models::Issue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PullDetails {
|
pub struct PullDetails {
|
||||||
@@ -154,5 +170,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.is_active("open")
|
.is_active("open")
|
||||||
);
|
);
|
||||||
|
assert!(!PullFilter::default().is_active("open"));
|
||||||
|
assert!(PullFilter::default().is_active("closed"));
|
||||||
|
assert!(
|
||||||
|
PullFilter {
|
||||||
|
milestone: "v1".into(),
|
||||||
|
}
|
||||||
|
.is_active("open")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,10 +344,68 @@ impl GotchaCore {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn pull_filters(&self) -> Result<PullFilterOptions, GotchaError> {
|
||||||
|
let server = self.server()?;
|
||||||
|
let filter = self
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.preferences
|
||||||
|
.pull_filters
|
||||||
|
.get(&server.url)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(pull_filter_options(
|
||||||
|
&load_pull_milestones(&server).await?,
|
||||||
|
&filter,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull_filters_active(&self) -> Result<bool, GotchaError> {
|
||||||
|
let server = self.server()?;
|
||||||
|
let state = self.state.lock().unwrap();
|
||||||
|
let filter = state
|
||||||
|
.preferences
|
||||||
|
.pull_filters
|
||||||
|
.get(&server.url)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(filter.is_active(&state.preferences.pull_status))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_pull_filters(&self, milestone: String) -> Result<(), GotchaError> {
|
||||||
|
let filter = PullFilter { milestone };
|
||||||
|
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 = server.url.clone();
|
||||||
|
if filter == PullFilter::default() {
|
||||||
|
state.preferences.pull_filters.remove(&key);
|
||||||
|
} else {
|
||||||
|
state.preferences.pull_filters.insert(key, filter);
|
||||||
|
}
|
||||||
|
save_preferences(&state.preferences).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn pulls(&self) -> Result<Vec<PullRow>, GotchaError> {
|
pub async fn pulls(&self) -> Result<Vec<PullRow>, GotchaError> {
|
||||||
let server = self.server()?;
|
let server = self.server()?;
|
||||||
let status = self.state.lock().unwrap().preferences.pull_status.clone();
|
let (status, filter) = {
|
||||||
Ok(pull_rows(&load_pulls(&server, &status).await?))
|
let state = self.state.lock().unwrap();
|
||||||
|
(
|
||||||
|
state.preferences.pull_status.clone(),
|
||||||
|
state
|
||||||
|
.preferences
|
||||||
|
.pull_filters
|
||||||
|
.get(&server.url)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
Ok(pull_rows(
|
||||||
|
&load_pulls(&server, &status, &filter.milestone).await?,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pull(
|
pub async fn pull(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
activity,
|
activity,
|
||||||
domain::{
|
domain::{
|
||||||
HistoryCommit, HomeData, IssueDetails, IssueFilter, MilestoneDetails, PullDetails,
|
HistoryCommit, HomeData, IssueDetails, IssueFilter, MilestoneDetails, PullDetails,
|
||||||
RepositoryData,
|
PullFilter, RepositoryData,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,6 +91,13 @@ pub struct MilestoneRow {
|
|||||||
pub struct MilestonePage {
|
pub struct MilestonePage {
|
||||||
pub milestone: MilestoneRow,
|
pub milestone: MilestoneRow,
|
||||||
pub issues: Vec<IssueRow>,
|
pub issues: Vec<IssueRow>,
|
||||||
|
pub pulls: Vec<PullRow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, uniffi::Record)]
|
||||||
|
pub struct PullFilterOptions {
|
||||||
|
pub milestones: Vec<String>,
|
||||||
|
pub selected_milestone: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
@@ -292,6 +299,20 @@ pub fn milestone_page(details: MilestoneDetails) -> MilestonePage {
|
|||||||
MilestonePage {
|
MilestonePage {
|
||||||
milestone: milestone_row(&details.milestone),
|
milestone: milestone_row(&details.milestone),
|
||||||
issues: issue_rows(&details.issues),
|
issues: issue_rows(&details.issues),
|
||||||
|
pulls: pull_rows(&details.pulls),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull_filter_options(milestones: &[String], filter: &PullFilter) -> PullFilterOptions {
|
||||||
|
let mut milestones = milestones.to_vec();
|
||||||
|
if !filter.milestone.is_empty() {
|
||||||
|
milestones.push(filter.milestone.clone());
|
||||||
|
}
|
||||||
|
milestones.sort_by_key(|milestone| milestone.to_lowercase());
|
||||||
|
milestones.dedup();
|
||||||
|
PullFilterOptions {
|
||||||
|
milestones,
|
||||||
|
selected_milestone: filter.milestone.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,6 +1207,37 @@ mod tests {
|
|||||||
assert_eq!(options.milestones, ["v1", "v2", "v3"]);
|
assert_eq!(options.milestones, ["v1", "v2", "v3"]);
|
||||||
assert_eq!(options.selected_labels, ["bug", "retired"]);
|
assert_eq!(options.selected_labels, ["bug", "retired"]);
|
||||||
assert_eq!(options.selected_milestone, "v3");
|
assert_eq!(options.selected_milestone, "v3");
|
||||||
|
|
||||||
|
let pull_options = pull_filter_options(
|
||||||
|
&["v2".into(), "v1".into()],
|
||||||
|
&PullFilter {
|
||||||
|
milestone: "v3".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(pull_options.milestones, ["v1", "v2", "v3"]);
|
||||||
|
assert_eq!(pull_options.selected_milestone, "v3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn milestone_page_includes_linked_pull_requests() {
|
||||||
|
let page = milestone_page(MilestoneDetails {
|
||||||
|
milestone: models::Milestone::default(),
|
||||||
|
issues: Vec::new(),
|
||||||
|
pulls: vec![models::Issue {
|
||||||
|
number: Some(7),
|
||||||
|
title: Some("Linked pull".into()),
|
||||||
|
repository: Some(Box::new(models::RepositoryMeta {
|
||||||
|
name: Some("demo".into()),
|
||||||
|
owner: Some("octo".into()),
|
||||||
|
..Default::default()
|
||||||
|
})),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(page.issues.is_empty());
|
||||||
|
assert_eq!(page.pulls.len(), 1);
|
||||||
|
assert_eq!(page.pulls[0].number, 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ mod tests {
|
|||||||
"https://gitea.example.com|octo/demo"
|
"https://gitea.example.com|octo/demo"
|
||||||
);
|
);
|
||||||
let preferences: Preferences = serde_json::from_str(
|
let preferences: Preferences = serde_json::from_str(
|
||||||
r#"{"issue_filters":{"server|octo/demo":{"milestone":"v1","labels":["bug"]}}}"#,
|
r#"{"issue_filters":{"server|octo/demo":{"milestone":"v1","labels":["bug"]}},"pull_filters":{"server":{"milestone":"v2"}}}"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -112,6 +112,7 @@ mod tests {
|
|||||||
.labels
|
.labels
|
||||||
.contains("bug")
|
.contains("bug")
|
||||||
);
|
);
|
||||||
|
assert_eq!(preferences.pull_filters["server"].milestone, "v2");
|
||||||
assert_eq!(Preferences::default().pull_status, "open");
|
assert_eq!(Preferences::default().pull_status, "open");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Preferences::default().appearance,
|
Preferences::default().appearance,
|
||||||
|
|||||||
@@ -633,6 +633,10 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func pullDiff(owner: String, repository: String, number: Int64, path: String) async throws -> DiffPage
|
func pullDiff(owner: String, repository: String, number: Int64, path: String) async throws -> DiffPage
|
||||||
|
|
||||||
|
func pullFilters() async throws -> PullFilterOptions
|
||||||
|
|
||||||
|
func pullFiltersActive() throws -> Bool
|
||||||
|
|
||||||
func pulls() async throws -> [PullRow]
|
func pulls() async throws -> [PullRow]
|
||||||
|
|
||||||
func repositories() async throws -> [RepositoryRow]
|
func repositories() async throws -> [RepositoryRow]
|
||||||
@@ -651,6 +655,8 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func setIssueStatus(status: String) throws
|
func setIssueStatus(status: String) throws
|
||||||
|
|
||||||
|
func setPullFilters(milestone: String) throws
|
||||||
|
|
||||||
func setPullStatus(status: String) throws
|
func setPullStatus(status: String) throws
|
||||||
|
|
||||||
func settings() -> Settings
|
func settings() -> Settings
|
||||||
@@ -942,6 +948,31 @@ open func pullDiff(owner: String, repository: String, number: Int64, path: Strin
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func pullFilters()async throws -> PullFilterOptions {
|
||||||
|
return
|
||||||
|
try await uniffiRustCallAsync(
|
||||||
|
rustFutureFunc: {
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_pull_filters(
|
||||||
|
self.uniffiCloneHandle()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
|
||||||
|
completeFunc: ffi_gotcha_core_rust_future_complete_rust_buffer,
|
||||||
|
freeFunc: ffi_gotcha_core_rust_future_free_rust_buffer,
|
||||||
|
liftFunc: FfiConverterTypePullFilterOptions_lift,
|
||||||
|
errorHandler: FfiConverterTypeGotchaError_lift
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
open func pullFiltersActive()throws -> Bool {
|
||||||
|
return try FfiConverterBool.lift(try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_pull_filters_active(
|
||||||
|
self.uniffiCloneHandle(),uniffiCallStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open func pulls()async throws -> [PullRow] {
|
open func pulls()async throws -> [PullRow] {
|
||||||
return
|
return
|
||||||
try await uniffiRustCallAsync(
|
try await uniffiRustCallAsync(
|
||||||
@@ -1054,6 +1085,15 @@ open func setIssueStatus(status: String)throws {try rustCallWithError(FfiConve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open func setPullFilters(milestone: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
|
uniffiCallStatus in
|
||||||
|
uniffi_gotcha_core_fn_method_gotchacore_set_pull_filters(
|
||||||
|
self.uniffiCloneHandle(),
|
||||||
|
FfiConverterString.lower(milestone),uniffiCallStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open func setPullStatus(status: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
open func setPullStatus(status: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_gotcha_core_fn_method_gotchacore_set_pull_status(
|
uniffi_gotcha_core_fn_method_gotchacore_set_pull_status(
|
||||||
@@ -1977,12 +2017,14 @@ public func FfiConverterTypeLabelRow_lower(_ value: LabelRow) -> RustBuffer {
|
|||||||
public struct MilestonePage: Equatable, Hashable {
|
public struct MilestonePage: Equatable, Hashable {
|
||||||
public var milestone: MilestoneRow
|
public var milestone: MilestoneRow
|
||||||
public var issues: [IssueRow]
|
public var issues: [IssueRow]
|
||||||
|
public var pulls: [PullRow]
|
||||||
|
|
||||||
// Default memberwise initializers are never public by default, so we
|
// Default memberwise initializers are never public by default, so we
|
||||||
// declare one manually.
|
// declare one manually.
|
||||||
public init(milestone: MilestoneRow, issues: [IssueRow]) {
|
public init(milestone: MilestoneRow, issues: [IssueRow], pulls: [PullRow]) {
|
||||||
self.milestone = milestone
|
self.milestone = milestone
|
||||||
self.issues = issues
|
self.issues = issues
|
||||||
|
self.pulls = pulls
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2002,13 +2044,15 @@ public struct FfiConverterTypeMilestonePage: FfiConverterRustBuffer {
|
|||||||
return
|
return
|
||||||
try MilestonePage(
|
try MilestonePage(
|
||||||
milestone: FfiConverterTypeMilestoneRow.read(from: &buf),
|
milestone: FfiConverterTypeMilestoneRow.read(from: &buf),
|
||||||
issues: FfiConverterSequenceTypeIssueRow.read(from: &buf)
|
issues: FfiConverterSequenceTypeIssueRow.read(from: &buf),
|
||||||
|
pulls: FfiConverterSequenceTypePullRow.read(from: &buf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func write(_ value: MilestonePage, into buf: inout [UInt8]) {
|
public static func write(_ value: MilestonePage, into buf: inout [UInt8]) {
|
||||||
FfiConverterTypeMilestoneRow.write(value.milestone, into: &buf)
|
FfiConverterTypeMilestoneRow.write(value.milestone, into: &buf)
|
||||||
FfiConverterSequenceTypeIssueRow.write(value.issues, into: &buf)
|
FfiConverterSequenceTypeIssueRow.write(value.issues, into: &buf)
|
||||||
|
FfiConverterSequenceTypePullRow.write(value.pulls, into: &buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2102,6 +2146,60 @@ public func FfiConverterTypeMilestoneRow_lower(_ value: MilestoneRow) -> RustBuf
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public struct PullFilterOptions: Equatable, Hashable {
|
||||||
|
public var milestones: [String]
|
||||||
|
public var selectedMilestone: String
|
||||||
|
|
||||||
|
// Default memberwise initializers are never public by default, so we
|
||||||
|
// declare one manually.
|
||||||
|
public init(milestones: [String], selectedMilestone: String) {
|
||||||
|
self.milestones = milestones
|
||||||
|
self.selectedMilestone = selectedMilestone
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if compiler(>=6)
|
||||||
|
extension PullFilterOptions: Sendable {}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public struct FfiConverterTypePullFilterOptions: FfiConverterRustBuffer {
|
||||||
|
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PullFilterOptions {
|
||||||
|
return
|
||||||
|
try PullFilterOptions(
|
||||||
|
milestones: FfiConverterSequenceString.read(from: &buf),
|
||||||
|
selectedMilestone: FfiConverterString.read(from: &buf)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func write(_ value: PullFilterOptions, into buf: inout [UInt8]) {
|
||||||
|
FfiConverterSequenceString.write(value.milestones, into: &buf)
|
||||||
|
FfiConverterString.write(value.selectedMilestone, into: &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypePullFilterOptions_lift(_ buf: RustBuffer) throws -> PullFilterOptions {
|
||||||
|
return try FfiConverterTypePullFilterOptions.lift(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if swift(>=5.8)
|
||||||
|
@_documentation(visibility: private)
|
||||||
|
#endif
|
||||||
|
public func FfiConverterTypePullFilterOptions_lower(_ value: PullFilterOptions) -> RustBuffer {
|
||||||
|
return FfiConverterTypePullFilterOptions.lower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public struct PullPage: Equatable, Hashable {
|
public struct PullPage: Equatable, Hashable {
|
||||||
public var title: String
|
public var title: String
|
||||||
public var meta: String
|
public var meta: String
|
||||||
@@ -3153,6 +3251,12 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_diff() != 61384) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_diff() != 61384) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_filters() != 10064) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_method_gotchacore_pull_filters_active() != 20260) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_pulls() != 13092) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_pulls() != 13092) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
@@ -3180,6 +3284,9 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_set_issue_status() != 44445) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_set_issue_status() != 44445) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
if (uniffi_gotcha_core_checksum_method_gotchacore_set_pull_filters() != 39023) {
|
||||||
|
return InitializationResult.apiChecksumMismatch
|
||||||
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_set_pull_status() != 1356) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_set_pull_status() != 1356) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,6 +334,16 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull(uint64_t ptr, RustBuffer o
|
|||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull_diff(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, RustBuffer path
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull_diff(uint64_t ptr, RustBuffer owner, RustBuffer repository, int64_t number, RustBuffer path
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_FILTERS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_FILTERS
|
||||||
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pull_filters(uint64_t ptr
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_FILTERS_ACTIVE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULL_FILTERS_ACTIVE
|
||||||
|
int8_t uniffi_gotcha_core_fn_method_gotchacore_pull_filters_active(uint64_t ptr, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_PULLS
|
||||||
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr
|
uint64_t uniffi_gotcha_core_fn_method_gotchacore_pulls(uint64_t ptr
|
||||||
@@ -379,6 +389,11 @@ void uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(uint64_t ptr, Rus
|
|||||||
void uniffi_gotcha_core_fn_method_gotchacore_set_issue_status(uint64_t ptr, RustBuffer status, RustCallStatus *_Nonnull out_status
|
void uniffi_gotcha_core_fn_method_gotchacore_set_issue_status(uint64_t ptr, RustBuffer status, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_FILTERS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_FILTERS
|
||||||
|
void uniffi_gotcha_core_fn_method_gotchacore_set_pull_filters(uint64_t ptr, RustBuffer milestone, RustCallStatus *_Nonnull out_status
|
||||||
|
);
|
||||||
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_STATUS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_STATUS
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_STATUS
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_STATUS
|
||||||
void uniffi_gotcha_core_fn_method_gotchacore_set_pull_status(uint64_t ptr, RustBuffer status, RustCallStatus *_Nonnull out_status
|
void uniffi_gotcha_core_fn_method_gotchacore_set_pull_status(uint64_t ptr, RustBuffer status, RustCallStatus *_Nonnull out_status
|
||||||
@@ -747,6 +762,18 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pull(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_DIFF
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_DIFF
|
||||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pull_diff(void
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pull_diff(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_FILTERS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_FILTERS
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pull_filters(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_FILTERS_ACTIVE
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULL_FILTERS_ACTIVE
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_pull_filters_active(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULLS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_PULLS
|
||||||
@@ -801,6 +828,12 @@ uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_issue_filters(void
|
|||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_ISSUE_STATUS
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_ISSUE_STATUS
|
||||||
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_issue_status(void
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_issue_status(void
|
||||||
|
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_PULL_FILTERS
|
||||||
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_PULL_FILTERS
|
||||||
|
uint16_t uniffi_gotcha_core_checksum_method_gotchacore_set_pull_filters(void
|
||||||
|
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_PULL_STATUS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_CHECKSUM_METHOD_GOTCHACORE_SET_PULL_STATUS
|
||||||
|
|||||||
@@ -426,18 +426,32 @@ final class MilestoneViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int { page == nil ? 0 : 2 }
|
override func numberOfSections(in tableView: UITableView) -> Int { page == nil ? 0 : 3 }
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
section == 0 ? 1 : page?.issues.count ?? 0
|
switch section {
|
||||||
|
case 0: 1
|
||||||
|
case 1: page?.issues.count ?? 0
|
||||||
|
default: page?.pulls.count ?? 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||||
section == 1 ? "Issues" : nil
|
switch section {
|
||||||
|
case 1: "Issues"
|
||||||
|
case 2: "Pull Requests"
|
||||||
|
default: nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||||
section == 1 && page?.issues.isEmpty == true ? "No issues are assigned to this milestone." : nil
|
if section == 1, page?.issues.isEmpty == true {
|
||||||
|
return "No issues are assigned to this milestone."
|
||||||
|
}
|
||||||
|
if section == 2, page?.pulls.isEmpty == true {
|
||||||
|
return "No pull requests are assigned to this milestone."
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(
|
override func tableView(
|
||||||
@@ -453,23 +467,42 @@ final class MilestoneViewController: RefreshingTableViewController {
|
|||||||
cell.configure(page.milestone, disclosure: false)
|
cell.configure(page.milestone, disclosure: false)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
|
if indexPath.section == 1 {
|
||||||
cell.configure(page.issues[indexPath.row])
|
let cell = tableView.dequeueReusableCell(withIdentifier: "issue", for: indexPath) as! IssueCell
|
||||||
|
cell.configure(page.issues[indexPath.row])
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: "pull")
|
||||||
|
?? UITableViewCell(style: .subtitle, reuseIdentifier: "pull")
|
||||||
|
let pull = page.pulls[indexPath.row]
|
||||||
|
configureTextCell(cell, title: pull.title, detail: "\(pull.summary)\n\(pull.meta)")
|
||||||
|
cell.accessoryType = .disclosureIndicator
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
guard indexPath.section == 1, let issue = page?.issues[indexPath.row] else { return }
|
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
navigationController?.pushViewController(
|
if indexPath.section == 1, let issue = page?.issues[indexPath.row] {
|
||||||
IssueViewController(
|
navigationController?.pushViewController(
|
||||||
context: context,
|
IssueViewController(
|
||||||
owner: owner,
|
context: context,
|
||||||
repository: repository,
|
owner: owner,
|
||||||
number: issue.number
|
repository: repository,
|
||||||
),
|
number: issue.number
|
||||||
animated: true
|
),
|
||||||
)
|
animated: true
|
||||||
|
)
|
||||||
|
} else if indexPath.section == 2, let pull = page?.pulls[indexPath.row] {
|
||||||
|
navigationController?.pushViewController(
|
||||||
|
PullViewController(
|
||||||
|
context: context,
|
||||||
|
owner: pull.owner,
|
||||||
|
repository: pull.repository,
|
||||||
|
number: pull.number
|
||||||
|
),
|
||||||
|
animated: true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,6 +555,8 @@ final class MilestoneCell: UITableViewCell {
|
|||||||
final class PullsViewController: RefreshingTableViewController {
|
final class PullsViewController: RefreshingTableViewController {
|
||||||
private let context: AppContext
|
private let context: AppContext
|
||||||
private var rows: [PullRow] = []
|
private var rows: [PullRow] = []
|
||||||
|
private var filterOptions: PullFilterOptions?
|
||||||
|
private var filterTask: Task<Void, Never>?
|
||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -532,6 +567,8 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||||
|
|
||||||
|
deinit { filterTask?.cancel() }
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||||
@@ -557,6 +594,11 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
refreshControl?.endRefreshing()
|
refreshControl?.endRefreshing()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
loadFilterOptions()
|
||||||
|
loadPulls(refreshing: refreshing)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPulls(refreshing: Bool) {
|
||||||
beginLoading(refreshing: refreshing)
|
beginLoading(refreshing: refreshing)
|
||||||
loadingTask?.cancel()
|
loadingTask?.cancel()
|
||||||
loadingTask = Task {
|
loadingTask = Task {
|
||||||
@@ -567,7 +609,9 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
tableView.backgroundView = rows.isEmpty
|
tableView.backgroundView = rows.isEmpty
|
||||||
? EmptyBackgroundView(
|
? EmptyBackgroundView(
|
||||||
title: "No \(status) pull requests",
|
title: "No \(status) pull requests",
|
||||||
detail: "No pull requests match the selected status."
|
detail: filtersActive
|
||||||
|
? "No pull requests match the selected filters."
|
||||||
|
: "No pull requests match the selected status."
|
||||||
)
|
)
|
||||||
: nil
|
: nil
|
||||||
} catch {
|
} catch {
|
||||||
@@ -577,6 +621,19 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadFilterOptions() {
|
||||||
|
filterTask?.cancel()
|
||||||
|
filterTask = Task {
|
||||||
|
do {
|
||||||
|
filterOptions = try await context.core.pullFilters()
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
updateFilterMenu()
|
||||||
|
} catch {
|
||||||
|
if !Task.isCancelled { show(error: error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
rows.count
|
rows.count
|
||||||
}
|
}
|
||||||
@@ -617,22 +674,74 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
|
|
||||||
private func updateFilterMenu() {
|
private func updateFilterMenu() {
|
||||||
let current = context.core.settings().pullStatus
|
let current = context.core.settings().pullStatus
|
||||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
let status = UIMenu(
|
||||||
image: context.symbol("line.3.horizontal.decrease.circle"),
|
title: "Status",
|
||||||
menu: UIMenu(children: ["open", "closed"].map { status in
|
image: context.symbol("circle.lefthalf.filled"),
|
||||||
|
options: .singleSelection,
|
||||||
|
children: ["open", "closed"].map { status in
|
||||||
UIAction(title: status.capitalized, state: current == status ? .on : .off) {
|
UIAction(title: status.capitalized, state: current == status ? .on : .off) {
|
||||||
[weak self] _ in
|
[weak self] _ in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
do {
|
do {
|
||||||
try self.context.core.setPullStatus(status: status)
|
try self.context.core.setPullStatus(status: status)
|
||||||
self.updateFilterMenu()
|
self.updateFilterMenu()
|
||||||
self.loadContent(refreshing: false)
|
self.loadPulls(refreshing: false)
|
||||||
} catch {
|
} catch {
|
||||||
self.show(error: error)
|
self.show(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
)
|
)
|
||||||
|
let milestone: UIMenuElement = filterOptions.map(milestoneMenu)
|
||||||
|
?? UIAction(title: "Loading milestones…", attributes: .disabled) { _ in }
|
||||||
|
let item = navigationItem.rightBarButtonItem ?? UIBarButtonItem(
|
||||||
|
image: context.symbol("line.3.horizontal.decrease.circle")
|
||||||
|
)
|
||||||
|
item.menu = UIMenu(children: [status, milestone])
|
||||||
|
item.accessibilityLabel = "Filter pull requests"
|
||||||
|
navigationItem.rightBarButtonItem = item
|
||||||
|
updateFilterTint()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func milestoneMenu(_ options: PullFilterOptions) -> UIMenu {
|
||||||
|
let selected = options.selectedMilestone
|
||||||
|
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
|
||||||
|
[weak self] _ in self?.selectMilestone("")
|
||||||
|
}
|
||||||
|
let milestones = options.milestones.map { milestone in
|
||||||
|
UIAction(title: milestone, state: selected == milestone ? .on : .off) {
|
||||||
|
[weak self] _ in self?.selectMilestone(milestone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UIMenu(
|
||||||
|
title: "Milestone",
|
||||||
|
image: context.symbol("flag"),
|
||||||
|
options: .singleSelection,
|
||||||
|
children: [all] + milestones
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectMilestone(_ milestone: String) {
|
||||||
|
filterTask?.cancel()
|
||||||
|
do {
|
||||||
|
try context.core.setPullFilters(milestone: milestone)
|
||||||
|
filterOptions?.selectedMilestone = milestone
|
||||||
|
updateFilterMenu()
|
||||||
|
loadPulls(refreshing: false)
|
||||||
|
} catch {
|
||||||
|
show(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var filtersActive: Bool {
|
||||||
|
(try? context.core.pullFiltersActive()) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateFilterTint() {
|
||||||
|
navigationItem.rightBarButtonItem?.tintColor = filtersActive ? .tintColor : .secondaryLabel
|
||||||
|
navigationItem.rightBarButtonItem?.accessibilityValue = filtersActive
|
||||||
|
? "Filters active"
|
||||||
|
: "Default filters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user