gitbay/Issues/LabelListViewModel.swift

v1.6.2
gitbay-ios/gitbay/Issues/LabelListViewModel.swift history · blame · raw

86 lines · 2693 bytes

 1import Foundation
 2import Observation
 3
 4/// Whose label or milestone set a screen manages. A repository's is
 5/// `label <verb> <repo>`; an org's is `org label <verb> <org>`, and its
 6/// rows are seen by every repository under the org (v1.19.0).
 7nonisolated enum LabelOwner: Hashable, Sendable {
 8    case repo(String)
 9    case org(String)
10
11    /// The command prefix for `noun` (`label` or `milestone`) and `verb`.
12    func argv(_ noun: String, _ verb: String) -> [String] {
13        switch self {
14        case .repo(let path): [noun, verb, path]
15        case .org(let name): ["org", noun, verb, name]
16        }
17    }
18
19    var isOrg: Bool {
20        if case .org = self { return true }
21        return false
22    }
23}
24
25/// `label list`, plus the two commands that change the set: `label set`
26/// creates one or recolours it, `label remove` takes it off the owner and
27/// off every issue that carried it.
28@Observable
29@MainActor
30final class LabelListViewModel {
31
32    private(set) var state: LoadState<[IssueLabel]> = .loading
33    private(set) var actionError: String?
34    private(set) var working = false
35
36    private let client: GitbayClient
37    let owner: LabelOwner
38
39    init(client: GitbayClient, owner: LabelOwner) {
40        self.client = client
41        self.owner = owner
42    }
43
44    convenience init(client: GitbayClient, repoPath: String) {
45        self.init(client: client, owner: .repo(repoPath))
46    }
47
48    func load() async {
49        state = .loading
50        do {
51            let labels = try await client.readList(
52                owner.argv("label", "list"), of: IssueLabel.self
53            )
54            state = labels.isEmpty ? .empty("No labels yet.") : .loaded(labels)
55        } catch {
56            state = .from(error)
57        }
58    }
59
60    /// Create a label or set its colour. A nil colour omits `--color`
61    /// entirely, which the server reads as "make sure it exists" and
62    /// leaves any colour it already had; an empty string clears it.
63    func setLabel(_ name: String, color: String?) async {
64        var argv = owner.argv("label", "set") + [name]
65        if let color { argv.append(contentsOf: ["--color", color]) }
66        await perform(argv)
67    }
68
69    func remove(_ name: String) async {
70        await perform(owner.argv("label", "remove") + [name])
71    }
72
73    private func perform(_ argv: [String]) async {
74        working = true
75        actionError = nil
76        defer { working = false }
77        do {
78            try await client.run(argv)
79            await load()
80        } catch let error as GitbayError {
81            actionError = error.userFacingMessage
82        } catch {
83            actionError = GitbayError.transport(error).userFacingMessage
84        }
85    }
86}