gitbay/MRs/MRListViewModel.swift

v1.8.0
gitbay-ios/gitbay/MRs/MRListViewModel.swift history · blame · raw

92 lines · 2936 bytes

 1import Foundation
 2import Observation
 3
 4/// `mr list <repo> --state <s>`, paginated.
 5@Observable
 6@MainActor
 7final class MRListViewModel {
 8
 9    let list: PagedListModel<MergeRequest>
10    let repoPath: String
11    private let client: GitbayClient
12    var filter = MRFilter() {
13        didSet {
14            guard filter != oldValue else { return }
15            configureList()
16            // Only the free-text fields changed: debounce, so typing does
17            // not fire a request per keystroke. Anything else — a picker —
18            // reloads now. MRFilter has no `assignee` field: `mr list`
19            // rejects it.
20            var typed = oldValue
21            typed.search = filter.search
22            typed.author = filter.author
23            if typed == filter {
24                scheduleSearchReload()
25            } else {
26                reloadTask?.cancel()
27                reloadTask = Task { await list.reload() }
28            }
29        }
30    }
31
32    /// Open and closed milestones for the filter picker; fetched on first use.
33    private(set) var milestones: [Milestone] = []
34    private var reloadTask: Task<Void, Never>?
35
36    init(client: GitbayClient, repoPath: String) {
37        self.client = client
38        self.repoPath = repoPath
39        list = PagedListModel(
40            client: client,
41            argv: ["mr", "list", repoPath] + MRFilter().flags(),
42            emptyMessage: MRFilter().emptyMessage
43        )
44    }
45
46    var state: LoadState<[MergeRequest]> { list.state }
47
48    func load() async {
49        reloadTask?.cancel()
50        await list.reload()
51    }
52
53    /// The list's `.task`: reads only while nothing is shown yet.
54    func loadIfNeeded() async {
55        guard list.needsLoad else { return }
56        await load()
57    }
58
59    func loadMilestones() async {
60        guard milestones.isEmpty else { return }
61        milestones = (try? await client.readList(
62            ["milestone", "list", repoPath, "--state", "all"], of: Milestone.self
63        )) ?? []
64    }
65
66    /// The repository's labels for the filter picker and its chips;
67    /// fetched on first use, like the milestones.
68    private(set) var labels: [IssueLabel] = []
69    private(set) var colors = LabelColors()
70    private var labelsLoaded = false
71
72    func loadLabels() async {
73        guard !labelsLoaded else { return }
74        labelsLoaded = true
75        labels = (try? await client.readList(["label", "list", repoPath], of: IssueLabel.self)) ?? []
76        colors = LabelColors(labels)
77    }
78
79    private func scheduleSearchReload() {
80        reloadTask?.cancel()
81        reloadTask = Task {
82            guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return }
83            guard !Task.isCancelled else { return }
84            await list.reload()
85        }
86    }
87
88    private func configureList() {
89        list.argv = ["mr", "list", repoPath] + filter.flags()
90        list.emptyMessage = filter.emptyMessage
91    }
92}