From 090c829a5f800266da7f4ff0e59fb389f0dbfdd8 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 18 Mar 2026 20:03:39 -0500 Subject: show tracker creation errors inside the creation sheet --- Hutch/Views/Tickets/TrackerListView.swift | 27 +++++++++++++++++++++++++- Hutch/Views/Tickets/TrackerListViewModel.swift | 19 +++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) (limited to 'Hutch/Views/Tickets') 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)" + } } -- cgit v1.2.3 From 90da3a7aa2bf991904c1d7201250154ea84f1493 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Wed, 18 Mar 2026 20:07:59 -0500 Subject: Omit resolution when reopening resolved tickets --- Hutch/Views/Tickets/TicketDetailView.swift | 5 +---- Hutch/Views/Tickets/TicketDetailViewModel.swift | 20 ++++++++++++----- HutchTests/TicketDetailViewModelTests.swift | 30 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 HutchTests/TicketDetailViewModelTests.swift (limited to 'Hutch/Views/Tickets') diff --git a/Hutch/Views/Tickets/TicketDetailView.swift b/Hutch/Views/Tickets/TicketDetailView.swift index 6b2be4f..f553bd8 100644 --- a/Hutch/Views/Tickets/TicketDetailView.swift +++ b/Hutch/Views/Tickets/TicketDetailView.swift @@ -71,10 +71,7 @@ struct TicketDetailView: View { if ticket.status == .resolved { Button { Task { - await viewModel.updateStatus( - status: .reported, - resolution: .unresolved - ) + await viewModel.updateStatus(status: .reported) } } label: { SwiftUI.Label("Reopen", systemImage: "arrow.uturn.backward") diff --git a/Hutch/Views/Tickets/TicketDetailViewModel.swift b/Hutch/Views/Tickets/TicketDetailViewModel.swift index 70eaf0a..90e33aa 100644 --- a/Hutch/Views/Tickets/TicketDetailViewModel.swift +++ b/Hutch/Views/Tickets/TicketDetailViewModel.swift @@ -130,6 +130,19 @@ final class TicketDetailViewModel { return lhs.created < rhs.created } + static func statusUpdateInput( + status: TicketStatus, + resolution: TicketResolution? + ) -> [String: any Sendable] { + var input: [String: any Sendable] = [ + "status": status.rawValue + ] + if status == .resolved, let resolution { + input["resolution"] = resolution.rawValue + } + return input + } + init(ownerUsername: String, trackerName: String, trackerId: Int, trackerRid: String, ticketId: Int, client: SRHTClient) { self.ownerUsername = ownerUsername self.trackerName = trackerName @@ -360,16 +373,13 @@ final class TicketDetailViewModel { // MARK: - Ticket Actions - func updateStatus(status: TicketStatus, resolution: TicketResolution) async { + func updateStatus(status: TicketStatus, resolution: TicketResolution? = nil) async { guard !isPerformingAction else { return } isPerformingAction = true error = nil do { - let input: [String: any Sendable] = [ - "status": status.rawValue, - "resolution": resolution.rawValue - ] + let input = Self.statusUpdateInput(status: status, resolution: resolution) _ = try await client.execute( service: .todo, query: Self.updateStatusMutation, diff --git a/HutchTests/TicketDetailViewModelTests.swift b/HutchTests/TicketDetailViewModelTests.swift new file mode 100644 index 0000000..bf0bc60 --- /dev/null +++ b/HutchTests/TicketDetailViewModelTests.swift @@ -0,0 +1,30 @@ +import Foundation +import Testing +@testable import Hutch + +struct TicketDetailViewModelTests { + + @Test + @MainActor + func reopenStatusInputOmitsResolution() { + let input = TicketDetailViewModel.statusUpdateInput( + status: .reported, + resolution: .unresolved + ) + + #expect(input["status"] as? String == TicketStatus.reported.rawValue) + #expect(input["resolution"] == nil) + } + + @Test + @MainActor + func resolveStatusInputIncludesResolution() { + let input = TicketDetailViewModel.statusUpdateInput( + status: .resolved, + resolution: .fixed + ) + + #expect(input["status"] as? String == TicketStatus.resolved.rawValue) + #expect(input["resolution"] as? String == TicketResolution.fixed.rawValue) + } +} -- cgit v1.2.3