a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Issues/IssueDetailViewModel.swift

main
gitbay-ios/gitbay/Issues/IssueDetailViewModel.swift history · blame · raw

99 lines · 3054 bytes

 1import Foundation
 2import Observation
 3
 4/// One issue and everything triage needs: comment, close, reopen, labels,
 5/// assignees. Writes reload; refusals surface verbatim.
 6@Observable
 7@MainActor
 8final class IssueDetailViewModel {
 9
10    private(set) var state: LoadState<IssueDetail> = .loading
11    private(set) var actionError: String?
12    private(set) var working = false
13    /// Open milestones for the picker; fetched on first use.
14    private(set) var availableMilestones: [Milestone]?
15
16    private let client: GitbayClient
17    let repoPath: String
18    let number: Int64
19
20    init(client: GitbayClient, repoPath: String, number: Int64) {
21        self.client = client
22        self.repoPath = repoPath
23        self.number = number
24    }
25
26    private var ref: [String] { [repoPath, String(number)] }
27
28    func load() async {
29        do {
30            state = .loaded(try await client.read(["issue", "show"] + ref, as: IssueDetail.self))
31        } catch {
32            state = .from(error)
33        }
34    }
35
36    /// Both fields are always sent: the title as given, the body over
37    /// stdin  an empty body clears it, matching the CLI.
38    func edit(title: String, body: String) async {
39        await perform(
40            ["issue", "edit"] + ref + ["--title", title, "--file", "-"],
41            stdin: body
42        )
43    }
44
45    /// nil clears the milestone; the command spells that "none".
46    func setMilestone(_ title: String?) async {
47        await perform(["issue", "milestone"] + ref + [title ?? "none"])
48    }
49
50    func loadMilestones() async {
51        guard availableMilestones == nil else { return }
52        availableMilestones = (try? await client.readList(
53            ["milestone", "list", repoPath, "--state", "open"], of: Milestone.self
54        )) ?? []
55    }
56
57    func comment(_ text: String) async {
58        // stdin is only read when argv carries --file -.
59        await perform(["issue", "comment"] + ref + ["--file", "-"], stdin: text)
60    }
61
62    func close() async {
63        await perform(["issue", "close"] + ref)
64    }
65
66    func reopen() async {
67        await perform(["issue", "reopen"] + ref)
68    }
69
70    func addLabel(_ label: String) async {
71        await perform(["issue", "label"] + ref + ["--add", label])
72    }
73
74    func removeLabel(_ label: String) async {
75        await perform(["issue", "label"] + ref + ["--remove", label])
76    }
77
78    func addAssignee(_ user: String) async {
79        await perform(["issue", "assign"] + ref + ["--add", user])
80    }
81
82    func removeAssignee(_ user: String) async {
83        await perform(["issue", "assign"] + ref + ["--remove", user])
84    }
85
86    private func perform(_ argv: [String], stdin: String? = nil) async {
87        working = true
88        actionError = nil
89        defer { working = false }
90        do {
91            try await client.run(argv, stdin: stdin)
92            await load()
93        } catch let error as GitbayError {
94            actionError = error.userFacingMessage
95        } catch {
96            actionError = GitbayError.transport(error).userFacingMessage
97        }
98    }
99}