import Foundation import Observation /// One of `issue templates ` — a body the compose sheet can start from. nonisolated struct IssueTemplate: Decodable, Sendable, Hashable, Identifiable { let name: String let body: String var id: String { name } } /// `issue create --title --file -`, and the repo's templates. @Observable @MainActor final class IssueCreateViewModel { private(set) var working = false private(set) var errorMessage: String? private(set) var templates: [IssueTemplate] = [] var format: String? private let client: GitbayClient let repoPath: String init(client: GitbayClient, repoPath: String) { self.client = client self.repoPath = repoPath } nonisolated private struct Created: Decodable, Sendable { let number: Int64 } /// `issue templates `. Templates are a convenience, so a failed /// read leaves the sheet without them rather than surfacing an error. func loadTemplates() async { templates = (try? await client.readList( ["issue", "templates", repoPath], of: IssueTemplate.self)) ?? [] } /// Returns the new issue's number, or nil with `errorMessage` set. func create(title: String, body: String) async -> Int64? { working = true errorMessage = nil defer { working = false } do { var argv = ["issue", "create", repoPath, "--title", title] var stdin: String? if !body.isEmpty { argv.append(contentsOf: ["--file", "-"]) stdin = body } if let format { argv.append(contentsOf: ["--format", format]) } let created = try await client.run(argv, stdin: stdin, as: Created.self) return created?.number } catch let error as GitbayError { errorMessage = error.userFacingMessage } catch { errorMessage = GitbayError.transport(error).userFacingMessage } return nil } }