summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hutch/Views/Settings/SettingsViewModel.swift4
-rw-r--r--Hutch/Views/Tickets/TrackerListView.swift27
-rw-r--r--Hutch/Views/Tickets/TrackerListViewModel.swift19
-rw-r--r--HutchTests/SettingsViewModelTests.swift39
-rw-r--r--HutchTests/TrackerListViewModelTests.swift16
5 files changed, 101 insertions, 4 deletions
diff --git a/Hutch/Views/Settings/SettingsViewModel.swift b/Hutch/Views/Settings/SettingsViewModel.swift
index 204afbf..1cc5426 100644
--- a/Hutch/Views/Settings/SettingsViewModel.swift
+++ b/Hutch/Views/Settings/SettingsViewModel.swift
@@ -24,7 +24,7 @@ private struct CreateSSHKeyResponse: Decodable, Sendable {
}
private struct DeleteSSHKeyResponse: Decodable, Sendable {
- let deleteSSHKey: DeleteResult
+ let deleteSSHKey: DeleteResult?
}
private struct CreatePGPKeyResponse: Decodable, Sendable {
@@ -32,7 +32,7 @@ private struct CreatePGPKeyResponse: Decodable, Sendable {
}
private struct DeletePGPKeyResponse: Decodable, Sendable {
- let deletePGPKey: DeleteResult
+ let deletePGPKey: DeleteResult?
}
private struct DeleteResult: Decodable, Sendable {
diff --git a/Hutch/Views/Tickets/TrackerListView.swift b/Hutch/Views/Tickets/TrackerListView.swift
index d16246b..ee253ec 100644
--- a/Hutch/Views/Tickets/TrackerListView.swift
+++ b/Hutch/Views/Tickets/TrackerListView.swift
@@ -124,10 +124,17 @@ private struct CreateTrackerSheet: View {
let onCreated: (TrackerSummary) -> Void
@Environment(\.dismiss) private var dismiss
+ @Bindable var viewModelBindable: TrackerListViewModel
@State private var name = ""
@State private var description = ""
@State private var visibility: Visibility = .public
+ init(viewModel: TrackerListViewModel, onCreated: @escaping (TrackerSummary) -> Void) {
+ self.viewModel = viewModel
+ self._viewModelBindable = Bindable(viewModel)
+ self.onCreated = onCreated
+ }
+
var body: some View {
NavigationStack {
Form {
@@ -143,12 +150,30 @@ private struct CreateTrackerSheet: View {
Text("Private").tag(Visibility.private)
}
}
+
+ if let error = viewModel.error {
+ Section {
+ Label {
+ Text(error)
+ } icon: {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .foregroundStyle(.red)
+ }
+ .foregroundStyle(.red)
+ }
+ }
}
.navigationTitle("New Tracker")
.navigationBarTitleDisplayMode(.inline)
+ .onDisappear {
+ viewModelBindable.error = nil
+ }
.toolbar {
ToolbarItem(placement: .cancellationAction) {
- Button("Cancel") { dismiss() }
+ Button("Cancel") {
+ viewModelBindable.error = nil
+ dismiss()
+ }
}
ToolbarItem(placement: .confirmationAction) {
Button {
diff --git a/Hutch/Views/Tickets/TrackerListViewModel.swift b/Hutch/Views/Tickets/TrackerListViewModel.swift
index 9704071..978a610 100644
--- a/Hutch/Views/Tickets/TrackerListViewModel.swift
+++ b/Hutch/Views/Tickets/TrackerListViewModel.swift
@@ -139,7 +139,7 @@ final class TrackerListViewModel {
trackers.insert(tracker, at: 0)
return tracker
} catch {
- self.error = "Couldn’t create the tracker. \(error.localizedDescription)"
+ self.error = trackerCreationErrorMessage(for: error)
return nil
}
}
@@ -163,4 +163,21 @@ final class TrackerListViewModel {
private struct CreateTrackerResponse: Decodable, Sendable {
let createTracker: TrackerSummary
}
+
+ private func trackerCreationErrorMessage(for error: Error) -> String {
+ let message: String
+
+ if let srhtError = error as? SRHTError {
+ switch srhtError {
+ case .graphQLErrors(let errors):
+ message = errors.map(\.message).joined(separator: "\n")
+ default:
+ message = srhtError.localizedDescription
+ }
+ } else {
+ message = error.localizedDescription
+ }
+
+ return "Couldn’t create the tracker. \(message)"
+ }
}
diff --git a/HutchTests/SettingsViewModelTests.swift b/HutchTests/SettingsViewModelTests.swift
new file mode 100644
index 0000000..fc18162
--- /dev/null
+++ b/HutchTests/SettingsViewModelTests.swift
@@ -0,0 +1,39 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+private struct DeletePGPKeyEnvelope: Decodable {
+ let deletePGPKey: DeleteResultPayload?
+}
+
+private struct DeleteResultPayload: Decodable {
+ let id: Int?
+}
+
+struct SettingsViewModelTests {
+
+ @Test
+ @MainActor
+ func deletePGPKeyResponseDecodesNullPayloadWithGraphQLErrors() throws {
+ let json = """
+ {
+ "errors": [
+ {
+ "message": "PGP key ID 13629 is set as the user's preferred PGP key - it must be unset before removing the key"
+ }
+ ],
+ "data": {
+ "deletePGPKey": null
+ }
+ }
+ """
+
+ let decoded = try JSONDecoder().decode(
+ GraphQLResponse<DeletePGPKeyEnvelope>.self,
+ from: Data(json.utf8)
+ )
+
+ #expect(decoded.data?.deletePGPKey == nil)
+ #expect(decoded.errors?.first?.message.contains("preferred PGP key") == true)
+ }
+}
diff --git a/HutchTests/TrackerListViewModelTests.swift b/HutchTests/TrackerListViewModelTests.swift
new file mode 100644
index 0000000..1caedea
--- /dev/null
+++ b/HutchTests/TrackerListViewModelTests.swift
@@ -0,0 +1,16 @@
+import Foundation
+import Testing
+@testable import Hutch
+
+struct TrackerListViewModelTests {
+
+ @Test
+ @MainActor
+ func graphQLErrorDescriptionIsPreservedForTrackerCreationFailures() {
+ let error = SRHTError.graphQLErrors([
+ GraphQLError(message: "A tracker named bugs already exists", locations: nil)
+ ])
+
+ #expect(error.localizedDescription == "GraphQL error: A tracker named bugs already exists")
+ }
+}