gitbay/Discovery/SavedQuery.swift
144 lines · 4500 bytes
1import Foundation
2import Observation
3
4/// One saved query (`query list`, gitbay v1.39.0): an issue and merge
5/// request query across repositories, kept under a name.
6nonisolated struct SavedQuery: Decodable, Sendable, Hashable, Identifiable {
7 let name: String
8 let query: String
9 let pinned: Bool
10 var id: String { name }
11}
12
13/// A row of `query run` and of a pinned query on the dashboard: an issue
14/// or merge request, naming its repository.
15nonisolated struct QueryItem: Decodable, Sendable, Hashable, Identifiable {
16 let kind: String
17 let repo: String
18 let number: Int64
19 let title: String
20 let state: String
21 let draft: Bool?
22 let author: String
23 let milestone: String?
24 let createdAt: Date
25 let updatedAt: Date
26
27 enum CodingKeys: String, CodingKey {
28 case kind, repo, number, title, state, draft, author, milestone
29 case createdAt = "created_at"
30 case updatedAt = "updated_at"
31 }
32
33 var isMR: Bool { kind == "mr" }
34 /// owner/name#n or owner/name!n, as a person writes it.
35 var ref: String { repo + (isMR ? "!" : "#") + String(number) }
36 var id: String { ref }
37
38 var route: any Hashable {
39 isMR ? MRRoute.mr(repo: repo, number: number) as any Hashable
40 : IssueRoute.issue(repo: repo, number: number)
41 }
42}
43
44/// A pinned query on the dashboard: its first rows and how many it
45/// matches in all. `error` is set when the saved text no longer parses.
46nonisolated struct DashboardQuery: Decodable, Sendable, Hashable, Identifiable {
47 let name: String
48 let query: String
49 let count: Int
50 let items: [QueryItem]
51 let error: String?
52 var id: String { name }
53}
54
55/// `query list`, and the writes on it: save, pin, unpin, remove.
56@Observable
57@MainActor
58final class SavedQueriesViewModel {
59
60 private(set) var state: LoadState<[SavedQuery]> = .loading
61 private(set) var actionError: String?
62 private(set) var working = false
63
64 private let client: GitbayClient
65
66 init(client: GitbayClient) {
67 self.client = client
68 }
69
70 func load() async {
71 do {
72 let queries = try await client.readList(["query", "list"], of: SavedQuery.self)
73 state = queries.isEmpty
74 ? .empty("No saved queries. Save one from the + button.")
75 : .loaded(queries)
76 } catch {
77 state = .from(error)
78 }
79 }
80
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.
83 func save(name: String, query: String, replace: Bool) async -> Bool {
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"]
87 if replace { argv.append("--force") }
88 argv += ["--", name.trimmingCharacters(in: .whitespaces),
89 query.trimmingCharacters(in: .whitespacesAndNewlines)]
90 return await perform(argv)
91 }
92
93 func setPinned(_ query: SavedQuery, _ pinned: Bool) async {
94 await perform(["query", pinned ? "pin" : "unpin", query.name])
95 }
96
97 func remove(_ query: SavedQuery) async {
98 await perform(["query", "remove", query.name])
99 }
100
101 /// A closed sheet takes its error with it.
102 func clearError() {
103 actionError = nil
104 }
105
106 @discardableResult
107 private func perform(_ argv: [String]) async -> Bool {
108 working = true
109 actionError = nil
110 defer { working = false }
111 do {
112 try await client.run(argv)
113 } catch let error as GitbayError {
114 // The name and the query are typed, so a usage refusal is theirs.
115 actionError = error.messageForTypedInput
116 return false
117 } catch {
118 actionError = GitbayError.transport(error).userFacingMessage
119 return false
120 }
121 await load()
122 // The dashboard underneath shows pinned queries.
123 NotificationCenter.default.post(name: .gitbaySavedQueriesChanged, object: nil)
124 return true
125 }
126}
127
128/// `query run <name>`, paged.
129@Observable
130@MainActor
131final class QueryResultsViewModel {
132
133 let list: PagedListModel<QueryItem>
134 let name: String
135
136 init(client: GitbayClient, name: String) {
137 self.name = name
138 list = PagedListModel(client: client, argv: ["query", "run", name], emptyMessage: "Nothing matches.")
139 }
140
141 func load() async {
142 await list.reload()
143 }
144}