summaryrefslogtreecommitdiff
path: root/Hutch/Views
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 19:07:57 -0500
committerChristian Cleberg <[email protected]>2026-04-12 19:07:57 -0500
commitd4757fd9f3aba663df967ae7cadb7f74c3d7acb4 (patch)
treeb6739ffcddfb40fe8b703ee24a6ade76798da85d /Hutch/Views
parentb3d3e68e742650cb8a448a1f51fea7fb7783c36f (diff)
downloadhutch-d4757fd9f3aba663df967ae7cadb7f74c3d7acb4.tar.gz
hutch-d4757fd9f3aba663df967ae7cadb7f74c3d7acb4.tar.bz2
hutch-d4757fd9f3aba663df967ae7cadb7f74c3d7acb4.zip
fix: polish build retry and cancel actions
fixes: https://todo.sr.ht/~ccleberg/hutch/19
Diffstat (limited to 'Hutch/Views')
-rw-r--r--Hutch/Views/Builds/BuildDetailView.swift14
-rw-r--r--Hutch/Views/Builds/BuildDetailViewModel.swift50
2 files changed, 50 insertions, 14 deletions
diff --git a/Hutch/Views/Builds/BuildDetailView.swift b/Hutch/Views/Builds/BuildDetailView.swift
index 33590e2..5070536 100644
--- a/Hutch/Views/Builds/BuildDetailView.swift
+++ b/Hutch/Views/Builds/BuildDetailView.swift
@@ -262,6 +262,10 @@ struct BuildDetailView: View {
get: { viewModel.error },
set: { viewModel.error = $0 }
))
+ .srhtErrorBanner(error: Binding(
+ get: { viewModel.actionError },
+ set: { _ in viewModel.dismissActionError() }
+ ))
}
}
@@ -287,7 +291,6 @@ private struct EditResubmitBuildSheet: View {
let onSubmitted: (Int) -> Void
@Environment(\.dismiss) private var dismiss
- @Bindable var viewModelBindable: BuildDetailViewModel
@State private var manifest: String
@State private var tagsText: String
@State private var note: String
@@ -297,7 +300,6 @@ private struct EditResubmitBuildSheet: View {
init(viewModel: BuildDetailViewModel, job: JobDetail, onSubmitted: @escaping (Int) -> Void) {
self.viewModel = viewModel
- self._viewModelBindable = Bindable(viewModel)
self.job = job
self.onSubmitted = onSubmitted
_manifest = State(initialValue: job.manifest ?? "")
@@ -337,10 +339,10 @@ private struct EditResubmitBuildSheet: View {
.foregroundStyle(.secondary)
}
- if let error = viewModel.error {
+ if let actionError = viewModel.actionError {
Section {
Label {
- Text(error)
+ Text(actionError)
} icon: {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
@@ -352,12 +354,12 @@ private struct EditResubmitBuildSheet: View {
.navigationTitle("Edit & Resubmit")
.navigationBarTitleDisplayMode(.inline)
.onDisappear {
- viewModelBindable.error = nil
+ viewModel.dismissActionError()
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
- viewModelBindable.error = nil
+ viewModel.dismissActionError()
dismiss()
}
}
diff --git a/Hutch/Views/Builds/BuildDetailViewModel.swift b/Hutch/Views/Builds/BuildDetailViewModel.swift
index 88b9e42..803ab33 100644
--- a/Hutch/Views/Builds/BuildDetailViewModel.swift
+++ b/Hutch/Views/Builds/BuildDetailViewModel.swift
@@ -45,12 +45,32 @@ final class BuildDetailViewModel {
private(set) var isRebuilding = false
private(set) var isSubmittingEditedBuild = false
var error: String?
+ /// Transient error shown for action failures (cancel, rebuild, submit).
+ /// Separate from `error` so auto-refresh doesn't immediately clear it.
+ private(set) var actionError: String?
+ private var actionErrorDismissTask: Task<Void, Never>?
init(jobId: Int, client: SRHTClient) {
self.jobId = jobId
self.client = client
}
+ func dismissActionError() {
+ actionError = nil
+ actionErrorDismissTask?.cancel()
+ actionErrorDismissTask = nil
+ }
+
+ private func setActionError(_ message: String) {
+ actionError = message
+ actionErrorDismissTask?.cancel()
+ actionErrorDismissTask = Task {
+ try? await Task.sleep(for: .seconds(5))
+ guard !Task.isCancelled else { return }
+ actionError = nil
+ }
+ }
+
// MARK: - Queries
private static let detailQuery = """
@@ -208,8 +228,18 @@ final class BuildDetailViewModel {
func cancelJob() async {
guard let job, job.status.isCancellable, !isCancelling else { return }
+ let originalJob = job
isCancelling = true
- error = nil
+
+ // Optimistic update: show cancelled status immediately.
+ self.job = JobDetail(
+ id: job.id, created: job.created, updated: job.updated,
+ status: .cancelled, note: job.note, tags: job.tags,
+ visibility: job.visibility, image: job.image,
+ manifest: job.manifest, tasks: job.tasks,
+ log: job.log, owner: job.owner
+ )
+ stopAutoRefresh()
do {
_ = try await client.execute(
@@ -218,10 +248,14 @@ final class BuildDetailViewModel {
variables: ["id": jobId],
responseType: CancelResponse.self
)
- // Reload job to get updated status.
await loadJob()
} catch {
- self.error = error.userFacingMessage
+ // Revert optimistic update on failure.
+ self.job = originalJob
+ if !originalJob.status.isTerminal {
+ startAutoRefresh()
+ }
+ setActionError("Couldn't cancel build. \(error.userFacingMessage)")
}
isCancelling = false
@@ -233,7 +267,7 @@ final class BuildDetailViewModel {
}
isRebuilding = true
- error = nil
+ dismissActionError()
defer { isRebuilding = false }
var variables: [String: any Sendable] = [
@@ -258,7 +292,7 @@ final class BuildDetailViewModel {
)
return result.submit.id
} catch {
- self.error = error.userFacingMessage
+ setActionError("Couldn't rebuild. \(error.userFacingMessage)")
return nil
}
}
@@ -275,12 +309,12 @@ final class BuildDetailViewModel {
let trimmedManifest = manifest.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedManifest.isEmpty else {
- error = "Paste a build manifest."
+ setActionError("Paste a build manifest.")
return nil
}
isSubmittingEditedBuild = true
- error = nil
+ dismissActionError()
defer { isSubmittingEditedBuild = false }
var variables: [String: any Sendable] = [
@@ -306,7 +340,7 @@ final class BuildDetailViewModel {
)
return result.submit.id
} catch {
- self.error = "Couldn’t submit the build. \(error.userFacingMessage)"
+ setActionError("Couldn’t submit the build. \(error.userFacingMessage)")
return nil
}
}