diff options
| author | Christian Cleberg <[email protected]> | 2026-04-12 00:34:54 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-12 00:34:54 -0500 |
| commit | 5f6d545eb3455a9e4da5a3cb4460811e3a48d939 (patch) | |
| tree | 7b7591146da4e02bd617ddb9f26e47b1c4474ef6 /Hutch/Views/Builds | |
| parent | cb1ea5ea4f163d87053285d3fb7999b12a3558b9 (diff) | |
| download | hutch-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.swift | 38 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildListView.swift | 11 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildListViewModel.swift | 35 |
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 + } + } + } } |
