gitbay/MRs/MRListViewModel.swift
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}