Commit fe2832564a

fe2832564a7c2963abe2c7ce86124c825f36cf76

parent: 563215175a

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-14 07:17 UTC

Saved queries: the dashboard follows; Edit; a query may start with dashes

A save, pin, unpin or removal reloads the dashboard underneath, which
showed the old pinned queries until pulled. Edit fills the sheet with a
saved query to replace, which is how one whose text no longer parses is
fixed. The query goes after "--", so one starting with dashes is not
read as a flag, and a closed sheet takes its error with it.

Ref #30

Layout: unified · split

gitbay/Discovery/SavedQuery.swift +12 −2
@@ -81,9 +81,12 @@ final class SavedQueriesViewModel {
8181 /// `query save <name> <query> [--force]`. The query goes as one word;
8282 /// the server reads it the same as many. Returns whether it saved.
8383 func save(name: String, query: String, replace: Bool) async -> Bool {
84 var argv = ["query", "save", name.trimmingCharacters(in: .whitespaces),
85 query.trimmingCharacters(in: .whitespacesAndNewlines)]
84 // Flags, then "--": a query may start with dashes, and the server's
85 // parser would read it as a flag.
86 var argv = ["query", "save"]
8687 if replace { argv.append("--force") }
88 argv += ["--", name.trimmingCharacters(in: .whitespaces),
89 query.trimmingCharacters(in: .whitespacesAndNewlines)]
8790 return await perform(argv)
8891 }
8992
@@ -95,6 +98,11 @@ final class SavedQueriesViewModel {
9598 await perform(["query", "remove", query.name])
9699 }
97100
101 /// A closed sheet takes its error with it.
102 func clearError() {
103 actionError = nil
104 }
105
98106 @discardableResult
99107 private func perform(_ argv: [String]) async -> Bool {
100108 working = true
@@ -111,6 +119,8 @@ final class SavedQueriesViewModel {
111119 return false
112120 }
113121 await load()
122 // The dashboard underneath shows pinned queries.
123 NotificationCenter.default.post(name: .gitbaySavedQueriesChanged, object: nil)
114124 return true
115125 }
116126}
gitbay/Views/Dashboard/DashboardView.swift +3
@@ -92,6 +92,9 @@ struct DashboardView: View {
9292 }
9393 .task { await model.load() }
9494 .refreshable { await model.load() }
95 .onReceive(NotificationCenter.default.publisher(for: .gitbaySavedQueriesChanged)) { _ in
96 Task { await model.load() }
97 }
9598 .onChange(of: model.state.value?.unread) { _, count in
9699 if let count { unread = count }
97100 }
gitbay/Views/Discovery/SavedQueriesView.swift +24 −5
@@ -6,6 +6,8 @@ struct SavedQueriesView: View {
66
77 @State private var model: SavedQueriesViewModel
88 @State private var composing = false
9 /// The query the sheet edits; nil saves a new one.
10 @State private var editing: SavedQuery?
911 @State private var removing: SavedQuery?
1012
1113 init(client: GitbayClient) {
@@ -49,6 +51,12 @@ struct SavedQueriesView: View {
4951 .tint(Color.gbAccent)
5052 }
5153 .contextMenu {
54 Button {
55 editing = query
56 composing = true
57 } label: {
58 Label("Edit", systemImage: "pencil")
59 }
5260 Button {
5361 Task { await model.setPinned(query, !query.pinned) }
5462 } label: {
@@ -69,6 +77,7 @@ struct SavedQueriesView: View {
6977 .toolbar {
7078 ToolbarItem(placement: .topBarTrailing) {
7179 Button {
80 editing = nil
7281 composing = true
7382 } label: {
7483 Image(systemName: "plus")
@@ -86,8 +95,8 @@ struct SavedQueriesView: View {
8695 Button("Remove", role: .destructive) { Task { await model.remove(query) } }
8796 Button("Cancel", role: .cancel) {}
8897 }
89 .sheet(isPresented: $composing) {
90 SaveQuerySheet(model: model) { composing = false }
98 .sheet(isPresented: $composing, onDismiss: { model.clearError() }) {
99 SaveQuerySheet(model: model, editing: editing) { composing = false }
91100 }
92101 .task { await model.load() }
93102 .refreshable { await model.load() }
@@ -99,12 +108,22 @@ private struct SaveQuerySheet: View {
99108 let model: SavedQueriesViewModel
100109 let onSaved: () -> Void
101110
102 @State private var name = ""
103 @State private var query = ""
104 @State private var replace = false
111 @State private var name: String
112 @State private var query: String
113 @State private var replace: Bool
105114 @State private var sent = false
106115 @Environment(\.dismiss) private var dismiss
107116
117 /// `editing` fills the sheet with a saved query to change, replacing it
118 /// under the same name: how one whose text no longer parses is fixed.
119 init(model: SavedQueriesViewModel, editing: SavedQuery?, onSaved: @escaping () -> Void) {
120 self.model = model
121 self.onSaved = onSaved
122 _name = State(initialValue: editing?.name ?? "")
123 _query = State(initialValue: editing?.query ?? "")
124 _replace = State(initialValue: editing != nil)
125 }
126
108127 var body: some View {
109128 NavigationStack {
110129 Form {
gitbay/Views/Shared/ChangeNotices.swift added +11
@@ -0,0 +1,11 @@
1import Foundation
2
3/// Writes on one screen that change what another, already on the stack
4/// underneath, is showing. The one-shot `.task` loads of those screens do
5/// not run again on a pop, so they listen for these instead.
6extension Notification.Name {
7 /// A repository was deleted, moved or renamed; `object` is its old path.
8 static let gitbayRepositoryChanged = Notification.Name("gitbayRepositoryChanged")
9 /// A saved query was saved, pinned, unpinned or removed.
10 static let gitbaySavedQueriesChanged = Notification.Name("gitbaySavedQueriesChanged")
11}
gitbayTests/SavedQueryTests.swift +2 −2
@@ -56,7 +56,7 @@ struct SavedQueriesViewModelTests {
5656 let saved = await model.save(name: " review ", query: #" is:mr label:"needs review" "#, replace: false)
5757
5858 #expect(saved)
59 #expect(try posted(stub) == [["query", "save", "review", #"is:mr label:"needs review""#]])
59 #expect(try posted(stub) == [["query", "save", "--", "review", #"is:mr label:"needs review""#]])
6060 }
6161
6262 @Test func replacingAddsForce() async throws {
@@ -66,7 +66,7 @@ struct SavedQueriesViewModelTests {
6666
6767 _ = await model.save(name: "bugs", query: "is:issue label:bug is:open", replace: true)
6868
69 #expect(try posted(stub) == [["query", "save", "bugs", "is:issue label:bug is:open", "--force"]])
69 #expect(try posted(stub) == [["query", "save", "--force", "--", "bugs", "is:issue label:bug is:open"]])
7070 }
7171
7272 @Test func aRefusedSaveKeepsTheSheetAndSaysWhy() async throws {