gitbay/Discovery/SavedQuery.swift

36835f99e6d463f3e18c5c3fc3562bb92513f52b
gitbay-ios/gitbay/Discovery/SavedQuery.swift history · blame · raw

144 lines · 4500 bytes

10 symbols in this file
  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}