summaryrefslogtreecommitdiff
path: root/Hutch/Views/Builds
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 00:34:54 -0500
committerChristian Cleberg <[email protected]>2026-04-12 00:34:54 -0500
commit5f6d545eb3455a9e4da5a3cb4460811e3a48d939 (patch)
tree7b7591146da4e02bd617ddb9f26e47b1c4474ef6 /Hutch/Views/Builds
parentcb1ea5ea4f163d87053285d3fb7999b12a3558b9 (diff)
downloadhutch-5f6d545eb3455a9e4da5a3cb4460811e3a48d939.tar.gz
hutch-5f6d545eb3455a9e4da5a3cb4460811e3a48d939.tar.bz2
hutch-5f6d545eb3455a9e4da5a3cb4460811e3a48d939.zip
improve home attention flows and cross-linkingv2.14.0
Diffstat (limited to 'Hutch/Views/Builds')
-rw-r--r--Hutch/Views/Builds/BuildDetailView.swift38
-rw-r--r--Hutch/Views/Builds/BuildListView.swift11
-rw-r--r--Hutch/Views/Builds/BuildListViewModel.swift35
3 files changed, 82 insertions, 2 deletions
diff --git a/Hutch/Views/Builds/BuildDetailView.swift b/Hutch/Views/Builds/BuildDetailView.swift
index 1c2e2bb..33590e2 100644
--- a/Hutch/Views/Builds/BuildDetailView.swift
+++ b/Hutch/Views/Builds/BuildDetailView.swift
@@ -9,6 +9,7 @@ struct BuildDetailView: View {
@State private var selectedTaskName: String?
@State private var showEditResubmitSheet = false
@State private var showCancelConfirmation = false
+ @State private var isOpeningRepository = false
private var isPresentingLogSheet: Bool {
selectedTaskName != nil
@@ -158,6 +159,28 @@ struct BuildDetailView: View {
LabeledContent("Updated", value: job.updated.relativeDescription)
}
+ if let repositoryReference = HomeViewModel.primaryRepositoryReference(in: job.manifest) {
+ Section("Source") {
+ Button {
+ openRepository(ownerCanonicalName: repositoryReference.ownerCanonicalName, repositoryName: repositoryReference.name)
+ } label: {
+ HStack {
+ Label("\(repositoryReference.ownerCanonicalName)/\(repositoryReference.name)", systemImage: "book.closed")
+ Spacer()
+ if isOpeningRepository {
+ ProgressView()
+ .controlSize(.small)
+ } else {
+ Image(systemName: "arrow.up.right")
+ .font(.caption)
+ .foregroundStyle(.tertiary)
+ }
+ }
+ }
+ .disabled(isOpeningRepository)
+ }
+ }
+
// Per-task logs
if !job.tasks.isEmpty {
ForEach(job.tasks) { task in
@@ -241,6 +264,21 @@ struct BuildDetailView: View {
))
}
}
+
+ private func openRepository(ownerCanonicalName: String, repositoryName: String) {
+ guard !isOpeningRepository else { return }
+ isOpeningRepository = true
+ Task {
+ defer { isOpeningRepository = false }
+ do {
+ let ownerUsername = ownerCanonicalName.hasPrefix("~") ? String(ownerCanonicalName.dropFirst()) : ownerCanonicalName
+ let repository = try await appState.resolveRepository(owner: ownerUsername, name: repositoryName)
+ appState.navigateToRepository(repository)
+ } catch {
+ appState.presentRepositoryDeepLinkError()
+ }
+ }
+ }
}
private struct EditResubmitBuildSheet: View {
diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift
index 6bfbd6b..705f97c 100644
--- a/Hutch/Views/Builds/BuildListView.swift
+++ b/Hutch/Views/Builds/BuildListView.swift
@@ -65,6 +65,17 @@ struct BuildListView: View {
@Bindable var vm = viewModel
List {
+ Section {
+ Picker("Filter", selection: $vm.filter) {
+ ForEach(BuildListFilter.allCases, id: \.self) { filter in
+ Text(filter.rawValue).tag(filter)
+ }
+ }
+ .pickerStyle(.segmented)
+ .listRowBackground(Color.clear)
+ .listRowInsets(EdgeInsets())
+ }
+
ForEach(viewModel.filteredJobs) { job in
NavigationLink(value: job) {
BuildRowView(job: job)
diff --git a/Hutch/Views/Builds/BuildListViewModel.swift b/Hutch/Views/Builds/BuildListViewModel.swift
index 0757ad8..8468cff 100644
--- a/Hutch/Views/Builds/BuildListViewModel.swift
+++ b/Hutch/Views/Builds/BuildListViewModel.swift
@@ -19,6 +19,12 @@ private struct SubmittedJob: Decodable, Sendable {
let id: Int
}
+enum BuildListFilter: String, CaseIterable, Sendable {
+ case attention = "Attention"
+ case active = "Active"
+ case all = "All"
+}
+
// MARK: - View Model
@Observable
@@ -31,6 +37,7 @@ final class BuildListViewModel {
private(set) var isRefreshing = false
private(set) var isSubmitting = false
var error: String?
+ var filter: BuildListFilter = .attention
var searchText = ""
private var cursor: String?
@@ -44,9 +51,10 @@ final class BuildListViewModel {
}
var filteredJobs: [JobSummary] {
+ let statusFiltered = Self.filterJobs(jobs, filter: filter)
let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- guard !q.isEmpty else { return jobs }
- return jobs.filter {
+ guard !q.isEmpty else { return statusFiltered }
+ return statusFiltered.filter {
String($0.id).contains(q) ||
$0.tags.contains { $0.lowercased().contains(q) } ||
($0.note?.lowercased().contains(q) == true) ||
@@ -272,4 +280,27 @@ final class BuildListViewModel {
let cancel: CancelResult
}
+
+ nonisolated static func filterJobs(_ jobs: [JobSummary], filter: BuildListFilter) -> [JobSummary] {
+ jobs.filter { job in
+ switch filter {
+ case .attention:
+ switch job.status {
+ case .failed, .timeout, .running, .queued, .pending:
+ return true
+ case .success, .cancelled:
+ return false
+ }
+ case .active:
+ switch job.status {
+ case .running, .queued, .pending:
+ return true
+ case .success, .failed, .cancelled, .timeout:
+ return false
+ }
+ case .all:
+ return true
+ }
+ }
+ }
}