summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hutch.xcodeproj/project.pbxproj16
-rw-r--r--Hutch/Views/Builds/BuildDetailView.swift14
-rw-r--r--Hutch/Views/Builds/BuildDetailViewModel.swift50
3 files changed, 58 insertions, 22 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index b1ee360..51c63c4 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -515,7 +515,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 37;
+ CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.16.1;
+ MARKETING_VERSION = 2.16.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 37;
+ CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.16.1;
+ MARKETING_VERSION = 2.16.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 37;
+ CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.16.1;
+ MARKETING_VERSION = 2.16.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 37;
+ CURRENT_PROJECT_VERSION = 38;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.16.1;
+ MARKETING_VERSION = 2.16.2;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
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
}
}