Commit bdc1800126
bdc1800126f35631c4dad1dfd195a77ee40bdeb8
parent: b5af561d68
Unsigned
cmc <hello@cleberg.net> · 2026-03-19 01:03 UTC
show tracker creation errors inside the creation sheet
Layout: unified · split
Hutch/Views/Settings/SettingsViewModel.swift
+2 −2
| @@ -24,7 +24,7 @@ private struct CreateSSHKeyResponse: Decodable, Sendable { |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | 26 | private struct DeleteSSHKeyResponse: Decodable, Sendable { |
| 27 | | let deleteSSHKey: DeleteResult |
| 27 | let deleteSSHKey: DeleteResult? |
| 28 | 28 | } |
| 29 | 29 | |
| 30 | 30 | private struct CreatePGPKeyResponse: Decodable, Sendable { |
| @@ -32,7 +32,7 @@ private struct CreatePGPKeyResponse: Decodable, Sendable { |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | 34 | private struct DeletePGPKeyResponse: Decodable, Sendable { |
| 35 | | let deletePGPKey: DeleteResult |
| 35 | let deletePGPKey: DeleteResult? |
| 36 | 36 | } |
| 37 | 37 | |
| 38 | 38 | private struct DeleteResult: Decodable, Sendable { |
Hutch/Views/Tickets/TrackerListView.swift
+26 −1
| @@ -124,10 +124,17 @@ private struct CreateTrackerSheet: View { |
| 124 | 124 | let onCreated: (TrackerSummary) -> Void |
| 125 | 125 | |
| 126 | 126 | @Environment(\.dismiss) private var dismiss |
| 127 | @Bindable var viewModelBindable: TrackerListViewModel |
| 127 | 128 | @State private var name = "" |
| 128 | 129 | @State private var description = "" |
| 129 | 130 | @State private var visibility: Visibility = .public |
| 130 | 131 | |
| 132 | init(viewModel: TrackerListViewModel, onCreated: @escaping (TrackerSummary) -> Void) { |
| 133 | self.viewModel = viewModel |
| 134 | self._viewModelBindable = Bindable(viewModel) |
| 135 | self.onCreated = onCreated |
| 136 | } |
| 137 | |
| 131 | 138 | var body: some View { |
| 132 | 139 | NavigationStack { |
| 133 | 140 | Form { |
| @@ -143,12 +150,30 @@ private struct CreateTrackerSheet: View { |
| 143 | 150 | Text("Private").tag(Visibility.private) |
| 144 | 151 | } |
| 145 | 152 | } |
| 153 | |
| 154 | if let error = viewModel.error { |
| 155 | Section { |
| 156 | Label { |
| 157 | Text(error) |
| 158 | } icon: { |
| 159 | Image(systemName: "exclamationmark.triangle.fill") |
| 160 | .foregroundStyle(.red) |
| 161 | } |
| 162 | .foregroundStyle(.red) |
| 163 | } |
| 164 | } |
| 146 | 165 | } |
| 147 | 166 | .navigationTitle("New Tracker") |
| 148 | 167 | .navigationBarTitleDisplayMode(.inline) |
| 168 | .onDisappear { |
| 169 | viewModelBindable.error = nil |
| 170 | } |
| 149 | 171 | .toolbar { |
| 150 | 172 | ToolbarItem(placement: .cancellationAction) { |
| 151 | | Button("Cancel") { dismiss() } |
| 173 | Button("Cancel") { |
| 174 | viewModelBindable.error = nil |
| 175 | dismiss() |
| 176 | } |
| 152 | 177 | } |
| 153 | 178 | ToolbarItem(placement: .confirmationAction) { |
| 154 | 179 | Button { |
Hutch/Views/Tickets/TrackerListViewModel.swift
+18 −1
| @@ -139,7 +139,7 @@ final class TrackerListViewModel { |
| 139 | 139 | trackers.insert(tracker, at: 0) |
| 140 | 140 | return tracker |
| 141 | 141 | } catch { |
| 142 | | self.error = "Couldn’t create the tracker. \(error.localizedDescription)" |
| 142 | self.error = trackerCreationErrorMessage(for: error) |
| 143 | 143 | return nil |
| 144 | 144 | } |
| 145 | 145 | } |
| @@ -163,4 +163,21 @@ final class TrackerListViewModel { |
| 163 | 163 | private struct CreateTrackerResponse: Decodable, Sendable { |
| 164 | 164 | let createTracker: TrackerSummary |
| 165 | 165 | } |
| 166 | |
| 167 | private func trackerCreationErrorMessage(for error: Error) -> String { |
| 168 | let message: String |
| 169 | |
| 170 | if let srhtError = error as? SRHTError { |
| 171 | switch srhtError { |
| 172 | case .graphQLErrors(let errors): |
| 173 | message = errors.map(\.message).joined(separator: "\n") |
| 174 | default: |
| 175 | message = srhtError.localizedDescription |
| 176 | } |
| 177 | } else { |
| 178 | message = error.localizedDescription |
| 179 | } |
| 180 | |
| 181 | return "Couldn’t create the tracker. \(message)" |
| 182 | } |
| 166 | 183 | } |
HutchTests/SettingsViewModelTests.swift
added
+39
| @@ -0,0 +1,39 @@ |
| 1 | import Foundation |
| 2 | import Testing |
| 3 | @testable import Hutch |
| 4 | |
| 5 | private struct DeletePGPKeyEnvelope: Decodable { |
| 6 | let deletePGPKey: DeleteResultPayload? |
| 7 | } |
| 8 | |
| 9 | private struct DeleteResultPayload: Decodable { |
| 10 | let id: Int? |
| 11 | } |
| 12 | |
| 13 | struct SettingsViewModelTests { |
| 14 | |
| 15 | @Test |
| 16 | @MainActor |
| 17 | func deletePGPKeyResponseDecodesNullPayloadWithGraphQLErrors() throws { |
| 18 | let json = """ |
| 19 | { |
| 20 | "errors": [ |
| 21 | { |
| 22 | "message": "PGP key ID 13629 is set as the user's preferred PGP key - it must be unset before removing the key" |
| 23 | } |
| 24 | ], |
| 25 | "data": { |
| 26 | "deletePGPKey": null |
| 27 | } |
| 28 | } |
| 29 | """ |
| 30 | |
| 31 | let decoded = try JSONDecoder().decode( |
| 32 | GraphQLResponse<DeletePGPKeyEnvelope>.self, |
| 33 | from: Data(json.utf8) |
| 34 | ) |
| 35 | |
| 36 | #expect(decoded.data?.deletePGPKey == nil) |
| 37 | #expect(decoded.errors?.first?.message.contains("preferred PGP key") == true) |
| 38 | } |
| 39 | } |
HutchTests/TrackerListViewModelTests.swift
added
+16
| @@ -0,0 +1,16 @@ |
| 1 | import Foundation |
| 2 | import Testing |
| 3 | @testable import Hutch |
| 4 | |
| 5 | struct TrackerListViewModelTests { |
| 6 | |
| 7 | @Test |
| 8 | @MainActor |
| 9 | func graphQLErrorDescriptionIsPreservedForTrackerCreationFailures() { |
| 10 | let error = SRHTError.graphQLErrors([ |
| 11 | GraphQLError(message: "A tracker named bugs already exists", locations: nil) |
| 12 | ]) |
| 13 | |
| 14 | #expect(error.localizedDescription == "GraphQL error: A tracker named bugs already exists") |
| 15 | } |
| 16 | } |