Add server-side work item search
This commit is contained in:
25
TESTING.md
25
TESTING.md
@@ -157,15 +157,20 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
|||||||
- [ ] Select one and then multiple labels in the issue filter. Checkmarks and
|
- [ ] Select one and then multiple labels in the issue filter. Checkmarks and
|
||||||
rows update without dismissing the menu; clearing every label restores
|
rows update without dismissing the menu; clearing every label restores
|
||||||
the unfiltered label result.
|
the unfiltered label result.
|
||||||
|
- [ ] Open **Search Text** in the issue filter, enter a term, and verify the
|
||||||
|
server-filtered rows and menu subtitle update. Combine it with Closed and
|
||||||
|
one or more labels and verify every filter applies together.
|
||||||
- [ ] Select a milestone and then All Milestones; the checkmark and issue rows
|
- [ ] Select a milestone and then All Milestones; the checkmark and issue rows
|
||||||
update. Relaunch and revisit repositories to verify label and milestone
|
update. Relaunch and revisit repositories to verify label and milestone
|
||||||
selections persist independently per repository and status persists.
|
selections persist independently per repository and status persists.
|
||||||
- [ ] The issue-filter icon uses a neutral color for Open + All Milestones + no
|
- [ ] The issue-filter icon uses a neutral color for Open + All Milestones + no
|
||||||
labels, and the app accent color whenever any non-default filter is set.
|
labels, and the app accent color whenever any non-default filter is set.
|
||||||
- [ ] In the issue filter panel, the Status, Milestone, and Labels icons use the
|
- [ ] In the issue filter panel, the Search Text, Status, Milestone, and Labels
|
||||||
app accent color independently when their filter is non-default. **Clear
|
icons use the app accent color independently when their filter is
|
||||||
Filters** restores Open + All Milestones + no labels, refreshes the rows,
|
non-default. Search text remains while navigating during the current app
|
||||||
returns every filter icon to neutral, and remains cleared after relaunch.
|
session but is empty after relaunch. **Clear Filters** restores empty
|
||||||
|
search + Open + All Milestones + no labels, refreshes the rows, and
|
||||||
|
returns every filter icon to neutral.
|
||||||
- [ ] Tap the add button. **New Issue** appears as a native modal with Cancel
|
- [ ] Tap the add button. **New Issue** appears as a native modal with Cancel
|
||||||
and Save, title and Markdown body fields, multi-select labels, a
|
and Save, title and Markdown body fields, multi-select labels, a
|
||||||
single-select optional milestone, and an optional inline due-date picker.
|
single-select optional milestone, and an optional inline due-date picker.
|
||||||
@@ -257,15 +262,19 @@ 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.
|
||||||
|
- [ ] Open **Search Text** in the pull-request filter, enter a term, and verify
|
||||||
|
the server-filtered rows and menu subtitle update. Combine it with Closed
|
||||||
|
and a milestone and verify every filter applies together.
|
||||||
- [ ] Select a milestone and then All Milestones; the checkmark and pull-request
|
- [ ] Select a milestone and then All Milestones; the checkmark and pull-request
|
||||||
rows update, and the milestone selection persists independently per
|
rows update, and the milestone selection persists independently per
|
||||||
server after relaunch.
|
server after relaunch.
|
||||||
- [ ] The pull-request filter icon uses a neutral color for Open + All
|
- [ ] The pull-request filter icon uses a neutral color for Open + All
|
||||||
Milestones and the app accent color whenever either filter is non-default.
|
Milestones and the app accent color whenever either filter is non-default.
|
||||||
- [ ] In the pull-request filter panel, Status and Milestone icons use the app
|
- [ ] In the pull-request filter panel, Search Text, Status, and Milestone icons
|
||||||
accent color independently when non-default. **Clear Filters** restores
|
use the app accent color independently when non-default. Search text
|
||||||
Open + All Milestones, refreshes the rows, returns both icons to neutral,
|
remains while navigating during the current app session but is empty
|
||||||
and remains cleared after relaunch.
|
after relaunch. **Clear Filters** restores empty search + Open + All
|
||||||
|
Milestones, refreshes the rows, and returns every icon to neutral.
|
||||||
- [ ] 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. Open rows show a green
|
count, and draft/merged state where applicable. Open rows show a green
|
||||||
icon, closed rows show a purple icon, and VoiceOver announces the state.
|
icon, closed rows show a purple icon, and VoiceOver announces the state.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use gotcha_gitea::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::{
|
domain::{
|
||||||
HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, MilestoneDetails,
|
HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, IssueFilter,
|
||||||
MilestoneDraft, Page, PullDetails, RepositoryData, Server,
|
MilestoneDetails, MilestoneDraft, Page, PullDetails, RepositoryData, Server,
|
||||||
},
|
},
|
||||||
presentation::compact_date,
|
presentation::compact_date,
|
||||||
};
|
};
|
||||||
@@ -46,8 +46,7 @@ pub async fn load_issues(
|
|||||||
owner: &str,
|
owner: &str,
|
||||||
repository: &str,
|
repository: &str,
|
||||||
status: &str,
|
status: &str,
|
||||||
labels: &[String],
|
filter: &IssueFilter,
|
||||||
milestone: &str,
|
|
||||||
page: i32,
|
page: i32,
|
||||||
) -> Result<Page<models::Issue>, String> {
|
) -> Result<Page<models::Issue>, String> {
|
||||||
client(server)?
|
client(server)?
|
||||||
@@ -55,8 +54,10 @@ pub async fn load_issues(
|
|||||||
&scope(owner, repository)?,
|
&scope(owner, repository)?,
|
||||||
&IssueQuery {
|
&IssueQuery {
|
||||||
state: status.into(),
|
state: status.into(),
|
||||||
labels: (!labels.is_empty()).then(|| labels.join(",")),
|
labels: (!filter.labels.is_empty())
|
||||||
milestones: (!milestone.is_empty()).then(|| milestone.into()),
|
.then(|| filter.labels.iter().cloned().collect::<Vec<_>>().join(",")),
|
||||||
|
milestones: (!filter.milestone.is_empty()).then(|| filter.milestone.clone()),
|
||||||
|
keyword: (!filter.search_text.is_empty()).then(|| filter.search_text.clone()),
|
||||||
page,
|
page,
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -150,12 +151,14 @@ pub async fn load_pulls(
|
|||||||
server: &Server,
|
server: &Server,
|
||||||
status: &str,
|
status: &str,
|
||||||
milestone: &str,
|
milestone: &str,
|
||||||
|
search_text: &str,
|
||||||
page: i32,
|
page: i32,
|
||||||
) -> Result<Page<models::Issue>, String> {
|
) -> Result<Page<models::Issue>, String> {
|
||||||
client(server)?
|
client(server)?
|
||||||
.search_pulls(
|
.search_pulls(
|
||||||
status,
|
status,
|
||||||
(!milestone.is_empty()).then_some(milestone),
|
(!milestone.is_empty()).then_some(milestone),
|
||||||
|
(!search_text.is_empty()).then_some(search_text),
|
||||||
page,
|
page,
|
||||||
PAGE_SIZE,
|
PAGE_SIZE,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ pub struct IssueFilter {
|
|||||||
pub milestone: String,
|
pub milestone: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub labels: BTreeSet<String>,
|
pub labels: BTreeSet<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub search_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IssueFilter {
|
impl IssueFilter {
|
||||||
@@ -96,6 +98,8 @@ impl IssueFilter {
|
|||||||
pub struct PullFilter {
|
pub struct PullFilter {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub milestone: String,
|
pub milestone: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub search_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PullFilter {
|
impl PullFilter {
|
||||||
@@ -185,8 +189,44 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
PullFilter {
|
PullFilter {
|
||||||
milestone: "v1".into(),
|
milestone: "v1".into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.is_active("open")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
IssueFilter {
|
||||||
|
search_text: "needle".into(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.is_active("open")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
PullFilter {
|
||||||
|
search_text: "needle".into(),
|
||||||
|
..Default::default()
|
||||||
}
|
}
|
||||||
.is_active("open")
|
.is_active("open")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_text_is_session_only() {
|
||||||
|
let issue = IssueFilter {
|
||||||
|
search_text: "needle".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let pull = PullFilter {
|
||||||
|
search_text: "needle".into(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!serde_json::to_string(&issue).unwrap().contains("needle"));
|
||||||
|
assert!(!serde_json::to_string(&pull).unwrap().contains("needle"));
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<IssueFilter>(r#"{"milestone":"v1"}"#)
|
||||||
|
.unwrap()
|
||||||
|
.search_text,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,14 +268,12 @@ impl GotchaCore {
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let labels: Vec<_> = filter.labels.into_iter().collect();
|
|
||||||
let page = load_issues(
|
let page = load_issues(
|
||||||
&server,
|
&server,
|
||||||
&owner,
|
&owner,
|
||||||
&repository,
|
&repository,
|
||||||
&status,
|
&status,
|
||||||
&labels,
|
&filter,
|
||||||
&filter.milestone,
|
|
||||||
valid_page(page)?,
|
valid_page(page)?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -333,6 +331,7 @@ impl GotchaCore {
|
|||||||
repository: String,
|
repository: String,
|
||||||
milestone: String,
|
milestone: String,
|
||||||
labels: Vec<String>,
|
labels: Vec<String>,
|
||||||
|
search_text: String,
|
||||||
) -> Result<(), GotchaError> {
|
) -> Result<(), GotchaError> {
|
||||||
let owner = owner.trim();
|
let owner = owner.trim();
|
||||||
let repository = repository.trim();
|
let repository = repository.trim();
|
||||||
@@ -345,6 +344,7 @@ impl GotchaCore {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|label| !label.is_empty())
|
.filter(|label| !label.is_empty())
|
||||||
.collect(),
|
.collect(),
|
||||||
|
search_text: search_text.trim().into(),
|
||||||
};
|
};
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
let server = state
|
let server = state
|
||||||
@@ -585,8 +585,15 @@ impl GotchaCore {
|
|||||||
Ok(filter.is_active(&state.preferences.pull_status))
|
Ok(filter.is_active(&state.preferences.pull_status))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_pull_filters(&self, milestone: String) -> Result<(), GotchaError> {
|
pub fn set_pull_filters(
|
||||||
let filter = PullFilter { milestone };
|
&self,
|
||||||
|
milestone: String,
|
||||||
|
search_text: String,
|
||||||
|
) -> Result<(), GotchaError> {
|
||||||
|
let filter = PullFilter {
|
||||||
|
milestone,
|
||||||
|
search_text: search_text.trim().into(),
|
||||||
|
};
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
let server = state
|
let server = state
|
||||||
.active_server
|
.active_server
|
||||||
@@ -627,7 +634,14 @@ impl GotchaCore {
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let page = load_pulls(&server, &status, &filter.milestone, valid_page(page)?).await?;
|
let page = load_pulls(
|
||||||
|
&server,
|
||||||
|
&status,
|
||||||
|
&filter.milestone,
|
||||||
|
&filter.search_text,
|
||||||
|
valid_page(page)?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(PullListPage {
|
Ok(PullListPage {
|
||||||
rows: pull_rows(&page.items),
|
rows: pull_rows(&page.items),
|
||||||
has_more: page.has_more,
|
has_more: page.has_more,
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ pub struct IssueFilterOptions {
|
|||||||
pub unavailable_labels: Vec<String>,
|
pub unavailable_labels: Vec<String>,
|
||||||
pub selected_milestone: String,
|
pub selected_milestone: String,
|
||||||
pub selected_labels: Vec<String>,
|
pub selected_labels: Vec<String>,
|
||||||
|
pub search_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
@@ -153,6 +154,7 @@ pub struct MilestoneEditorPage {
|
|||||||
pub struct PullFilterOptions {
|
pub struct PullFilterOptions {
|
||||||
pub milestones: Vec<String>,
|
pub milestones: Vec<String>,
|
||||||
pub selected_milestone: String,
|
pub selected_milestone: String,
|
||||||
|
pub search_text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, uniffi::Record)]
|
#[derive(Clone, uniffi::Record)]
|
||||||
@@ -365,6 +367,7 @@ pub fn issue_filter_options(
|
|||||||
unavailable_labels,
|
unavailable_labels,
|
||||||
selected_milestone: filter.milestone.clone(),
|
selected_milestone: filter.milestone.clone(),
|
||||||
selected_labels: filter.labels.iter().cloned().collect(),
|
selected_labels: filter.labels.iter().cloned().collect(),
|
||||||
|
search_text: filter.search_text.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +422,7 @@ pub fn pull_filter_options(milestones: &[String], filter: &PullFilter) -> PullFi
|
|||||||
PullFilterOptions {
|
PullFilterOptions {
|
||||||
milestones,
|
milestones,
|
||||||
selected_milestone: filter.milestone.clone(),
|
selected_milestone: filter.milestone.clone(),
|
||||||
|
search_text: filter.search_text.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1365,6 +1369,7 @@ mod tests {
|
|||||||
let filter = IssueFilter {
|
let filter = IssueFilter {
|
||||||
milestone: "v3".into(),
|
milestone: "v3".into(),
|
||||||
labels: ["bug".into(), "retired".into()].into(),
|
labels: ["bug".into(), "retired".into()].into(),
|
||||||
|
search_text: "login".into(),
|
||||||
};
|
};
|
||||||
let options = issue_filter_options(
|
let options = issue_filter_options(
|
||||||
&[
|
&[
|
||||||
@@ -1395,15 +1400,18 @@ 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");
|
||||||
|
assert_eq!(options.search_text, "login");
|
||||||
|
|
||||||
let pull_options = pull_filter_options(
|
let pull_options = pull_filter_options(
|
||||||
&["v2".into(), "v1".into()],
|
&["v2".into(), "v1".into()],
|
||||||
&PullFilter {
|
&PullFilter {
|
||||||
milestone: "v3".into(),
|
milestone: "v3".into(),
|
||||||
|
search_text: "review".into(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert_eq!(pull_options.milestones, ["v1", "v2", "v3"]);
|
assert_eq!(pull_options.milestones, ["v1", "v2", "v3"]);
|
||||||
assert_eq!(pull_options.selected_milestone, "v3");
|
assert_eq!(pull_options.selected_milestone, "v3");
|
||||||
|
assert_eq!(pull_options.search_text, "review");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ impl Client {
|
|||||||
&self,
|
&self,
|
||||||
state: &str,
|
state: &str,
|
||||||
milestone: Option<&str>,
|
milestone: Option<&str>,
|
||||||
|
keyword: Option<&str>,
|
||||||
page: i32,
|
page: i32,
|
||||||
limit: i32,
|
limit: i32,
|
||||||
) -> Result<Page<models::Issue>> {
|
) -> Result<Page<models::Issue>> {
|
||||||
@@ -84,7 +85,7 @@ impl Client {
|
|||||||
Some(state),
|
Some(state),
|
||||||
None,
|
None,
|
||||||
milestone,
|
milestone,
|
||||||
None,
|
keyword,
|
||||||
None,
|
None,
|
||||||
Some("pulls"),
|
Some("pulls"),
|
||||||
None,
|
None,
|
||||||
@@ -122,7 +123,7 @@ impl Client {
|
|||||||
let mut pulls = Vec::new();
|
let mut pulls = Vec::new();
|
||||||
for page in 1.. {
|
for page in 1.. {
|
||||||
let batch = self
|
let batch = self
|
||||||
.search_pulls(state, None, page, DEFAULT_PAGE_SIZE)
|
.search_pulls(state, None, None, page, DEFAULT_PAGE_SIZE)
|
||||||
.await?;
|
.await?;
|
||||||
pulls.extend(batch.items);
|
pulls.extend(batch.items);
|
||||||
if !batch.has_more {
|
if !batch.has_more {
|
||||||
|
|||||||
@@ -665,11 +665,11 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
|
|||||||
|
|
||||||
func setAppearance(index: UInt32) throws
|
func setAppearance(index: UInt32) throws
|
||||||
|
|
||||||
func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String]) throws
|
func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String], searchText: String) throws
|
||||||
|
|
||||||
func setIssueStatus(status: String) throws
|
func setIssueStatus(status: String) throws
|
||||||
|
|
||||||
func setPullFilters(milestone: String) throws
|
func setPullFilters(milestone: String, searchText: String) throws
|
||||||
|
|
||||||
func setPullStatus(status: String) throws
|
func setPullStatus(status: String) throws
|
||||||
|
|
||||||
@@ -1176,14 +1176,15 @@ open func setAppearance(index: UInt32)throws {try rustCallWithError(FfiConvert
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String])throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
open func setIssueFilters(owner: String, repository: String, milestone: String, labels: [String], searchText: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(
|
uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(
|
||||||
self.uniffiCloneHandle(),
|
self.uniffiCloneHandle(),
|
||||||
FfiConverterString.lower(owner),
|
FfiConverterString.lower(owner),
|
||||||
FfiConverterString.lower(repository),
|
FfiConverterString.lower(repository),
|
||||||
FfiConverterString.lower(milestone),
|
FfiConverterString.lower(milestone),
|
||||||
FfiConverterSequenceString.lower(labels),uniffiCallStatus
|
FfiConverterSequenceString.lower(labels),
|
||||||
|
FfiConverterString.lower(searchText),uniffiCallStatus
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1197,11 +1198,12 @@ open func setIssueStatus(status: String)throws {try rustCallWithError(FfiConve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open func setPullFilters(milestone: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
open func setPullFilters(milestone: String, searchText: String)throws {try rustCallWithError(FfiConverterTypeGotchaError_lift) {
|
||||||
uniffiCallStatus in
|
uniffiCallStatus in
|
||||||
uniffi_gotcha_core_fn_method_gotchacore_set_pull_filters(
|
uniffi_gotcha_core_fn_method_gotchacore_set_pull_filters(
|
||||||
self.uniffiCloneHandle(),
|
self.uniffiCloneHandle(),
|
||||||
FfiConverterString.lower(milestone),uniffiCallStatus
|
FfiConverterString.lower(milestone),
|
||||||
|
FfiConverterString.lower(searchText),uniffiCallStatus
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2117,15 +2119,17 @@ public struct IssueFilterOptions: Equatable, Hashable {
|
|||||||
public var unavailableLabels: [String]
|
public var unavailableLabels: [String]
|
||||||
public var selectedMilestone: String
|
public var selectedMilestone: String
|
||||||
public var selectedLabels: [String]
|
public var selectedLabels: [String]
|
||||||
|
public var searchText: String
|
||||||
|
|
||||||
// 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(milestones: [String], labels: [String], unavailableLabels: [String], selectedMilestone: String, selectedLabels: [String]) {
|
public init(milestones: [String], labels: [String], unavailableLabels: [String], selectedMilestone: String, selectedLabels: [String], searchText: String) {
|
||||||
self.milestones = milestones
|
self.milestones = milestones
|
||||||
self.labels = labels
|
self.labels = labels
|
||||||
self.unavailableLabels = unavailableLabels
|
self.unavailableLabels = unavailableLabels
|
||||||
self.selectedMilestone = selectedMilestone
|
self.selectedMilestone = selectedMilestone
|
||||||
self.selectedLabels = selectedLabels
|
self.selectedLabels = selectedLabels
|
||||||
|
self.searchText = searchText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2148,7 +2152,8 @@ public struct FfiConverterTypeIssueFilterOptions: FfiConverterRustBuffer {
|
|||||||
labels: FfiConverterSequenceString.read(from: &buf),
|
labels: FfiConverterSequenceString.read(from: &buf),
|
||||||
unavailableLabels: FfiConverterSequenceString.read(from: &buf),
|
unavailableLabels: FfiConverterSequenceString.read(from: &buf),
|
||||||
selectedMilestone: FfiConverterString.read(from: &buf),
|
selectedMilestone: FfiConverterString.read(from: &buf),
|
||||||
selectedLabels: FfiConverterSequenceString.read(from: &buf)
|
selectedLabels: FfiConverterSequenceString.read(from: &buf),
|
||||||
|
searchText: FfiConverterString.read(from: &buf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2158,6 +2163,7 @@ public struct FfiConverterTypeIssueFilterOptions: FfiConverterRustBuffer {
|
|||||||
FfiConverterSequenceString.write(value.unavailableLabels, into: &buf)
|
FfiConverterSequenceString.write(value.unavailableLabels, into: &buf)
|
||||||
FfiConverterString.write(value.selectedMilestone, into: &buf)
|
FfiConverterString.write(value.selectedMilestone, into: &buf)
|
||||||
FfiConverterSequenceString.write(value.selectedLabels, into: &buf)
|
FfiConverterSequenceString.write(value.selectedLabels, into: &buf)
|
||||||
|
FfiConverterString.write(value.searchText, into: &buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2696,12 +2702,14 @@ public func FfiConverterTypeMilestoneRow_lower(_ value: MilestoneRow) -> RustBuf
|
|||||||
public struct PullFilterOptions: Equatable, Hashable {
|
public struct PullFilterOptions: Equatable, Hashable {
|
||||||
public var milestones: [String]
|
public var milestones: [String]
|
||||||
public var selectedMilestone: String
|
public var selectedMilestone: String
|
||||||
|
public var searchText: String
|
||||||
|
|
||||||
// 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(milestones: [String], selectedMilestone: String) {
|
public init(milestones: [String], selectedMilestone: String, searchText: String) {
|
||||||
self.milestones = milestones
|
self.milestones = milestones
|
||||||
self.selectedMilestone = selectedMilestone
|
self.selectedMilestone = selectedMilestone
|
||||||
|
self.searchText = searchText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2721,13 +2729,15 @@ public struct FfiConverterTypePullFilterOptions: FfiConverterRustBuffer {
|
|||||||
return
|
return
|
||||||
try PullFilterOptions(
|
try PullFilterOptions(
|
||||||
milestones: FfiConverterSequenceString.read(from: &buf),
|
milestones: FfiConverterSequenceString.read(from: &buf),
|
||||||
selectedMilestone: FfiConverterString.read(from: &buf)
|
selectedMilestone: FfiConverterString.read(from: &buf),
|
||||||
|
searchText: FfiConverterString.read(from: &buf)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func write(_ value: PullFilterOptions, into buf: inout [UInt8]) {
|
public static func write(_ value: PullFilterOptions, into buf: inout [UInt8]) {
|
||||||
FfiConverterSequenceString.write(value.milestones, into: &buf)
|
FfiConverterSequenceString.write(value.milestones, into: &buf)
|
||||||
FfiConverterString.write(value.selectedMilestone, into: &buf)
|
FfiConverterString.write(value.selectedMilestone, into: &buf)
|
||||||
|
FfiConverterString.write(value.searchText, into: &buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4211,13 +4221,13 @@ private let initializationResult: InitializationResult = {
|
|||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_set_appearance() != 61293) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_set_appearance() != 61293) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
if (uniffi_gotcha_core_checksum_method_gotchacore_set_issue_filters() != 45204) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_set_issue_filters() != 24891) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
return InitializationResult.apiChecksumMismatch
|
||||||
}
|
}
|
||||||
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) {
|
if (uniffi_gotcha_core_checksum_method_gotchacore_set_pull_filters() != 25211) {
|
||||||
return InitializationResult.apiChecksumMismatch
|
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) {
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ void uniffi_gotcha_core_fn_method_gotchacore_set_appearance(uint64_t ptr, uint32
|
|||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_FILTERS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_FILTERS
|
||||||
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_FILTERS
|
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_FILTERS
|
||||||
void uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer milestone, RustBuffer labels, RustCallStatus *_Nonnull out_status
|
void uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer milestone, RustBuffer labels, RustBuffer search_text, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_STATUS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_ISSUE_STATUS
|
||||||
@@ -426,7 +426,7 @@ void uniffi_gotcha_core_fn_method_gotchacore_set_issue_status(uint64_t ptr, Rust
|
|||||||
#endif
|
#endif
|
||||||
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_FILTERS
|
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_FILTERS
|
||||||
#define 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
|
void uniffi_gotcha_core_fn_method_gotchacore_set_pull_filters(uint64_t ptr, RustBuffer milestone, RustBuffer search_text, RustCallStatus *_Nonnull out_status
|
||||||
);
|
);
|
||||||
#endif
|
#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
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
private extension UIViewController {
|
||||||
|
func promptForSearchText(title: String, current: String, apply: @escaping (String) -> Void) {
|
||||||
|
let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert)
|
||||||
|
alert.addTextField { field in
|
||||||
|
field.text = current
|
||||||
|
field.placeholder = "Search text"
|
||||||
|
field.accessibilityLabel = "Search text"
|
||||||
|
field.clearButtonMode = .whileEditing
|
||||||
|
field.returnKeyType = .search
|
||||||
|
}
|
||||||
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
|
||||||
|
alert.addAction(UIAlertAction(title: "Apply", style: .default) { [weak alert] _ in
|
||||||
|
apply(alert?.textFields?.first?.text ?? "")
|
||||||
|
})
|
||||||
|
present(alert, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class IssuesViewController: RefreshingTableViewController {
|
final class IssuesViewController: RefreshingTableViewController {
|
||||||
private let context: AppContext
|
private let context: AppContext
|
||||||
@@ -149,6 +167,7 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
)
|
)
|
||||||
var children: [UIMenuElement] = [status]
|
var children: [UIMenuElement] = [status]
|
||||||
if let filterOptions {
|
if let filterOptions {
|
||||||
|
children.insert(searchAction(filterOptions), at: 0)
|
||||||
children.append(milestoneMenu(filterOptions))
|
children.append(milestoneMenu(filterOptions))
|
||||||
children.append(labelMenu(filterOptions))
|
children.append(labelMenu(filterOptions))
|
||||||
} else {
|
} else {
|
||||||
@@ -160,6 +179,21 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
updateFilterTint()
|
updateFilterTint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func searchAction(_ options: IssueFilterOptions) -> UIAction {
|
||||||
|
UIAction(
|
||||||
|
title: "Search Text",
|
||||||
|
subtitle: options.searchText.isEmpty ? "Any text" : options.searchText,
|
||||||
|
image: filterMenuImage("magnifyingglass", active: !options.searchText.isEmpty)
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.promptForSearchText(
|
||||||
|
title: "Search Issues",
|
||||||
|
current: options.searchText
|
||||||
|
) { [weak self] searchText in
|
||||||
|
self?.setSearchText(searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu {
|
private func milestoneMenu(_ options: IssueFilterOptions) -> UIMenu {
|
||||||
let selected = options.selectedMilestone
|
let selected = options.selectedMilestone
|
||||||
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
|
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
|
||||||
@@ -192,7 +226,11 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
var labels = Set(options.selectedLabels)
|
var labels = Set(options.selectedLabels)
|
||||||
if labels.remove(label) == nil { labels.insert(label) }
|
if labels.remove(label) == nil { labels.insert(label) }
|
||||||
do {
|
do {
|
||||||
try self.saveFilters(milestone: options.selectedMilestone, labels: labels)
|
try self.saveFilters(
|
||||||
|
milestone: options.selectedMilestone,
|
||||||
|
labels: labels,
|
||||||
|
searchText: options.searchText
|
||||||
|
)
|
||||||
self.filterOptions?.selectedLabels = Array(labels)
|
self.filterOptions?.selectedLabels = Array(labels)
|
||||||
action.state = labels.contains(label) ? .on : .off
|
action.state = labels.contains(label) ? .on : .off
|
||||||
self.updateFilterMenu()
|
self.updateFilterMenu()
|
||||||
@@ -215,7 +253,11 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
guard let options = filterOptions else { return }
|
guard let options = filterOptions else { return }
|
||||||
filterTask?.cancel()
|
filterTask?.cancel()
|
||||||
do {
|
do {
|
||||||
try saveFilters(milestone: milestone, labels: Set(options.selectedLabels))
|
try saveFilters(
|
||||||
|
milestone: milestone,
|
||||||
|
labels: Set(options.selectedLabels),
|
||||||
|
searchText: options.searchText
|
||||||
|
)
|
||||||
filterOptions?.selectedMilestone = milestone
|
filterOptions?.selectedMilestone = milestone
|
||||||
updateFilterMenu()
|
updateFilterMenu()
|
||||||
loadIssues(refreshing: false)
|
loadIssues(refreshing: false)
|
||||||
@@ -224,12 +266,34 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveFilters(milestone: String, labels: Set<String>) throws {
|
private func setSearchText(_ searchText: String) {
|
||||||
|
guard let options = filterOptions else { return }
|
||||||
|
filterTask?.cancel()
|
||||||
|
do {
|
||||||
|
try saveFilters(
|
||||||
|
milestone: options.selectedMilestone,
|
||||||
|
labels: Set(options.selectedLabels),
|
||||||
|
searchText: searchText
|
||||||
|
)
|
||||||
|
filterOptions?.searchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
updateFilterMenu()
|
||||||
|
loadIssues(refreshing: false)
|
||||||
|
} catch {
|
||||||
|
show(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveFilters(
|
||||||
|
milestone: String,
|
||||||
|
labels: Set<String>,
|
||||||
|
searchText: String
|
||||||
|
) throws {
|
||||||
try context.core.setIssueFilters(
|
try context.core.setIssueFilters(
|
||||||
owner: owner,
|
owner: owner,
|
||||||
repository: repository,
|
repository: repository,
|
||||||
milestone: milestone,
|
milestone: milestone,
|
||||||
labels: Array(labels)
|
labels: Array(labels),
|
||||||
|
searchText: searchText
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +316,7 @@ final class IssuesViewController: RefreshingTableViewController {
|
|||||||
try context.core.clearIssueFilters(owner: owner, repository: repository)
|
try context.core.clearIssueFilters(owner: owner, repository: repository)
|
||||||
filterOptions?.selectedMilestone = ""
|
filterOptions?.selectedMilestone = ""
|
||||||
filterOptions?.selectedLabels = []
|
filterOptions?.selectedLabels = []
|
||||||
|
filterOptions?.searchText = ""
|
||||||
updateFilterMenu()
|
updateFilterMenu()
|
||||||
loadIssues(refreshing: false)
|
loadIssues(refreshing: false)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -930,12 +995,33 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
let item = navigationItem.rightBarButtonItem ?? UIBarButtonItem(
|
let item = navigationItem.rightBarButtonItem ?? UIBarButtonItem(
|
||||||
image: context.symbol("line.3.horizontal.decrease.circle")
|
image: context.symbol("line.3.horizontal.decrease.circle")
|
||||||
)
|
)
|
||||||
item.menu = UIMenu(children: [status, milestone, clearFiltersMenu()])
|
var children: [UIMenuElement] = [status]
|
||||||
|
if let filterOptions {
|
||||||
|
children.insert(searchAction(filterOptions), at: 0)
|
||||||
|
}
|
||||||
|
children.append(milestone)
|
||||||
|
children.append(clearFiltersMenu())
|
||||||
|
item.menu = UIMenu(children: children)
|
||||||
item.accessibilityLabel = "Filter pull requests"
|
item.accessibilityLabel = "Filter pull requests"
|
||||||
navigationItem.rightBarButtonItem = item
|
navigationItem.rightBarButtonItem = item
|
||||||
updateFilterTint()
|
updateFilterTint()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func searchAction(_ options: PullFilterOptions) -> UIAction {
|
||||||
|
UIAction(
|
||||||
|
title: "Search Text",
|
||||||
|
subtitle: options.searchText.isEmpty ? "Any text" : options.searchText,
|
||||||
|
image: filterMenuImage("magnifyingglass", active: !options.searchText.isEmpty)
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.promptForSearchText(
|
||||||
|
title: "Search Pull Requests",
|
||||||
|
current: options.searchText
|
||||||
|
) { [weak self] searchText in
|
||||||
|
self?.setSearchText(searchText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func milestoneMenu(_ options: PullFilterOptions) -> UIMenu {
|
private func milestoneMenu(_ options: PullFilterOptions) -> UIMenu {
|
||||||
let selected = options.selectedMilestone
|
let selected = options.selectedMilestone
|
||||||
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
|
let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) {
|
||||||
@@ -955,9 +1041,13 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func selectMilestone(_ milestone: String) {
|
private func selectMilestone(_ milestone: String) {
|
||||||
|
guard let options = filterOptions else { return }
|
||||||
filterTask?.cancel()
|
filterTask?.cancel()
|
||||||
do {
|
do {
|
||||||
try context.core.setPullFilters(milestone: milestone)
|
try context.core.setPullFilters(
|
||||||
|
milestone: milestone,
|
||||||
|
searchText: options.searchText
|
||||||
|
)
|
||||||
filterOptions?.selectedMilestone = milestone
|
filterOptions?.selectedMilestone = milestone
|
||||||
updateFilterMenu()
|
updateFilterMenu()
|
||||||
loadPulls(refreshing: false)
|
loadPulls(refreshing: false)
|
||||||
@@ -966,6 +1056,22 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setSearchText(_ searchText: String) {
|
||||||
|
guard let options = filterOptions else { return }
|
||||||
|
filterTask?.cancel()
|
||||||
|
do {
|
||||||
|
try context.core.setPullFilters(
|
||||||
|
milestone: options.selectedMilestone,
|
||||||
|
searchText: searchText
|
||||||
|
)
|
||||||
|
filterOptions?.searchText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
updateFilterMenu()
|
||||||
|
loadPulls(refreshing: false)
|
||||||
|
} catch {
|
||||||
|
show(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func clearFiltersMenu() -> UIMenu {
|
private func clearFiltersMenu() -> UIMenu {
|
||||||
UIMenu(
|
UIMenu(
|
||||||
options: .displayInline,
|
options: .displayInline,
|
||||||
@@ -984,6 +1090,7 @@ final class PullsViewController: RefreshingTableViewController {
|
|||||||
do {
|
do {
|
||||||
try context.core.clearPullFilters()
|
try context.core.clearPullFilters()
|
||||||
filterOptions?.selectedMilestone = ""
|
filterOptions?.selectedMilestone = ""
|
||||||
|
filterOptions?.searchText = ""
|
||||||
updateFilterMenu()
|
updateFilterMenu()
|
||||||
loadPulls(refreshing: false)
|
loadPulls(refreshing: false)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user