import Foundation import Observation /// `milestone list --state ` — milestones with their progress, /// plus `milestone create`, `close` and `reopen`. @Observable @MainActor final class MilestoneListViewModel { enum StateFilter: String, CaseIterable, Identifiable, Sendable { case open, closed, all var id: String { rawValue } } private(set) var state: LoadState<[Milestone]> = .loading private(set) var actionError: String? private(set) var working = false var filter: StateFilter = .open { didSet { guard filter != oldValue else { return } Task { await load() } } } private let client: GitbayClient let repoPath: String init(client: GitbayClient, repoPath: String) { self.client = client self.repoPath = repoPath } func load() async { state = .loading do { let milestones = try await client.readList( ["milestone", "list", repoPath, "--state", filter.rawValue], of: Milestone.self ) state = milestones.isEmpty ? .empty("No \(filter == .all ? "" : filter.rawValue + " ")milestones.") : .loaded(milestones) } catch { state = .from(error) } } /// `milestone create [--description <d>] [--due YYYY-MM-DD]`. /// Empty description and due are omitted rather than sent blank. func create(title: String, description: String, due: String) async -> Bool { let title = title.trimmingCharacters(in: .whitespacesAndNewlines) guard !title.isEmpty else { return false } var argv = ["milestone", "create", repoPath, title] let description = description.trimmingCharacters(in: .whitespacesAndNewlines) if !description.isEmpty { argv += ["--description", description] } let due = due.trimmingCharacters(in: .whitespacesAndNewlines) if !due.isEmpty { argv += ["--due", due] } await perform(argv) return actionError == nil } func close(_ title: String) async { await perform(["milestone", "close", repoPath, title]) } func reopen(_ title: String) async { await perform(["milestone", "reopen", repoPath, title]) } 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 } } }