gitbay/Issues/MilestoneListViewModel.swift
82 lines · 2718 bytes
1import Foundation
2import Observation
3
4/// `milestone list <repo> --state <s>` — milestones with their progress,
5/// plus `milestone create`, `close` and `reopen`.
6@Observable
7@MainActor
8final class MilestoneListViewModel {
9
10 enum StateFilter: String, CaseIterable, Identifiable, Sendable {
11 case open, closed, all
12 var id: String { rawValue }
13 }
14
15 private(set) var state: LoadState<[Milestone]> = .loading
16 private(set) var actionError: String?
17 private(set) var working = false
18 var filter: StateFilter = .open {
19 didSet {
20 guard filter != oldValue else { return }
21 Task { await load() }
22 }
23 }
24
25 private let client: GitbayClient
26 let repoPath: String
27
28 init(client: GitbayClient, repoPath: String) {
29 self.client = client
30 self.repoPath = repoPath
31 }
32
33 func load() async {
34 state = .loading
35 do {
36 let milestones = try await client.readList(
37 ["milestone", "list", repoPath, "--state", filter.rawValue], of: Milestone.self
38 )
39 state = milestones.isEmpty
40 ? .empty("No \(filter == .all ? "" : filter.rawValue + " ")milestones.")
41 : .loaded(milestones)
42 } catch {
43 state = .from(error)
44 }
45 }
46
47 /// `milestone create <repo> <title> [--description <d>] [--due YYYY-MM-DD]`.
48 /// Empty description and due are omitted rather than sent blank.
49 func create(title: String, description: String, due: String) async -> Bool {
50 let title = title.trimmingCharacters(in: .whitespacesAndNewlines)
51 guard !title.isEmpty else { return false }
52 var argv = ["milestone", "create", repoPath, title]
53 let description = description.trimmingCharacters(in: .whitespacesAndNewlines)
54 if !description.isEmpty { argv += ["--description", description] }
55 let due = due.trimmingCharacters(in: .whitespacesAndNewlines)
56 if !due.isEmpty { argv += ["--due", due] }
57 await perform(argv)
58 return actionError == nil
59 }
60
61 func close(_ title: String) async {
62 await perform(["milestone", "close", repoPath, title])
63 }
64
65 func reopen(_ title: String) async {
66 await perform(["milestone", "reopen", repoPath, title])
67 }
68
69 private func perform(_ argv: [String]) async {
70 working = true
71 actionError = nil
72 defer { working = false }
73 do {
74 try await client.run(argv)
75 await load()
76 } catch let error as GitbayError {
77 actionError = error.userFacingMessage
78 } catch {
79 actionError = GitbayError.transport(error).userFacingMessage
80 }
81 }
82}