gitbay/Builds/BuildListViewModel.swift
67 lines · 2195 bytes
1import Foundation
2import Observation
3
4/// `build list <repo>`, narrowed by `filter`.
5@Observable
6@MainActor
7final class BuildListViewModel {
8
9 private(set) var state: LoadState<[Build]> = .loading
10 /// A change reloads. The sheet edits a copy and assigns once, so a
11 /// typed branch does not fire a request per keystroke.
12 var filter = BuildFilter() {
13 didSet {
14 guard filter != oldValue else { return }
15 reloadTask?.cancel()
16 reloadTask = Task { await load() }
17 }
18 }
19 private var reloadTask: Task<Void, Never>?
20 /// The jobs a trigger can name. Empty when the repo has no CI
21 /// config — the web hides its trigger form in that case.
22 private(set) var jobs: [CIJob] = []
23 private(set) var actionError: String?
24 private(set) var working = false
25
26 private let client: GitbayClient
27 let repoPath: String
28
29 init(client: GitbayClient, repoPath: String) {
30 self.client = client
31 self.repoPath = repoPath
32 }
33
34 func load() async {
35 do {
36 let builds = try await client.readList(
37 ["build", "list", repoPath] + filter.flags(), of: Build.self)
38 state = builds.isEmpty
39 ? .empty(filter.emptyMessage)
40 : .loaded(builds.sorted { $0.number > $1.number })
41 } catch {
42 state = .from(error)
43 }
44 }
45
46 /// `build jobs <owner/name>` — what the picker offers. A repo
47 /// without a CI config answers "not found"; that means no jobs, not
48 /// a failure worth showing.
49 func loadJobs() async {
50 jobs = (try? await client.readList(["build", "jobs", repoPath], of: CIJob.self)) ?? []
51 }
52
53 /// `build trigger <owner/name> <job>` — queue a job now.
54 func trigger(job: String) async {
55 working = true
56 actionError = nil
57 defer { working = false }
58 do {
59 try await client.run(["build", "trigger", repoPath, job])
60 await load()
61 } catch let error as GitbayError {
62 actionError = error.userFacingMessage
63 } catch {
64 actionError = GitbayError.transport(error).userFacingMessage
65 }
66 }
67}