import SwiftUI struct BuildListView: View { @State private var model: BuildListViewModel @State private var triggering = false @State private var jobName = "" init(client: GitbayClient, repo: String) { _model = State(initialValue: BuildListViewModel(client: client, repoPath: repo)) } var body: some View { List { if let error = model.actionError { Section { Label(error, systemImage: "hand.raised") .foregroundStyle(.orange) .font(.subheadline) } } ForEach(model.state.value ?? []) { build in NavigationLink(value: BuildRoute.log(repo: model.repoPath, number: build.number)) { BuildRow(build: build) } } } .overlay { LoadStateOverlay(state: model.state) } .navigationTitle("Builds") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { jobName = model.latestJob ?? "" triggering = true } label: { Image(systemName: "play.circle") } .disabled(model.working) .accessibilityIdentifier("build-trigger-button") } } .alert("Trigger a build", isPresented: $triggering) { TextField("Job name", text: $jobName) .autocorrectionDisabled() .textInputAutocapitalization(.never) Button("Trigger") { Task { await model.trigger(job: jobName.trimmingCharacters(in: .whitespaces)) } } Button("Cancel", role: .cancel) {} } message: { Text("Runs the named job from .gitbay/ at the default branch head.") } .task { await model.load() } .refreshable { await model.load() } } } private struct BuildRow: View { let build: Build var body: some View { HStack(spacing: 10) { Image(systemName: icon) .foregroundStyle(color) VStack(alignment: .leading, spacing: 2) { Text("#\(build.number) \(build.job)") .font(.subheadline.weight(.medium)) HStack(spacing: 6) { Text(build.ref) Text(build.shortSHA) .font(.caption.monospaced()) } .font(.caption) .foregroundStyle(.secondary) } Spacer() VStack(alignment: .trailing, spacing: 2) { Text(build.status) .font(.caption.weight(.medium)) .foregroundStyle(color) Text(build.createdAt, format: .relative(presentation: .named)) .font(.caption) .foregroundStyle(.tertiary) } } .padding(.vertical, 2) } private var icon: String { switch build.status { case "success": "checkmark.circle.fill" case "failure", "error": "xmark.circle.fill" case "running": "circle.dotted" case "queued", "pending": "clock" default: "questionmark.circle" } } private var color: Color { switch build.status { case "success": .green case "failure", "error": .red case "running", "queued", "pending": .orange default: .secondary } } } nonisolated enum BuildRoute: Hashable { case list(repo: String) case log(repo: String, number: Int64) }