gitbay/Issues/IssueListViewModel.swift
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}