import SwiftUI struct BuildListView: View { @State private var model: BuildListViewModel init(client: GitbayClient, repo: String) { _model = State(initialValue: BuildListViewModel(client: client, repoPath: repo)) } var body: some View { List { if let error = model.actionError { Section { GBNotice(error, .gbWarn) } } ForEach(model.state.value ?? []) { build in NavigationLink(value: BuildRoute.detail(repo: model.repoPath, number: build.number)) { BuildRow(build: build) } } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle("Builds") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { // A menu of jobs, like the web's picker — not a name to // type. Nothing to trigger without a job file. Menu { ForEach(model.jobs) { job in Button("\(job.name) — \(job.trigger)") { Task { await model.trigger(job: job.name) } } } } label: { Image(systemName: "play.circle") } .disabled(model.working || model.jobs.isEmpty) .accessibilityIdentifier("build-trigger-button") } } .task { await model.load() await model.loadJobs() } .refreshable { await model.load() await model.loadJobs() } } } private struct BuildRow: View { let build: Build var body: some View { HStack(spacing: 10) { Image(systemName: build.statusIcon) .foregroundStyle(build.statusColor) VStack(alignment: .leading, spacing: 2) { Text("#\(build.number) \(build.job)") .font(.gbSans(.subheadline).weight(.medium)) HStack(spacing: 6) { Text(build.ref) Text(build.shortSHA) .font(.gbMono(.caption)) } .font(.gbSans(.caption)) .foregroundStyle(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 2) { Text(build.status) .font(.gbSans(.caption).weight(.medium)) .foregroundStyle(build.statusColor) Text(build.createdAt, format: .relative(presentation: .named)) .font(.gbSans(.caption)) .foregroundStyle(.secondary) } } .padding(.vertical, 2) } } nonisolated enum BuildRoute: Hashable { case list(repo: String) case detail(repo: String, number: Int64) }