gitbay/Issues/IssueCreateViewModel.swift
62 lines · 2041 bytes
1import Foundation
2import Observation
3
4/// One of `issue templates <repo>` — a body the compose sheet can start from.
5nonisolated struct IssueTemplate: Decodable, Sendable, Hashable, Identifiable {
6 let name: String
7 let body: String
8 var id: String { name }
9}
10
11/// `issue create <repo> --title <t> --file -`, and the repo's templates.
12@Observable
13@MainActor
14final class IssueCreateViewModel {
15
16 private(set) var working = false
17 private(set) var errorMessage: String?
18 private(set) var templates: [IssueTemplate] = []
19 var format: String?
20
21 private let client: GitbayClient
22 let repoPath: String
23
24 init(client: GitbayClient, repoPath: String) {
25 self.client = client
26 self.repoPath = repoPath
27 }
28
29 nonisolated private struct Created: Decodable, Sendable {
30 let number: Int64
31 }
32
33 /// `issue templates <repo>`. Templates are a convenience, so a failed
34 /// read leaves the sheet without them rather than surfacing an error.
35 func loadTemplates() async {
36 templates = (try? await client.readList(
37 ["issue", "templates", repoPath], of: IssueTemplate.self)) ?? []
38 }
39
40 /// Returns the new issue's number, or nil with `errorMessage` set.
41 func create(title: String, body: String) async -> Int64? {
42 working = true
43 errorMessage = nil
44 defer { working = false }
45 do {
46 var argv = ["issue", "create", repoPath, "--title", title]
47 var stdin: String?
48 if !body.isEmpty {
49 argv.append(contentsOf: ["--file", "-"])
50 stdin = body
51 }
52 if let format { argv.append(contentsOf: ["--format", format]) }
53 let created = try await client.run(argv, stdin: stdin, as: Created.self)
54 return created?.number
55 } catch let error as GitbayError {
56 errorMessage = error.userFacingMessage
57 } catch {
58 errorMessage = GitbayError.transport(error).userFacingMessage
59 }
60 return nil
61 }
62}