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