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 { |
| 81 | 81 | /// `query save <name> <query> [--force]`. The query goes as one word; |
| 82 | 82 | /// the server reads it the same as many. Returns whether it saved. |
| 83 | 83 | 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"] |
| 86 | 87 | if replace { argv.append("--force") } |
| 88 | argv += ["--", name.trimmingCharacters(in: .whitespaces), |
| 89 | query.trimmingCharacters(in: .whitespacesAndNewlines)] |
| 87 | 90 | return await perform(argv) |
| 88 | 91 | } |
| 89 | 92 | |
| @@ -95,6 +98,11 @@ final class SavedQueriesViewModel { |
| 95 | 98 | await perform(["query", "remove", query.name]) |
| 96 | 99 | } |
| 97 | 100 | |
| 101 | /// A closed sheet takes its error with it. |
| 102 | func clearError() { |
| 103 | actionError = nil |
| 104 | } |
| 105 | |
| 98 | 106 | @discardableResult |
| 99 | 107 | private func perform(_ argv: [String]) async -> Bool { |
| 100 | 108 | working = true |
| @@ -111,6 +119,8 @@ final class SavedQueriesViewModel { |
| 111 | 119 | return false |
| 112 | 120 | } |
| 113 | 121 | await load() |
| 122 | // The dashboard underneath shows pinned queries. |
| 123 | NotificationCenter.default.post(name: .gitbaySavedQueriesChanged, object: nil) |
| 114 | 124 | return true |
| 115 | 125 | } |
| 116 | 126 | } |
gitbay/Views/Dashboard/DashboardView.swift
+3
| @@ -92,6 +92,9 @@ struct DashboardView: View { |
| 92 | 92 | } |
| 93 | 93 | .task { await model.load() } |
| 94 | 94 | .refreshable { await model.load() } |
| 95 | .onReceive(NotificationCenter.default.publisher(for: .gitbaySavedQueriesChanged)) { _ in |
| 96 | Task { await model.load() } |
| 97 | } |
| 95 | 98 | .onChange(of: model.state.value?.unread) { _, count in |
| 96 | 99 | if let count { unread = count } |
| 97 | 100 | } |
gitbay/Views/Discovery/SavedQueriesView.swift
+24 −5
| @@ -6,6 +6,8 @@ struct SavedQueriesView: View { |
| 6 | 6 | |
| 7 | 7 | @State private var model: SavedQueriesViewModel |
| 8 | 8 | @State private var composing = false |
| 9 | /// The query the sheet edits; nil saves a new one. |
| 10 | @State private var editing: SavedQuery? |
| 9 | 11 | @State private var removing: SavedQuery? |
| 10 | 12 | |
| 11 | 13 | init(client: GitbayClient) { |
| @@ -49,6 +51,12 @@ struct SavedQueriesView: View { |
| 49 | 51 | .tint(Color.gbAccent) |
| 50 | 52 | } |
| 51 | 53 | .contextMenu { |
| 54 | Button { |
| 55 | editing = query |
| 56 | composing = true |
| 57 | } label: { |
| 58 | Label("Edit", systemImage: "pencil") |
| 59 | } |
| 52 | 60 | Button { |
| 53 | 61 | Task { await model.setPinned(query, !query.pinned) } |
| 54 | 62 | } label: { |
| @@ -69,6 +77,7 @@ struct SavedQueriesView: View { |
| 69 | 77 | .toolbar { |
| 70 | 78 | ToolbarItem(placement: .topBarTrailing) { |
| 71 | 79 | Button { |
| 80 | editing = nil |
| 72 | 81 | composing = true |
| 73 | 82 | } label: { |
| 74 | 83 | Image(systemName: "plus") |
| @@ -86,8 +95,8 @@ struct SavedQueriesView: View { |
| 86 | 95 | Button("Remove", role: .destructive) { Task { await model.remove(query) } } |
| 87 | 96 | Button("Cancel", role: .cancel) {} |
| 88 | 97 | } |
| 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 } |
| 91 | 100 | } |
| 92 | 101 | .task { await model.load() } |
| 93 | 102 | .refreshable { await model.load() } |
| @@ -99,12 +108,22 @@ private struct SaveQuerySheet: View { |
| 99 | 108 | let model: SavedQueriesViewModel |
| 100 | 109 | let onSaved: () -> Void |
| 101 | 110 | |
| 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 |
| 105 | 114 | @State private var sent = false |
| 106 | 115 | @Environment(\.dismiss) private var dismiss |
| 107 | 116 | |
| 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 | |
| 108 | 127 | var body: some View { |
| 109 | 128 | NavigationStack { |
| 110 | 129 | Form { |
gitbay/Views/Shared/ChangeNotices.swift
added
+11
| @@ -0,0 +1,11 @@ |
| 1 | import 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. |
| 6 | extension 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 { |
| 56 | 56 | let saved = await model.save(name: " review ", query: #" is:mr label:"needs review" "#, replace: false) |
| 57 | 57 | |
| 58 | 58 | #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""#]]) |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | 62 | @Test func replacingAddsForce() async throws { |
| @@ -66,7 +66,7 @@ struct SavedQueriesViewModelTests { |
| 66 | 66 | |
| 67 | 67 | _ = await model.save(name: "bugs", query: "is:issue label:bug is:open", replace: true) |
| 68 | 68 | |
| 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"]]) |
| 70 | 70 | } |
| 71 | 71 | |
| 72 | 72 | @Test func aRefusedSaveKeepsTheSheetAndSaysWhy() async throws { |