import Foundation import Observation /// Whose label or milestone set a screen manages. A repository's is /// `label `; an org's is `org label `, and its /// rows are seen by every repository under the org (v1.19.0). nonisolated enum LabelOwner: Hashable, Sendable { case repo(String) case org(String) /// The command prefix for `noun` (`label` or `milestone`) and `verb`. func argv(_ noun: String, _ verb: String) -> [String] { switch self { case .repo(let path): [noun, verb, path] case .org(let name): ["org", noun, verb, name] } } var isOrg: Bool { if case .org = self { return true } return false } } /// `label list`, plus the two commands that change the set: `label set` /// creates one or recolours it, `label remove` takes it off the owner and /// off every issue that carried it. @Observable @MainActor final class LabelListViewModel { private(set) var state: LoadState<[IssueLabel]> = .loading private(set) var actionError: String? private(set) var working = false private let client: GitbayClient let owner: LabelOwner init(client: GitbayClient, owner: LabelOwner) { self.client = client self.owner = owner } convenience init(client: GitbayClient, repoPath: String) { self.init(client: client, owner: .repo(repoPath)) } func load() async { state = .loading do { let labels = try await client.readList( owner.argv("label", "list"), of: IssueLabel.self ) state = labels.isEmpty ? .empty("No labels yet.") : .loaded(labels) } catch { state = .from(error) } } /// Create a label or set its colour. A nil colour omits `--color` /// entirely, which the server reads as "make sure it exists" and /// leaves any colour it already had; an empty string clears it. func setLabel(_ name: String, color: String?) async { var argv = owner.argv("label", "set") + [name] if let color { argv.append(contentsOf: ["--color", color]) } await perform(argv) } func remove(_ name: String) async { await perform(owner.argv("label", "remove") + [name]) } private func perform(_ argv: [String]) async { working = true actionError = nil defer { working = false } do { try await client.run(argv) await load() } catch let error as GitbayError { actionError = error.userFacingMessage } catch { actionError = GitbayError.transport(error).userFacingMessage } } }