diff options
| author | Christian Cleberg <[email protected]> | 2026-04-01 22:03:14 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-01 22:03:14 -0500 |
| commit | 902055d5eaa7f5512b76e22f57df3939f956181f (patch) | |
| tree | 08b7bb220b980adc9eaa9c195dc26e846707b311 /Hutch/Views/Tickets/TrackerListViewModel.swift | |
| parent | 209155dc431cf70d9c8dba20625bab7e3b1a5085 (diff) | |
| download | hutch-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.swift | 112 |
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 { |
