gitbay/Issues/IssueListViewModel.swift

v1.0.7
gitbay-ios/gitbay/Issues/IssueListViewModel.swift history · blame · raw

91 lines · 3034 bytes

 1import Foundation
 2import Observation
 3
 4/// `issue list <repo> --state <s>`, paginated.
 5@Observable
 6@MainActor
 7final class IssueListViewModel {
 8
 9    let list: PagedListModel<Issue>
10    let repoPath: String
11    private let client: GitbayClient
12    var filter = IssueFilter() {
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.
19            var typed = oldValue
20            typed.search = filter.search
21            typed.author = filter.author
22            typed.assignee = filter.assignee
23            if typed == filter {
24                scheduleSearchReload()
25            } else {
26                reloadTask?.cancel()
27                reloadTask = Task { await list.reload() }
28            }
29        }
30    }
31
32    /// This repository's labels, loaded beside the issues: the names for
33    /// a picker, the colours for the chips. A failure here is not fatal —
34    /// every chip then derives its colour from its name.
35    private(set) var labels: [IssueLabel] = []
36    private(set) var colors = LabelColors()
37    /// Labels rarely change mid-session: fetch them once per view model,
38    /// not on every reload.
39    private var labelsLoaded = false
40
41    /// Open and closed milestones for the filter picker; fetched on first use.
42    private(set) var milestones: [Milestone] = []
43    private var reloadTask: Task<Void, Never>?
44
45    init(client: GitbayClient, repoPath: String) {
46        self.client = client
47        self.repoPath = repoPath
48        list = PagedListModel(
49            client: client,
50            argv: ["issue", "list", repoPath] + IssueFilter().flags(),
51            emptyMessage: IssueFilter().emptyMessage
52        )
53    }
54
55    var state: LoadState<[Issue]> { list.state }
56
57    func load() async {
58        reloadTask?.cancel()
59        await list.reload()
60        if case .failed = list.state { return }
61        guard !labelsLoaded else { return }
62        labelsLoaded = true
63        labels = await labelList()
64        colors = LabelColors(labels)
65    }
66
67    func loadMilestones() async {
68        guard milestones.isEmpty else { return }
69        milestones = (try? await client.readList(
70            ["milestone", "list", repoPath, "--state", "all"], of: Milestone.self
71        )) ?? []
72    }
73
74    private func labelList() async -> [IssueLabel] {
75        (try? await client.readList(["label", "list", repoPath], of: IssueLabel.self)) ?? []
76    }
77
78    private func scheduleSearchReload() {
79        reloadTask?.cancel()
80        reloadTask = Task {
81            guard (try? await Task.sleep(for: .milliseconds(300))) != nil else { return }
82            guard !Task.isCancelled else { return }
83            await list.reload()
84        }
85    }
86
87    private func configureList() {
88        list.argv = ["issue", "list", repoPath] + filter.flags()
89        list.emptyMessage = filter.emptyMessage
90    }
91}