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 {
2424}
2525
2626private struct DeleteSSHKeyResponse: Decodable, Sendable {
27 let deleteSSHKey: DeleteResult
27 let deleteSSHKey: DeleteResult?
2828}
2929
3030private struct CreatePGPKeyResponse: Decodable, Sendable {
@@ -32,7 +32,7 @@ private struct CreatePGPKeyResponse: Decodable, Sendable {
3232}
3333
3434private struct DeletePGPKeyResponse: Decodable, Sendable {
35 let deletePGPKey: DeleteResult
35 let deletePGPKey: DeleteResult?
3636}
3737
3838private struct DeleteResult: Decodable, Sendable {
Hutch/Views/Tickets/TrackerListView.swift +26 −1
@@ -124,10 +124,17 @@ private struct CreateTrackerSheet: View {
124124 let onCreated: (TrackerSummary) -> Void
125125
126126 @Environment(\.dismiss) private var dismiss
127 @Bindable var viewModelBindable: TrackerListViewModel
127128 @State private var name = ""
128129 @State private var description = ""
129130 @State private var visibility: Visibility = .public
130131
132 init(viewModel: TrackerListViewModel, onCreated: @escaping (TrackerSummary) -> Void) {
133 self.viewModel = viewModel
134 self._viewModelBindable = Bindable(viewModel)
135 self.onCreated = onCreated
136 }
137
131138 var body: some View {
132139 NavigationStack {
133140 Form {
@@ -143,12 +150,30 @@ private struct CreateTrackerSheet: View {
143150 Text("Private").tag(Visibility.private)
144151 }
145152 }
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 }
146165 }
147166 .navigationTitle("New Tracker")
148167 .navigationBarTitleDisplayMode(.inline)
168 .onDisappear {
169 viewModelBindable.error = nil
170 }
149171 .toolbar {
150172 ToolbarItem(placement: .cancellationAction) {
151 Button("Cancel") { dismiss() }
173 Button("Cancel") {
174 viewModelBindable.error = nil
175 dismiss()
176 }
152177 }
153178 ToolbarItem(placement: .confirmationAction) {
154179 Button {
Hutch/Views/Tickets/TrackerListViewModel.swift +18 −1
@@ -139,7 +139,7 @@ final class TrackerListViewModel {
139139 trackers.insert(tracker, at: 0)
140140 return tracker
141141 } catch {
142 self.error = "Couldn’t create the tracker. \(error.localizedDescription)"
142 self.error = trackerCreationErrorMessage(for: error)
143143 return nil
144144 }
145145 }
@@ -163,4 +163,21 @@ final class TrackerListViewModel {
163163 private struct CreateTrackerResponse: Decodable, Sendable {
164164 let createTracker: TrackerSummary
165165 }
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 }
166183}
HutchTests/SettingsViewModelTests.swift added +39
@@ -0,0 +1,39 @@
1import Foundation
2import Testing
3@testable import Hutch
4
5private struct DeletePGPKeyEnvelope: Decodable {
6 let deletePGPKey: DeleteResultPayload?
7}
8
9private struct DeleteResultPayload: Decodable {
10 let id: Int?
11}
12
13struct 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 @@
1import Foundation
2import Testing
3@testable import Hutch
4
5struct 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}