import Foundation import Observation /// `build list `, narrowed by `filter`. @Observable @MainActor final class BuildListViewModel { private(set) var state: LoadState<[Build]> = .loading /// A change reloads. The sheet edits a copy and assigns once, so a /// typed branch does not fire a request per keystroke. var filter = BuildFilter() { didSet { guard filter != oldValue else { return } reloadTask?.cancel() reloadTask = Task { await load() } } } private var reloadTask: Task? /// The jobs a trigger can name. Empty when the repo has no CI /// config — the web hides its trigger form in that case. private(set) var jobs: [CIJob] = [] private(set) var actionError: String? private(set) var working = false private let client: GitbayClient let repoPath: String init(client: GitbayClient, repoPath: String) { self.client = client self.repoPath = repoPath } func load() async { do { let builds = try await client.readList( ["build", "list", repoPath] + filter.flags(), of: Build.self) state = builds.isEmpty ? .empty(filter.emptyMessage) : .loaded(builds.sorted { $0.number > $1.number }) } catch { state = .from(error) } } /// `build jobs ` — what the picker offers. A repo /// without a CI config answers "not found"; that means no jobs, not /// a failure worth showing. func loadJobs() async { jobs = (try? await client.readList(["build", "jobs", repoPath], of: CIJob.self)) ?? [] } /// `build trigger ` — queue a job now. func trigger(job: String) async { working = true actionError = nil defer { working = false } do { try await client.run(["build", "trigger", repoPath, job]) await load() } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } }