gitbay/Issues/MilestoneListViewModel.swift

v1.1.0
gitbay-ios/gitbay/Issues/MilestoneListViewModel.swift history · blame · raw

82 lines · 2718 bytes

 1import Foundation
 2import Observation
 3
 4/// `milestone list <repo> --state <s>` — milestones with their progress,
 5/// plus `milestone create`, `close` and `reopen`.
 6@Observable
 7@MainActor
 8final class MilestoneListViewModel {
 9
10    enum StateFilter: String, CaseIterable, Identifiable, Sendable {
11        case open, closed, all
12        var id: String { rawValue }
13    }
14
15    private(set) var state: LoadState<[Milestone]> = .loading
16    private(set) var actionError: String?
17    private(set) var working = false
18    var filter: StateFilter = .open {
19        didSet {
20            guard filter != oldValue else { return }
21            Task { await load() }
22        }
23    }
24
25    private let client: GitbayClient
26    let repoPath: String
27
28    init(client: GitbayClient, repoPath: String) {
29        self.client = client
30        self.repoPath = repoPath
31    }
32
33    func load() async {
34        state = .loading
35        do {
36            let milestones = try await client.readList(
37                ["milestone", "list", repoPath, "--state", filter.rawValue], of: Milestone.self
38            )
39            state = milestones.isEmpty
40                ? .empty("No \(filter == .all ? "" : filter.rawValue + " ")milestones.")
41                : .loaded(milestones)
42        } catch {
43            state = .from(error)
44        }
45    }
46
47    /// `milestone create <repo> <title> [--description <d>] [--due YYYY-MM-DD]`.
48    /// Empty description and due are omitted rather than sent blank.
49    func create(title: String, description: String, due: String) async -> Bool {
50        let title = title.trimmingCharacters(in: .whitespacesAndNewlines)
51        guard !title.isEmpty else { return false }
52        var argv = ["milestone", "create", repoPath, title]
53        let description = description.trimmingCharacters(in: .whitespacesAndNewlines)
54        if !description.isEmpty { argv += ["--description", description] }
55        let due = due.trimmingCharacters(in: .whitespacesAndNewlines)
56        if !due.isEmpty { argv += ["--due", due] }
57        await perform(argv)
58        return actionError == nil
59    }
60
61    func close(_ title: String) async {
62        await perform(["milestone", "close", repoPath, title])
63    }
64
65    func reopen(_ title: String) async {
66        await perform(["milestone", "reopen", repoPath, title])
67    }
68
69    private func perform(_ argv: [String]) async {
70        working = true
71        actionError = nil
72        defer { working = false }
73        do {
74            try await client.run(argv)
75            await load()
76        } catch let error as GitbayError {
77            actionError = error.userFacingMessage
78        } catch {
79            actionError = GitbayError.transport(error).userFacingMessage
80        }
81    }
82}