Add server-side work item search

This commit is contained in:
Georg Bauer
2026-07-31 21:03:01 +02:00
parent af392209e8
commit a3962449a2
9 changed files with 234 additions and 42 deletions

View File

@@ -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<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(
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 {