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 | private struct DeleteSSHKeyResponse: Decodable, Sendable { |
26 | private struct DeleteSSHKeyResponse: Decodable, Sendable { |
| 27 | let deleteSSHKey: DeleteResult |
27 | let deleteSSHKey: DeleteResult? |
| 28 | } |
28 | } |
| 29 | |
29 | |
| 30 | private struct CreatePGPKeyResponse: Decodable, Sendable { |
30 | private struct CreatePGPKeyResponse: Decodable, Sendable { |
| @@ -32,7 +32,7 @@ private struct CreatePGPKeyResponse: Decodable, Sendable { |
| 32 | } |
32 | } |
| 33 | |
33 | |
| 34 | private struct DeletePGPKeyResponse: Decodable, Sendable { |
34 | private struct DeletePGPKeyResponse: Decodable, Sendable { |
| 35 | let deletePGPKey: DeleteResult |
35 | let deletePGPKey: DeleteResult? |
| 36 | } |
36 | } |
| 37 | |
37 | |
| 38 | private struct DeleteResult: Decodable, Sendable { |
38 | private struct DeleteResult: Decodable, Sendable { |
Hutch/Views/Tickets/TrackerListView.swift
+26 −1
| @@ -124,10 +124,17 @@ private struct CreateTrackerSheet: View { |
| 124 | let onCreated: (TrackerSummary) -> Void |
124 | let onCreated: (TrackerSummary) -> Void |
| 125 | |
125 | |
| 126 | @Environment(\.dismiss) private var dismiss |
126 | @Environment(\.dismiss) private var dismiss |
| |
127 | @Bindable var viewModelBindable: TrackerListViewModel |
| 127 | @State private var name = "" |
128 | @State private var name = "" |
| 128 | @State private var description = "" |
129 | @State private var description = "" |
| 129 | @State private var visibility: Visibility = .public |
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 | var body: some View { |
138 | var body: some View { |
| 132 | NavigationStack { |
139 | NavigationStack { |
| 133 | Form { |
140 | Form { |
| @@ -143,12 +150,30 @@ private struct CreateTrackerSheet: View { |
| 143 | Text("Private").tag(Visibility.private) |
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 | .navigationTitle("New Tracker") |
166 | .navigationTitle("New Tracker") |
| 148 | .navigationBarTitleDisplayMode(.inline) |
167 | .navigationBarTitleDisplayMode(.inline) |
| |
168 | .onDisappear { |
| |
169 | viewModelBindable.error = nil |
| |
170 | } |
| 149 | .toolbar { |
171 | .toolbar { |
| 150 | ToolbarItem(placement: .cancellationAction) { |
172 | ToolbarItem(placement: .cancellationAction) { |
| 151 | Button("Cancel") { dismiss() } |
173 | Button("Cancel") { |
| |
174 | viewModelBindable.error = nil |
| |
175 | dismiss() |
| |
176 | } |
| 152 | } |
177 | } |
| 153 | ToolbarItem(placement: .confirmationAction) { |
178 | ToolbarItem(placement: .confirmationAction) { |
| 154 | Button { |
179 | Button { |
Hutch/Views/Tickets/TrackerListViewModel.swift
+18 −1
| @@ -139,7 +139,7 @@ final class TrackerListViewModel { |
| 139 | trackers.insert(tracker, at: 0) |
139 | trackers.insert(tracker, at: 0) |
| 140 | return tracker |
140 | return tracker |
| 141 | } catch { |
141 | } catch { |
| 142 | self.error = "Couldn’t create the tracker. \(error.localizedDescription)" |
142 | self.error = trackerCreationErrorMessage(for: error) |
| 143 | return nil |
143 | return nil |
| 144 | } |
144 | } |
| 145 | } |
145 | } |
| @@ -163,4 +163,21 @@ final class TrackerListViewModel { |
| 163 | private struct CreateTrackerResponse: Decodable, Sendable { |
163 | private struct CreateTrackerResponse: Decodable, Sendable { |
| 164 | let createTracker: TrackerSummary |
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 | } |