import Foundation import Observation /// One issue and everything triage needs: comment, close, reopen, labels, /// assignees. Writes reload; refusals surface verbatim. @Observable @MainActor final class IssueDetailViewModel { private(set) var state: LoadState = .loading private(set) var actionError: String? private(set) var working = false /// Open milestones for the picker; fetched on first use. private(set) var availableMilestones: [Milestone]? private let client: GitbayClient let repoPath: String let number: Int64 init(client: GitbayClient, repoPath: String, number: Int64) { self.client = client self.repoPath = repoPath self.number = number } private var ref: [String] { [repoPath, String(number)] } func load() async { do { state = .loaded(try await client.read(["issue", "show"] + ref, as: IssueDetail.self)) } catch { state = .from(error) } } /// Both fields are always sent: the title as given, the body over /// stdin — an empty body clears it, matching the CLI. func edit(title: String, body: String) async { await perform( ["issue", "edit"] + ref + ["--title", title, "--file", "-"], stdin: body ) } /// nil clears the milestone; the command spells that "none". func setMilestone(_ title: String?) async { await perform(["issue", "milestone"] + ref + [title ?? "none"]) } func loadMilestones() async { guard availableMilestones == nil else { return } availableMilestones = (try? await client.readList( ["milestone", "list", repoPath, "--state", "open"], of: Milestone.self )) ?? [] } func comment(_ text: String) async { // stdin is only read when argv carries --file -. await perform(["issue", "comment"] + ref + ["--file", "-"], stdin: text) } func close() async { await perform(["issue", "close"] + ref) } func reopen() async { await perform(["issue", "reopen"] + ref) } func addLabel(_ label: String) async { await perform(["issue", "label"] + ref + ["--add", label]) } func removeLabel(_ label: String) async { await perform(["issue", "label"] + ref + ["--remove", label]) } func addAssignee(_ user: String) async { await perform(["issue", "assign"] + ref + ["--add", user]) } func removeAssignee(_ user: String) async { await perform(["issue", "assign"] + ref + ["--remove", user]) } private func perform(_ argv: [String], stdin: String? = nil) async { working = true actionError = nil defer { working = false } do { try await client.run(argv, stdin: stdin) await load() } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } }