gitbay/Issues/MilestoneListViewModel.swift

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

86 lines · 2868 bytes

 1import Foundation
 2import Observation
 3
 4/// `milestone list --state <s>` — milestones with their progress, plus
 5/// `milestone create`, `close` and `reopen`, for a repository or an org.
 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 owner: LabelOwner
27
28    init(client: GitbayClient, owner: LabelOwner) {
29        self.client = client
30        self.owner = owner
31    }
32
33    convenience init(client: GitbayClient, repoPath: String) {
34        self.init(client: client, owner: .repo(repoPath))
35    }
36
37    func load() async {
38        state = .loading
39        do {
40            let milestones = try await client.readList(
41                owner.argv("milestone", "list") + ["--state", filter.rawValue], of: Milestone.self
42            )
43            state = milestones.isEmpty
44                ? .empty("No \(filter == .all ? "" : filter.rawValue + " ")milestones.")
45                : .loaded(milestones)
46        } catch {
47            state = .from(error)
48        }
49    }
50
51    /// `milestone create <title> [--description <d>] [--due YYYY-MM-DD]`.
52    /// Empty description and due are omitted rather than sent blank.
53    func create(title: String, description: String, due: String) async -> Bool {
54        let title = title.trimmingCharacters(in: .whitespacesAndNewlines)
55        guard !title.isEmpty else { return false }
56        var argv = owner.argv("milestone", "create") + [title]
57        let description = description.trimmingCharacters(in: .whitespacesAndNewlines)
58        if !description.isEmpty { argv += ["--description", description] }
59        let due = due.trimmingCharacters(in: .whitespacesAndNewlines)
60        if !due.isEmpty { argv += ["--due", due] }
61        await perform(argv)
62        return actionError == nil
63    }
64
65    func close(_ title: String) async {
66        await perform(owner.argv("milestone", "close") + [title])
67    }
68
69    func reopen(_ title: String) async {
70        await perform(owner.argv("milestone", "reopen") + [title])
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}