summaryrefslogtreecommitdiff
path: root/Hutch/Views/Tickets
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-22 20:42:45 -0500
committerChristian Cleberg <[email protected]>2026-03-22 20:42:45 -0500
commit1f8b7a23529462c3c332753d33810b5c9f692bbc (patch)
tree17526dae77a7579b270e6b9cca6224c9020f772d /Hutch/Views/Tickets
parente03791d6c15ee0caac033d1b1b390c5d65a69d57 (diff)
downloadhutch-1f8b7a23529462c3c332753d33810b5c9f692bbc.tar.gz
hutch-1f8b7a23529462c3c332753d33810b5c9f692bbc.tar.bz2
hutch-1f8b7a23529462c3c332753d33810b5c9f692bbc.zip
feat: add search to builds, trackers, tickets, pastes, mailing lists, and inbox
Diffstat (limited to 'Hutch/Views/Tickets')
-rw-r--r--Hutch/Views/Tickets/TicketListView.swift8
-rw-r--r--Hutch/Views/Tickets/TicketListViewModel.swift80
-rw-r--r--Hutch/Views/Tickets/TrackerListView.swift9
-rw-r--r--Hutch/Views/Tickets/TrackerListViewModel.swift11
4 files changed, 102 insertions, 6 deletions
diff --git a/Hutch/Views/Tickets/TicketListView.swift b/Hutch/Views/Tickets/TicketListView.swift
index ef0da23..93a7032 100644
--- a/Hutch/Views/Tickets/TicketListView.swift
+++ b/Hutch/Views/Tickets/TicketListView.swift
@@ -108,6 +108,11 @@ struct TicketListView: View {
}
}
.listStyle(.plain)
+ .searchable(
+ text: $vm.searchText,
+ placement: .navigationBarDrawer(displayMode: .always),
+ prompt: "Search tickets"
+ )
.overlay {
if viewModel.isLoading, viewModel.tickets.isEmpty {
SRHTLoadingStateView(message: "Loading tickets…")
@@ -117,6 +122,9 @@ struct TicketListView: View {
message: error,
retryAction: { await viewModel.loadTickets() }
)
+ } else if !viewModel.searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
+ viewModel.filteredTickets.isEmpty {
+ ContentUnavailableView.search(text: viewModel.searchText)
} else if viewModel.filteredTickets.isEmpty, viewModel.error == nil {
ContentUnavailableView(
"No Tickets",
diff --git a/Hutch/Views/Tickets/TicketListViewModel.swift b/Hutch/Views/Tickets/TicketListViewModel.swift
index f3c9416..303fd66 100644
--- a/Hutch/Views/Tickets/TicketListViewModel.swift
+++ b/Hutch/Views/Tickets/TicketListViewModel.swift
@@ -35,23 +35,40 @@ final class TicketListViewModel {
let ownerUsername: String
let trackerName: String
let trackerId: Int
+ let trackerRid: String
private(set) var tickets: [TicketSummary] = []
private(set) var isLoading = false
private(set) var isLoadingMore = false
private(set) var isCreatingTicket = false
+ private(set) var isPerformingAction = false
+ private(set) var trackerLabels: [TicketLabel] = []
var error: String?
- var filter: TicketFilter = .open
+ var filter: TicketFilter = .open {
+ didSet {
+ UserDefaults.standard.set(filter.rawValue, forKey: filterDefaultsKey)
+ }
+ }
+ var searchText = ""
private var cursor: String?
private var hasMore = true
private let client: SRHTClient
- init(ownerUsername: String, trackerName: String, trackerId: Int, client: SRHTClient) {
+ private var filterDefaultsKey: String {
+ "ticketFilter_\(trackerRid)"
+ }
+
+ init(ownerUsername: String, trackerName: String, trackerId: Int, trackerRid: String, client: SRHTClient) {
self.ownerUsername = ownerUsername
self.trackerName = trackerName
self.trackerId = trackerId
+ self.trackerRid = trackerRid
self.client = client
+ if let raw = UserDefaults.standard.string(forKey: filterDefaultsKey),
+ let restored = TicketFilter(rawValue: raw) {
+ self.filter = restored
+ }
}
// MARK: - Query
@@ -93,17 +110,70 @@ final class TicketListViewModel {
}
"""
+ private static let updateStatusMutation = """
+ mutation updateTicketStatus($trackerId: Int!, $ticketId: Int!, $input: UpdateStatusInput!) {
+ updateTicketStatus(trackerId: $trackerId, ticketId: $ticketId, input: $input) {
+ eventType: __typename
+ }
+ }
+ """
+
+ private static let assignUserMutation = """
+ mutation assignUser($trackerId: Int!, $ticketId: Int!, $userId: Int!) {
+ assignUser(trackerId: $trackerId, ticketId: $ticketId, userId: $userId) { id }
+ }
+ """
+
+ private static let unassignUserMutation = """
+ mutation unassignUser($trackerId: Int!, $ticketId: Int!, $userId: Int!) {
+ unassignUser(trackerId: $trackerId, ticketId: $ticketId, userId: $userId) { id }
+ }
+ """
+
+ private static let labelTicketMutation = """
+ mutation labelTicket($trackerId: Int!, $ticketId: Int!, $labelId: Int!) {
+ labelTicket(trackerId: $trackerId, ticketId: $ticketId, labelId: $labelId) { id }
+ }
+ """
+
+ private static let unlabelTicketMutation = """
+ mutation unlabelTicket($trackerId: Int!, $ticketId: Int!, $labelId: Int!) {
+ unlabelTicket(trackerId: $trackerId, ticketId: $ticketId, labelId: $labelId) { id }
+ }
+ """
+
+ private static let trackerLabelsQuery = """
+ query trackerLabels($owner: String!, $tracker: String!) {
+ user(username: $owner) {
+ tracker(name: $tracker) {
+ labels {
+ results { id name backgroundColor foregroundColor }
+ }
+ }
+ }
+ }
+ """
+
// MARK: - Computed
/// Tickets filtered by the selected status filter.
var filteredTickets: [TicketSummary] {
+ let statusFiltered: [TicketSummary]
switch filter {
case .open:
- tickets.filter { $0.status.isOpen }
+ statusFiltered = tickets.filter { $0.status.isOpen }
case .resolved:
- tickets.filter { !$0.status.isOpen }
+ statusFiltered = tickets.filter { !$0.status.isOpen }
case .all:
- tickets
+ statusFiltered = tickets
+ }
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ guard !q.isEmpty else { return statusFiltered }
+ return statusFiltered.filter {
+ String($0.id).contains(q) ||
+ $0.title.lowercased().contains(q) ||
+ $0.submitter.canonicalName.lowercased().contains(q) ||
+ $0.labels.contains { $0.name.lowercased().contains(q) }
}
}
diff --git a/Hutch/Views/Tickets/TrackerListView.swift b/Hutch/Views/Tickets/TrackerListView.swift
index ee253ec..a324eae 100644
--- a/Hutch/Views/Tickets/TrackerListView.swift
+++ b/Hutch/Views/Tickets/TrackerListView.swift
@@ -65,7 +65,7 @@ struct TrackerListView: View {
@Bindable var vm = viewModel
List {
- ForEach(viewModel.trackers) { tracker in
+ ForEach(viewModel.filteredTrackers) { tracker in
NavigationLink(value: tracker) {
TrackerRowView(tracker: tracker)
}
@@ -84,6 +84,11 @@ struct TrackerListView: View {
}
}
.listStyle(.plain)
+ .searchable(
+ text: $vm.searchText,
+ placement: .navigationBarDrawer(displayMode: .always),
+ prompt: "Search trackers"
+ )
.overlay {
if viewModel.isLoading, viewModel.trackers.isEmpty {
SRHTLoadingStateView(message: "Loading trackers…")
@@ -93,6 +98,8 @@ struct TrackerListView: View {
message: error,
retryAction: { await viewModel.loadTrackers() }
)
+ } else if !viewModel.trackers.isEmpty, viewModel.filteredTrackers.isEmpty {
+ ContentUnavailableView.search(text: viewModel.searchText)
} else if viewModel.trackers.isEmpty, viewModel.error == nil {
ContentUnavailableView(
"No Trackers",
diff --git a/Hutch/Views/Tickets/TrackerListViewModel.swift b/Hutch/Views/Tickets/TrackerListViewModel.swift
index 9c241ae..31c07d7 100644
--- a/Hutch/Views/Tickets/TrackerListViewModel.swift
+++ b/Hutch/Views/Tickets/TrackerListViewModel.swift
@@ -22,6 +22,7 @@ final class TrackerListViewModel {
private(set) var isLoadingMore = false
private(set) var isCreatingTracker = false
var error: String?
+ var searchText = ""
private var cursor: String?
private var hasMore = true
@@ -31,6 +32,16 @@ final class TrackerListViewModel {
self.client = client
}
+ var filteredTrackers: [TrackerSummary] {
+ let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ guard !q.isEmpty else { return trackers }
+ return trackers.filter {
+ $0.name.lowercased().contains(q) ||
+ ($0.description?.lowercased().contains(q) == true) ||
+ $0.owner.canonicalName.lowercased().contains(q)
+ }
+ }
+
// MARK: - Query
private static let query = """