gitbay/Issues/MilestoneListViewModel.swift
86 lines · 2868 bytes
1import Foundation
2import Observation
3
4/// `milestone list --state <s>` — milestones with their progress, plus
5/// `milestone create`, `close` and `reopen`, for a repository or an org.
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 owner: LabelOwner
27
28 init(client: GitbayClient, owner: LabelOwner) {
29 self.client = client
30 self.owner = owner
31 }
32
33 convenience init(client: GitbayClient, repoPath: String) {
34 self.init(client: client, owner: .repo(repoPath))
35 }
36
37 func load() async {
38 state = .loading
39 do {
40 let milestones = try await client.readList(
41 owner.argv("milestone", "list") + ["--state", filter.rawValue], of: Milestone.self
42 )
43 state = milestones.isEmpty
44 ? .empty("No \(filter == .all ? "" : filter.rawValue + " ")milestones.")
45 : .loaded(milestones)
46 } catch {
47 state = .from(error)
48 }
49 }
50
51 /// `milestone create <title> [--description <d>] [--due YYYY-MM-DD]`.
52 /// Empty description and due are omitted rather than sent blank.
53 func create(title: String, description: String, due: String) async -> Bool {
54 let title = title.trimmingCharacters(in: .whitespacesAndNewlines)
55 guard !title.isEmpty else { return false }
56 var argv = owner.argv("milestone", "create") + [title]
57 let description = description.trimmingCharacters(in: .whitespacesAndNewlines)
58 if !description.isEmpty { argv += ["--description", description] }
59 let due = due.trimmingCharacters(in: .whitespacesAndNewlines)
60 if !due.isEmpty { argv += ["--due", due] }
61 await perform(argv)
62 return actionError == nil
63 }
64
65 func close(_ title: String) async {
66 await perform(owner.argv("milestone", "close") + [title])
67 }
68
69 func reopen(_ title: String) async {
70 await perform(owner.argv("milestone", "reopen") + [title])
71 }
72
73 private func perform(_ argv: [String]) async {
74 working = true
75 actionError = nil
76 defer { working = false }
77 do {
78 try await client.run(argv)
79 await load()
80 } catch let error as GitbayError {
81 actionError = error.userFacingMessage
82 } catch {
83 actionError = GitbayError.transport(error).userFacingMessage
84 }
85 }
86}