diff --git a/TESTING.md b/TESTING.md index 9a973af..0df258d 100644 --- a/TESTING.md +++ b/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 rows update without dismissing the menu; clearing every label restores 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 update. Relaunch and revisit repositories to verify label and milestone selections persist independently per repository and status persists. - [ ] 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. -- [ ] In the issue filter panel, the Status, Milestone, and Labels icons use the - app accent color independently when their filter is non-default. **Clear - Filters** restores Open + All Milestones + no labels, refreshes the rows, - returns every filter icon to neutral, and remains cleared after relaunch. +- [ ] In the issue filter panel, the Search Text, Status, Milestone, and Labels + icons use the app accent color independently when their filter is + non-default. Search text remains while navigating during the current app + 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 and Save, title and Markdown body fields, multi-select labels, a 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. - [ ] Change the native filter menu between Open and Closed; the checkmark, 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 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. -- [ ] In the pull-request filter panel, Status and Milestone icons use the app - accent color independently when non-default. **Clear Filters** restores - Open + All Milestones, refreshes the rows, returns both icons to neutral, - and remains cleared after relaunch. +- [ ] In the pull-request filter panel, Search Text, Status, and Milestone icons + use the app accent color independently when non-default. Search text + remains while navigating during the current app session but is empty + 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 count, and draft/merged state where applicable. Open rows show a green icon, closed rows show a purple icon, and VoiceOver announces the state. diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 7dcfa8a..9c5cea8 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -4,8 +4,8 @@ use gotcha_gitea::{ use crate::{ domain::{ - HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, MilestoneDetails, - MilestoneDraft, Page, PullDetails, RepositoryData, Server, + HistoryCommit, HomeData, IssueDetails, IssueDraft, IssueEditorData, IssueFilter, + MilestoneDetails, MilestoneDraft, Page, PullDetails, RepositoryData, Server, }, presentation::compact_date, }; @@ -46,8 +46,7 @@ pub async fn load_issues( owner: &str, repository: &str, status: &str, - labels: &[String], - milestone: &str, + filter: &IssueFilter, page: i32, ) -> Result, String> { client(server)? @@ -55,8 +54,10 @@ pub async fn load_issues( &scope(owner, repository)?, &IssueQuery { state: status.into(), - labels: (!labels.is_empty()).then(|| labels.join(",")), - milestones: (!milestone.is_empty()).then(|| milestone.into()), + labels: (!filter.labels.is_empty()) + .then(|| filter.labels.iter().cloned().collect::>().join(",")), + milestones: (!filter.milestone.is_empty()).then(|| filter.milestone.clone()), + keyword: (!filter.search_text.is_empty()).then(|| filter.search_text.clone()), page, limit: PAGE_SIZE, ..Default::default() @@ -150,12 +151,14 @@ pub async fn load_pulls( server: &Server, status: &str, milestone: &str, + search_text: &str, page: i32, ) -> Result, String> { client(server)? .search_pulls( status, (!milestone.is_empty()).then_some(milestone), + (!search_text.is_empty()).then_some(search_text), page, PAGE_SIZE, ) diff --git a/crates/app/src/domain.rs b/crates/app/src/domain.rs index 9788db4..996bcf2 100644 --- a/crates/app/src/domain.rs +++ b/crates/app/src/domain.rs @@ -84,6 +84,8 @@ pub struct IssueFilter { pub milestone: String, #[serde(default)] pub labels: BTreeSet, + #[serde(skip)] + pub search_text: String, } impl IssueFilter { @@ -96,6 +98,8 @@ impl IssueFilter { pub struct PullFilter { #[serde(default)] pub milestone: String, + #[serde(skip)] + pub search_text: String, } impl PullFilter { @@ -185,8 +189,44 @@ mod tests { assert!( PullFilter { 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") ); } + + #[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::(r#"{"milestone":"v1"}"#) + .unwrap() + .search_text, + "" + ); + } } diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index b5729b4..61ba555 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -268,14 +268,12 @@ impl GotchaCore { .unwrap_or_default(), ) }; - let labels: Vec<_> = filter.labels.into_iter().collect(); let page = load_issues( &server, &owner, &repository, &status, - &labels, - &filter.milestone, + &filter, valid_page(page)?, ) .await?; @@ -333,6 +331,7 @@ impl GotchaCore { repository: String, milestone: String, labels: Vec, + search_text: String, ) -> Result<(), GotchaError> { let owner = owner.trim(); let repository = repository.trim(); @@ -345,6 +344,7 @@ impl GotchaCore { .into_iter() .filter(|label| !label.is_empty()) .collect(), + search_text: search_text.trim().into(), }; let mut state = self.state.lock().unwrap(); let server = state @@ -585,8 +585,15 @@ impl GotchaCore { Ok(filter.is_active(&state.preferences.pull_status)) } - pub fn set_pull_filters(&self, milestone: String) -> Result<(), GotchaError> { - let filter = PullFilter { milestone }; + pub fn set_pull_filters( + &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 server = state .active_server @@ -627,7 +634,14 @@ impl GotchaCore { .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 { rows: pull_rows(&page.items), has_more: page.has_more, diff --git a/crates/app/src/presentation.rs b/crates/app/src/presentation.rs index 13167e1..214c602 100644 --- a/crates/app/src/presentation.rs +++ b/crates/app/src/presentation.rs @@ -114,6 +114,7 @@ pub struct IssueFilterOptions { pub unavailable_labels: Vec, pub selected_milestone: String, pub selected_labels: Vec, + pub search_text: String, } #[derive(Clone, uniffi::Record)] @@ -153,6 +154,7 @@ pub struct MilestoneEditorPage { pub struct PullFilterOptions { pub milestones: Vec, pub selected_milestone: String, + pub search_text: String, } #[derive(Clone, uniffi::Record)] @@ -365,6 +367,7 @@ pub fn issue_filter_options( unavailable_labels, selected_milestone: filter.milestone.clone(), 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 { milestones, selected_milestone: filter.milestone.clone(), + search_text: filter.search_text.clone(), } } @@ -1365,6 +1369,7 @@ mod tests { let filter = IssueFilter { milestone: "v3".into(), labels: ["bug".into(), "retired".into()].into(), + search_text: "login".into(), }; let options = issue_filter_options( &[ @@ -1395,15 +1400,18 @@ mod tests { assert_eq!(options.milestones, ["v1", "v2", "v3"]); assert_eq!(options.selected_labels, ["bug", "retired"]); assert_eq!(options.selected_milestone, "v3"); + assert_eq!(options.search_text, "login"); let pull_options = pull_filter_options( &["v2".into(), "v1".into()], &PullFilter { milestone: "v3".into(), + search_text: "review".into(), }, ); assert_eq!(pull_options.milestones, ["v1", "v2", "v3"]); assert_eq!(pull_options.selected_milestone, "v3"); + assert_eq!(pull_options.search_text, "review"); } #[test] diff --git a/crates/gitea/src/pulls.rs b/crates/gitea/src/pulls.rs index 8cd074a..fb4e023 100644 --- a/crates/gitea/src/pulls.rs +++ b/crates/gitea/src/pulls.rs @@ -70,6 +70,7 @@ impl Client { &self, state: &str, milestone: Option<&str>, + keyword: Option<&str>, page: i32, limit: i32, ) -> Result> { @@ -84,7 +85,7 @@ impl Client { Some(state), None, milestone, - None, + keyword, None, Some("pulls"), None, @@ -122,7 +123,7 @@ impl Client { let mut pulls = Vec::new(); for page in 1.. { let batch = self - .search_pulls(state, None, page, DEFAULT_PAGE_SIZE) + .search_pulls(state, None, None, page, DEFAULT_PAGE_SIZE) .await?; pulls.extend(batch.items); if !batch.has_more { diff --git a/ios/Generated/gotcha_core.swift b/ios/Generated/gotcha_core.swift index 8cbe97e..25573b0 100644 --- a/ios/Generated/gotcha_core.swift +++ b/ios/Generated/gotcha_core.swift @@ -665,11 +665,11 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable { 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 setPullFilters(milestone: String) throws + func setPullFilters(milestone: String, searchText: 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 uniffi_gotcha_core_fn_method_gotchacore_set_issue_filters( self.uniffiCloneHandle(), FfiConverterString.lower(owner), FfiConverterString.lower(repository), 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 uniffi_gotcha_core_fn_method_gotchacore_set_pull_filters( 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 selectedMilestone: String public var selectedLabels: [String] + public var searchText: String // Default memberwise initializers are never public by default, so we // 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.labels = labels self.unavailableLabels = unavailableLabels self.selectedMilestone = selectedMilestone self.selectedLabels = selectedLabels + self.searchText = searchText } @@ -2148,7 +2152,8 @@ public struct FfiConverterTypeIssueFilterOptions: FfiConverterRustBuffer { labels: FfiConverterSequenceString.read(from: &buf), unavailableLabels: FfiConverterSequenceString.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) FfiConverterString.write(value.selectedMilestone, 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 var milestones: [String] public var selectedMilestone: String + public var searchText: String // Default memberwise initializers are never public by default, so we // declare one manually. - public init(milestones: [String], selectedMilestone: String) { + public init(milestones: [String], selectedMilestone: String, searchText: String) { self.milestones = milestones self.selectedMilestone = selectedMilestone + self.searchText = searchText } @@ -2721,13 +2729,15 @@ public struct FfiConverterTypePullFilterOptions: FfiConverterRustBuffer { return try PullFilterOptions( 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]) { FfiConverterSequenceString.write(value.milestones, 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) { 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 } if (uniffi_gotcha_core_checksum_method_gotchacore_set_issue_status() != 44445) { 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 } if (uniffi_gotcha_core_checksum_method_gotchacore_set_pull_status() != 1356) { diff --git a/ios/Generated/gotcha_coreFFI.h b/ios/Generated/gotcha_coreFFI.h index 2de98c2..93aa58d 100644 --- a/ios/Generated/gotcha_coreFFI.h +++ b/ios/Generated/gotcha_coreFFI.h @@ -416,7 +416,7 @@ void uniffi_gotcha_core_fn_method_gotchacore_set_appearance(uint64_t ptr, uint32 #endif #ifndef 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 #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 #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 +void uniffi_gotcha_core_fn_method_gotchacore_set_pull_filters(uint64_t ptr, RustBuffer milestone, RustBuffer search_text, RustCallStatus *_Nonnull out_status ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_SET_PULL_STATUS diff --git a/ios/Sources/ContentScreens.swift b/ios/Sources/ContentScreens.swift index 82d8e22..d4075f1 100644 --- a/ios/Sources/ContentScreens.swift +++ b/ios/Sources/ContentScreens.swift @@ -1,5 +1,23 @@ 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 final class IssuesViewController: RefreshingTableViewController { private let context: AppContext @@ -149,6 +167,7 @@ final class IssuesViewController: RefreshingTableViewController { ) var children: [UIMenuElement] = [status] if let filterOptions { + children.insert(searchAction(filterOptions), at: 0) children.append(milestoneMenu(filterOptions)) children.append(labelMenu(filterOptions)) } else { @@ -160,6 +179,21 @@ final class IssuesViewController: RefreshingTableViewController { 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 { let selected = options.selectedMilestone let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) { @@ -192,7 +226,11 @@ final class IssuesViewController: RefreshingTableViewController { var labels = Set(options.selectedLabels) if labels.remove(label) == nil { labels.insert(label) } 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) action.state = labels.contains(label) ? .on : .off self.updateFilterMenu() @@ -215,7 +253,11 @@ final class IssuesViewController: RefreshingTableViewController { guard let options = filterOptions else { return } filterTask?.cancel() do { - try saveFilters(milestone: milestone, labels: Set(options.selectedLabels)) + try saveFilters( + milestone: milestone, + labels: Set(options.selectedLabels), + searchText: options.searchText + ) filterOptions?.selectedMilestone = milestone updateFilterMenu() loadIssues(refreshing: false) @@ -224,12 +266,34 @@ final class IssuesViewController: RefreshingTableViewController { } } - private func saveFilters(milestone: String, labels: Set) 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, + searchText: String + ) throws { try context.core.setIssueFilters( owner: owner, repository: repository, 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) filterOptions?.selectedMilestone = "" filterOptions?.selectedLabels = [] + filterOptions?.searchText = "" updateFilterMenu() loadIssues(refreshing: false) } catch { @@ -930,12 +995,33 @@ final class PullsViewController: RefreshingTableViewController { let item = navigationItem.rightBarButtonItem ?? UIBarButtonItem( 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" navigationItem.rightBarButtonItem = item 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 { let selected = options.selectedMilestone let all = UIAction(title: "All Milestones", state: selected.isEmpty ? .on : .off) { @@ -955,9 +1041,13 @@ final class PullsViewController: RefreshingTableViewController { } private func selectMilestone(_ milestone: String) { + guard let options = filterOptions else { return } filterTask?.cancel() do { - try context.core.setPullFilters(milestone: milestone) + try context.core.setPullFilters( + milestone: milestone, + searchText: options.searchText + ) filterOptions?.selectedMilestone = milestone updateFilterMenu() 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 { UIMenu( options: .displayInline, @@ -984,6 +1090,7 @@ final class PullsViewController: RefreshingTableViewController { do { try context.core.clearPullFilters() filterOptions?.selectedMilestone = "" + filterOptions?.searchText = "" updateFilterMenu() loadPulls(refreshing: false) } catch {