gitbay/Issues/IssueDetailViewModel.swift
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}