summaryrefslogtreecommitdiff
path: root/Hutch/Views/Tickets/TrackerListViewModel.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-01 22:03:14 -0500
committerChristian Cleberg <[email protected]>2026-04-01 22:03:14 -0500
commit902055d5eaa7f5512b76e22f57df3939f956181f (patch)
tree08b7bb220b980adc9eaa9c195dc26e846707b311 /Hutch/Views/Tickets/TrackerListViewModel.swift
parent209155dc431cf70d9c8dba20625bab7e3b1a5085 (diff)
downloadhutch-2.7.0.tar.gz
hutch-2.7.0.tar.bz2
hutch-2.7.0.zip
Add tracker management (delete/update, ACLs, labels)v2.7.0
- Add swipe actions (delete/edit) to tracker list - Add tracker settings menu on detail screen - Implement updateTracker and deleteTracker mutations - Implement ACL management (updateUserACL, updateTrackerACL, deleteACL) - Implement label management (createLabel, updateLabel, deleteLabel) - Add dedicated UI flows for ACLs and labels - Ensure state refresh after mutations - Handle loading, errors, and destructive confirmations Implements: https://todo.sr.ht/~ccleberg/Hutch/12
Diffstat (limited to 'Hutch/Views/Tickets/TrackerListViewModel.swift')
-rw-r--r--Hutch/Views/Tickets/TrackerListViewModel.swift112
1 files changed, 112 insertions, 0 deletions
diff --git a/Hutch/Views/Tickets/TrackerListViewModel.swift b/Hutch/Views/Tickets/TrackerListViewModel.swift
index 31c07d7..f18bb7b 100644
--- a/Hutch/Views/Tickets/TrackerListViewModel.swift
+++ b/Hutch/Views/Tickets/TrackerListViewModel.swift
@@ -11,6 +11,18 @@ private struct TrackersPage: Decodable, Sendable {
let cursor: String?
}
+private struct UpdateTrackerResponse: Decodable, Sendable {
+ let updateTracker: TrackerSummary
+}
+
+private struct DeleteTrackerResponse: Decodable, Sendable {
+ let deleteTracker: DeletedTracker
+}
+
+private struct DeletedTracker: Decodable, Sendable {
+ let id: Int
+}
+
// MARK: - View Model
@Observable
@@ -75,6 +87,28 @@ final class TrackerListViewModel {
}
"""
+ private static let updateTrackerMutation = """
+ mutation updateTracker($id: Int!, $input: TrackerInput!) {
+ updateTracker(id: $id, input: $input) {
+ id
+ rid
+ name
+ description
+ visibility
+ updated
+ owner { canonicalName }
+ }
+ }
+ """
+
+ private static let deleteTrackerMutation = """
+ mutation deleteTracker($id: Int!) {
+ deleteTracker(id: $id) {
+ id
+ }
+ }
+ """
+
// MARK: - Public API
func loadTrackers() async {
@@ -155,6 +189,84 @@ final class TrackerListViewModel {
}
}
+ func updateTracker(
+ _ tracker: TrackerSummary,
+ name: String,
+ description: String,
+ visibility: Visibility
+ ) async -> TrackerSummary? {
+ guard !isCreatingTracker else { return nil }
+
+ let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedName.isEmpty else {
+ error = "Enter a tracker name."
+ return nil
+ }
+
+ isCreatingTracker = true
+ error = nil
+ defer { isCreatingTracker = false }
+
+ let trimmedDescription = description.trimmingCharacters(in: .whitespacesAndNewlines)
+ let input: [String: any Sendable] = [
+ "name": trimmedName,
+ "description": trimmedDescription.isEmpty ? "" : trimmedDescription,
+ "visibility": visibility.rawValue
+ ]
+
+ do {
+ let result = try await client.execute(
+ service: .todo,
+ query: Self.updateTrackerMutation,
+ variables: [
+ "id": tracker.id,
+ "input": input
+ ],
+ responseType: UpdateTrackerResponse.self
+ )
+ applyTrackerUpdate(result.updateTracker)
+ return result.updateTracker
+ } catch {
+ self.error = "Couldn’t update the tracker. \(error.userFacingMessage)"
+ return nil
+ }
+ }
+
+ func deleteTracker(_ tracker: TrackerSummary) async -> Bool {
+ guard !isCreatingTracker else { return false }
+
+ isCreatingTracker = true
+ error = nil
+ defer { isCreatingTracker = false }
+
+ do {
+ _ = try await client.execute(
+ service: .todo,
+ query: Self.deleteTrackerMutation,
+ variables: ["id": tracker.id],
+ responseType: DeleteTrackerResponse.self
+ )
+ trackers.removeAll { $0.id == tracker.id }
+ await loadTrackers()
+ return true
+ } catch {
+ self.error = "Couldn’t delete the tracker. \(error.userFacingMessage)"
+ return false
+ }
+ }
+
+ func applyTrackerUpdate(_ tracker: TrackerSummary) {
+ if let index = trackers.firstIndex(where: { $0.id == tracker.id }) {
+ trackers[index] = tracker
+ } else {
+ trackers.insert(tracker, at: 0)
+ }
+ }
+
+ func applyTrackerDeletion(_ tracker: TrackerSummary) {
+ trackers.removeAll { $0.id == tracker.id }
+ }
+
// MARK: - Private
private func fetchPage(cursor: String?) async throws -> TrackersPage {