import SwiftUI struct PRDetailView: View { @StateObject private var vm: PRDetailViewModel @State private var selectedTab: DetailTab = .conversation @State private var showActionSheet = false @State private var showCommentSheet = false @State private var showMergeSheet = false @State private var showCloseConfirm = false @State private var showRerunConfirm: ActionRun? @State private var requestChangesMode = false enum DetailTab: String, CaseIterable, Identifiable { case conversation = "Conversation" case files = "Files" case commits = "Commits" case checks = "Checks" var id: String { rawValue } } init(repo: Repository, prNumber: Int) { _vm = StateObject(wrappedValue: PRDetailViewModel(repo: repo, prNumber: prNumber)) } var body: some View { Group { if let pr = vm.pr { content(pr) } else if vm.isLoading { ProgressView("Loading…") } else if let err = vm.errorMessage { ContentUnavailableView("Couldn't load PR", systemImage: "exclamationmark.triangle", description: Text(err)) .toolbar { ToolbarItem(placement: .primaryAction) { reloadButton } } } else { ProgressView("Loading…") } } .navigationTitle("#\(vm.prNumber)") .navigationBarTitleDisplayMode(.inline) .task { await vm.load() } .alert("Error", isPresented: .init( get: { vm.errorMessage != nil }, set: { if !$0 { vm.errorMessage = nil } })) { Button("OK", role: .cancel) {} } message: { Text(vm.errorMessage ?? "") } .overlay { if vm.busyMessage != nil { busyOverlay } } .confirmationDialog("Actions", isPresented: $showActionSheet, titleVisibility: .visible) { Button("Comment") { showCommentSheet = true } Button("Approve") { Task { await vm.submitReview(state: "approved", comment: nil) } } Button("Request changes") { requestChangesMode = true; showCommentSheet = true } Button("Merge") { showMergeSheet = true } if prIsOpen { Button("Close", role: .destructive) { showCloseConfirm = true } } else { Button("Reopen") { Task { await vm.setState("open") } } } } .confirmationDialog("Close this PR?", isPresented: $showCloseConfirm, titleVisibility: .visible) { Button("Close", role: .destructive) { Task { await vm.setState("closed") } } Button("Cancel", role: .cancel) {} } message: { Text("This will close the pull request.") } .sheet(isPresented: $showCommentSheet) { CommentSheet(vm: vm, mode: requestChangesMode ? .requestChanges : .comment) .onDisappear { requestChangesMode = false } } .sheet(isPresented: $showMergeSheet) { MergeSheet(vm: vm) } .confirmationDialog("Re-run workflow?", isPresented: .init( get: { showRerunConfirm != nil }, set: { if !$0 { showRerunConfirm = nil } }), titleVisibility: .visible) { Button("Re-run") { if let run = showRerunConfirm { Task { await vm.rerun(runID: run.id) } } showRerunConfirm = nil } Button("Cancel", role: .cancel) {} } message: { Text("Re-run \"\(showRerunConfirm?.name ?? "workflow")\"?") } } @ViewBuilder private func content(_ pr: PullRequest) -> some View { VStack(spacing: 0) { header(pr) Picker("Tab", selection: $selectedTab) { ForEach(DetailTab.allCases) { t in Text(t.rawValue).tag(t) } } .pickerStyle(.segmented) .padding(.horizontal) .padding(.bottom, 8) ScrollView { switch selectedTab { case .conversation: ConversationTab(vm: vm) case .files: FilesTab(vm: vm) case .commits: CommitsTab(commits: vm.commits) case .checks: ChecksTab(vm: vm, onRerun: { showRerunConfirm = $0 }) } } if prIsOpen { actionBar(pr) } } .toolbar { ToolbarItem(placement: .primaryAction) { reloadButton } } } @ViewBuilder private func header(_ pr: PullRequest) -> some View { VStack(alignment: .leading, spacing: 8) { Text(pr.title) .font(.title3.weight(.bold)) HStack(spacing: 6) { Text("\(vm.repo.fullName)#\(pr.number)") .font(.caption) .foregroundColor(.secondary) CICircle(status: vm.ciStatus) if vm.reviewState != .unknown { ReviewBadge(state: vm.reviewState) } if pr.draft == true { Text("Draft").font(.caption2).foregroundColor(.secondary) } } if let body = pr.body, !body.isEmpty { Text(body) .font(.callout) .frame(maxWidth: .infinity, alignment: .leading) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color(.secondarySystemBackground)) } @ViewBuilder private func actionBar(_ pr: PullRequest) -> some View { HStack { Button { showActionSheet = true } label: { Label("Review", systemImage: "checkmark.bubble") } .buttonStyle(.borderedProminent) Button("Merge") { showMergeSheet = true } .buttonStyle(.bordered) .disabled(vm.ciStatus == .failure && SettingsStore.shared.mergeGateMode == .block) Spacer() } .padding() .background(.bar) } private var prIsOpen: Bool { (vm.pr?.state.lowercased() ?? "") == "open" } private var reloadButton: some View { Button { Task { await vm.load() } } label: { Image(systemName: "arrow.clockwise") } } private var busyOverlay: some View { ZStack { Color.black.opacity(0.15) ProgressView(vm.busyMessage ?? "") .padding() .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) } .ignoresSafeArea() } }